From 67508f66670309f55fba1b75b56cf914ecac1035 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Tue, 23 Jun 2020 12:36:43 +0200 Subject: [PATCH 01/21] fix: Make docker build properly --- Dockerfile | 3 ++- yarn.lock | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index f989e6881..854b42faa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ # Stage 1: Build the application # docker build -t ohif/viewer:latest . -FROM node:10.16.3-slim as builder +FROM node:14.3.0-slim as builder RUN mkdir /usr/src/app WORKDIR /usr/src/app @@ -30,6 +30,7 @@ WORKDIR /usr/src/app COPY .docker /usr/src/app/.docker COPY .webpack /usr/src/app/.webpack COPY extensions /usr/src/app/extensions +COPY modes /usr/src/app/modes COPY platform /usr/src/app/platform COPY .browserslistrc /usr/src/app/.browserslistrc COPY aliases.config.js /usr/src/app/aliases.config.js diff --git a/yarn.lock b/yarn.lock index b909ed6c5..eb88b95ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17889,16 +17889,6 @@ react-cornerstone-viewport@2.3.8: prop-types "^15.7.2" react-resize-detector "^4.2.1" -react-cornerstone-viewport@2.3.9: - version "2.3.9" - resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.9.tgz#f9761da8e536f0a217137c6ca1a983f5882249f9" - integrity sha512-qrhq8CbX/jq6b93cQjV2qC/mhHOvFBpxzxcHlvHzEQZt/rmRMcaYxCqjpNaNbUmmBu61wNkxesUVsggkPTTcqg== - dependencies: - classnames "^2.2.6" - date-fns "^2.2.1" - prop-types "^15.7.2" - react-resize-detector "^4.2.1" - react-dates@21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8" From 911f93d378c161ff5e75bde000dc1c8ee1079631 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Wed, 24 Jun 2020 12:11:54 -0300 Subject: [PATCH 02/21] Update toolbar service and add active icon in nested button --- .../ViewerLayout/ToolbarButtonNestedMenu.jsx | 36 +++++++++++-------- extensions/default/src/ViewerLayout/index.jsx | 13 ++++--- .../services/ToolBarService/ToolBarService.js | 15 ++++---- .../ToolbarButton/ToolbarButton.jsx | 1 + 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx index 097257e75..c6d8f7a35 100644 --- a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx +++ b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx @@ -2,32 +2,31 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { ToolbarButton } from '@ohif/ui'; -function NestedMenu({ children }) { +function NestedMenu({ children, label, icon }) { const [isOpen, setIsOpen] = useState(false); - useEffect(() => { - function closeNestedMenu() { - if (isOpen) { - setIsOpen(false); - } + const toggleNestedMenu = () => setIsOpen(!isOpen); + + const closeNestedMenu = () => { + if (isOpen) { + setIsOpen(false); } + }; + + useEffect(() => { window.addEventListener('click', closeNestedMenu); return () => { window.removeEventListener('click', closeNestedMenu); }; }, [isOpen]); - const dropdownContent = isOpen ? children : undefined; - return ( { - setIsOpen(!isOpen); - }} - dropdownContent={dropdownContent} + id="NestedMenu" + label={label} + icon={icon} + onClick={toggleNestedMenu} + dropdownContent={isOpen && children} isActive={isOpen} type="primary" /> @@ -36,6 +35,13 @@ function NestedMenu({ children }) { NestedMenu.propTypes = { children: PropTypes.any.isRequired, + icon: PropTypes.string, + label: PropTypes.string, +}; + +NestedMenu.defaultProps = { + icon: "tool-more-menu", + label: "More", }; export default NestedMenu; diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 730ee515b..8a92a2d93 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -55,7 +55,13 @@ function ViewerLayout({ }; }; + const defaultTool = { icon: 'tool-more-menu', label: 'More' }; const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); + const [activeTool, setActiveTool] = useState(defaultTool); + const onSecondaryClickHandler = () => setActiveTool(defaultTool); + const onPrimaryClickHandler = (evt, btn) => { + setActiveTool(btn.props.isActive ? btn.props : defaultTool); + }; useEffect(() => { const { unsubscribe } = ToolBarService.subscribe( @@ -63,8 +69,8 @@ function ViewerLayout({ () => { console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary'), - secondary: ToolBarService.getButtonSection('secondary'), + primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler }), + secondary: ToolBarService.getButtonSection('secondary', { onClick: onSecondaryClickHandler }), }; setToolbars(updatedToolbars); } @@ -86,11 +92,10 @@ function ViewerLayout({ if (!isNested) { const { id, Component, componentProps } = toolDef; - return ; } else { return ( - +
{toolDef.map(x => { const { id, Component, componentProps } = x; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 95cc1b431..589839448 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -62,7 +62,7 @@ export default class ToolBarService { this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); } - getButtonSection(key) { + getButtonSection(key, props) { const buttonSectionIds = this.buttonSections[key]; const buttonsInSection = []; @@ -79,7 +79,7 @@ export default class ToolBarService { btnIds.forEach(nestedBtnId => { const nestedBtn = this.buttons[nestedBtnId]; - const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key); + const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, props); nestedButtons.push(mappedNestedBtn); }); @@ -90,7 +90,7 @@ export default class ToolBarService { } else { const btnId = btnIdOrArray; const btn = this.buttons[btnId]; - const mappedBtn = this._mapButtonToDisplay(btn, key); + const mappedBtn = this._mapButtonToDisplay(btn, key, props); buttonsInSection.push(mappedBtn); } @@ -136,8 +136,8 @@ export default class ToolBarService { * @param {*} btn * @param {*} btnSection */ - _mapButtonToDisplay(btn, btnSection) { - const { id, type, component, props } = btn; + _mapButtonToDisplay(btn, btnSection, props) { + const { id, type, component } = btn; const buttonType = this._buttonTypes()[type]; if (!buttonType) { @@ -154,12 +154,15 @@ export default class ToolBarService { if (btn.props.clickHandler) { btn.clickHandler(evt, btn, btnSection); } + if (props && props.onClick) { + props.onClick(evt, btn, btnSection, props); + } }; return { id, Component: component || buttonType.defaultComponent, - componentProps: Object.assign({}, props, { onClick }), // + componentProps: Object.assign({}, btn.props, props, { onClick }), // }; } } diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx index 904d0dcf3..3ce1b20e1 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx @@ -25,6 +25,7 @@ const ToolbarButton = ({ }; const shouldShowDropdown = !!isActive && !!dropdownContent; + return (
Date: Thu, 25 Jun 2020 11:15:33 +0100 Subject: [PATCH 03/21] WIP Measurements service. --- extensions/cornerstone/src/init.js | 47 +++-- .../ArrowAnnotate.js | 49 +++++ .../Bidirectional.js | 50 +++++ .../EllipticalRoi.js | 74 +++++++ .../measurementServiceMappings/Length.js | 79 ++++++++ .../constants/supportedTools.js | 1 + .../measurementServiceMappingsFactory.js | 190 +++++++----------- .../utils/getHandlesFromPoints.js | 5 + .../utils/getPointsFromHandles.js | 12 ++ .../utils/getSOPInstanceAttributes.js | 14 ++ .../PanelMeasurementTableTracking/index.js | 64 ++++-- .../MeasurementService/MeasurementService.js | 5 + .../MeasurementTable/MeasurementTable.jsx | 14 +- 13 files changed, 453 insertions(+), 151 deletions(-) create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/Length.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 48809b2b4..5f80a9d6f 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -90,6 +90,7 @@ export default function init({ servicesManager, configuration }) { /* Add extension tools configuration here. */ const internalToolsConfig = { + /* TODO ArrowAnnotate input ArrowAnnotate: { configuration: { getTextCallback: (callback, eventDetails) => @@ -98,6 +99,7 @@ export default function init({ servicesManager, configuration }) { callInputDialog(data, eventDetails, callback), }, }, + */ }; /* Abstract tools configuration using extension configuration. */ @@ -166,27 +168,48 @@ export default function init({ servicesManager, configuration }) { const _initMeasurementService = measurementService => { /* Initialization */ - const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory( - measurementService - ); + const { + Length, + Bidirectional, + EllipticalRoi, + ArrowAnnotate, + } = measurementServiceMappingsFactory(measurementService); const csToolsVer4MeasurementSource = measurementService.createSource( 'CornerstoneTools', '4' ); - /* Matching Criterias */ - const matchingCriteria = { - valueType: measurementService.VALUE_TYPES.POLYLINE, - points: 2, - }; - /* Mappings */ measurementService.addMapping( csToolsVer4MeasurementSource, 'Length', - matchingCriteria, - toAnnotation, - toMeasurement + Length.matchingCriteria, + Length.toAnnotation, + Length.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'Bidirectional', + Bidirectional.matchingCriteria, + Bidirectional.toAnnotation, + Bidirectional.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'EllipticalRoi', + EllipticalRoi.matchingCriteria, + EllipticalRoi.toAnnotation, + EllipticalRoi.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'ArrowAnnotate', + ArrowAnnotate.matchingCriteria, + ArrowAnnotate.toAnnotation, + ArrowAnnotate.toMeasurement ); return csToolsVer4MeasurementSource; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js new file mode 100644 index 000000000..b1d9f2f44 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -0,0 +1,49 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; +import getPointsFromHandles from './utils/getPointsFromHandles'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const ArrowAnnotate = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const points = []; + points.push(measurementData.handles); + + return { + id: measurementData._measurementServiceId, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + text: measurementData.text, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; + }, +}; + +export default ArrowAnnotate; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js new file mode 100644 index 000000000..5b6f3e893 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js @@ -0,0 +1,50 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const Bidirectional = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const { handles } = measurementData; + + const longAxis = [handles.start, handles.end]; + const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; + + return { + id: measurementData._measurementServiceId, + 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 }, + }; + }, +}; + +export default Bidirectional; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js new file mode 100644 index 000000000..dbd4d2403 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js @@ -0,0 +1,74 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const EllipticalRoi = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + 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._measurementServiceId, + 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, + }; + }, +}; + +export default EllipticalRoi; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js new file mode 100644 index 000000000..5f9eda649 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -0,0 +1,79 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; +import getPointsFromHandles from './utils/getPointsFromHandles'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const Length = { + toAnnotation: (measurement, definition) => { + const { + id, + label, + description, + points, + unit, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID, + } = measurement; + + return { + toolName: definition, + measurementData: { + sopInstanceUid: SOPInstanceUID, + frameOfReferenceUID: FrameOfReferenceUID, + SeriesInstanceUID: referenceSeriesUID, + unit, + text: label, + description, + handles: getHandlesFromPoints(points), + _measurementServiceId: id, + }, + }; + }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const points = []; + points.push(measurementData.handles); + + return { + id: measurementData._measurementServiceId, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; + }, +}; + +export default Length; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js new file mode 100644 index 000000000..a1ca5e83e --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -0,0 +1 @@ +export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate']; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index f279eaec0..6e325fb19 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -1,11 +1,7 @@ -import cornerstone from 'cornerstone-core'; - -const SUPPORTED_TOOLS = [ - 'Length', - 'EllipticalRoi', - 'RectangleRoi', - 'ArrowAnnotate', -]; +import Length from './Length'; +import Bidirectional from './Bidirectional'; +import ArrowAnnotate from './ArrowAnnotate'; +import EllipticalRoi from './EllipticalRoi'; const measurementServiceMappingsFactory = measurementService => { /** @@ -15,129 +11,87 @@ const measurementServiceMappingsFactory = measurementService => { * @param {string} definition The source definition * @return {Object} Cornerstone annotation data */ - const toAnnotation = (measurement, definition) => { - const { - id, - label, - description, - points, - unit, - SOPInstanceUID, - FrameOfReferenceUID, - referenceSeriesUID, - } = measurement; - - return { - toolName: definition, - measurementData: { - sopInstanceUid: SOPInstanceUID, - frameOfReferenceUID: FrameOfReferenceUID, - SeriesInstanceUID: referenceSeriesUID, - unit, - text: label, - description, - handles: _getHandlesFromPoints(points), - _measurementServiceId: id, - }, - }; - }; - - /** - * Maps cornerstone annotation event data to measurement service format. - * - * @param {Object} cornerstone Cornerstone event data - * @return {Measurement} Measurement instance - */ - const toMeasurement = csToolsAnnotation => { - const { element, measurementData } = csToolsAnnotation; - const tool = - csToolsAnnotation.toolType || - csToolsAnnotation.toolName || - measurementData.toolType; - - const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); - - if (!validToolType(tool)) { - throw new Error('Tool not supported'); - } - - const { - SOPInstanceUID, - FrameOfReferenceUID, - SeriesInstanceUID, - StudyInstanceUID, - } = _getAttributes(element); - - const points = []; - points.push(measurementData.handles); - - return { - id: measurementData._measurementServiceId, - 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: _getPointsFromHandles(measurementData.handles), - }; - }; - - const _getAttributes = element => { - const enabledElement = cornerstone.getEnabledElement(element); - const imageId = enabledElement.image.imageId; - const instance = cornerstone.metaData.get('instance', imageId); - - return { - SOPInstanceUID: instance.SOPInstanceUID, - FrameOfReferenceUID: instance.FrameOfReferenceUID, - SeriesInstanceUID: instance.SeriesInstanceUID, - StudyInstanceUID: instance.StudyInstanceUID, - }; - }; const _getValueTypeFromToolType = toolType => { - const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES; + const { + POLYLINE, + ELLIPSE, + POINT, + BIDIRECTIONAL, + } = measurementService.VALUE_TYPES; - /* TODO: Relocate static 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.. const TOOL_TYPE_TO_VALUE_TYPE = { Length: POLYLINE, EllipticalRoi: ELLIPSE, - RectangleRoi: POLYLINE, + Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, }; return TOOL_TYPE_TO_VALUE_TYPE[toolType]; }; - const _getPointsFromHandles = handles => { - let 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 points; - }; - - const _getHandlesFromPoints = points => { - return points - .map((p, i) => (i % 10 === 0 ? { start: p } : { end: p })) - .reduce((obj, item) => Object.assign(obj, { ...item }), {}); - }; - return { - toAnnotation, - toMeasurement, + Length: { + toAnnotation: Length.toAnnotation, + toMeasurement: csToolsAnnotation => + Length.toMeasurement(csToolsAnnotation, _getValueTypeFromToolType), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + Bidirectional: { + toAnnotation: Bidirectional.toAnnotation, + toMeasurement: csToolsAnnotation => + Bidirectional.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + // TODO -> We should eventually do something like shortAxis + longAxis, + // But its still a little unclear how these automatic interpretations will work. + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + ArrowAnnotate: { + toAnnotation: ArrowAnnotate.toAnnotation, + toMeasurement: csToolsAnnotation => + ArrowAnnotate.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], + }, + EllipticalRoi: { + toAnnotation: EllipticalRoi.toAnnotation, + toMeasurement: csToolsAnnotation => + EllipticalRoi.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.ELLIPSE, + }, + ], + }, }; }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js new file mode 100644 index 000000000..1440405bc --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js @@ -0,0 +1,5 @@ +export default function getHandlesFromPoints(points) { + return points + .map((p, i) => (i % 10 === 0 ? { start: p } : { end: p })) + .reduce((obj, item) => Object.assign(obj, { ...item }), {}); +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js new file mode 100644 index 000000000..dfd4d5092 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js @@ -0,0 +1,12 @@ +export default function getPointsFromHandles(handles) { + let 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 points; +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js new file mode 100644 index 000000000..01ca8ffd9 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js @@ -0,0 +1,14 @@ +import cornerstone from 'cornerstone-core'; + +export default function getSOPInstanceAttributes(element) { + const enabledElement = cornerstone.getEnabledElement(element); + const imageId = enabledElement.image.imageId; + const instance = cornerstone.metaData.get('instance', imageId); + + return { + SOPInstanceUID: instance.SOPInstanceUID, + FrameOfReferenceUID: instance.FrameOfReferenceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + }; +} diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index ece666e80..edfd44aa1 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -42,7 +42,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { trackedSeries.includes(m.referenceSeriesUID) ); const mappedMeasurements = filteredMeasurements.map((m, index) => - _mapMeasurementToDisplay(m, index) + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) ); setDisplayMeasurements(mappedMeasurements); // eslint-ignore-next-line @@ -130,7 +130,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { PanelMeasurementTableTracking.propTypes = {}; // TODO: This could be a MeasurementService mapper -function _mapMeasurementToDisplay(measurement, index) { +function _mapMeasurementToDisplay(measurement, index, types) { const { id, label, @@ -153,12 +153,14 @@ function _mapMeasurementToDisplay(measurement, index) { return { id: index + 1, label: '(empty)', // 'Label short description', - displayText: _getDisplayText( - measurement.points, - PixelSpacing, - SeriesNumber, - InstanceNumber - ), + displayText: + _getDisplayText( + measurement, + PixelSpacing, + SeriesNumber, + InstanceNumber, + types + ) || [], // TODO: handle one layer down isActive: false, // activeMeasurementItem === i + 1, }; @@ -169,7 +171,13 @@ function _mapMeasurementToDisplay(measurement, index) { * @param {*} points * @param {*} pixelSpacing */ -function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { +function _getDisplayText( + measurement, + pixelSpacing, + seriesNumber, + 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)', @@ -177,6 +185,8 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { // Bidirectional? // Freehand? + const { type, points } = measurement; + const hasPixelSpacing = pixelSpacing !== undefined && Array.isArray(pixelSpacing) && @@ -186,13 +196,37 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { : [1, 1]; const unit = hasPixelSpacing ? 'mm' : 'px'; - const { x: x1, y: y1 } = points[0]; - const { x: x2, y: y2 } = points[1]; - const dx = (x2 - x1) * colPixelSpacing; - const dy = (y2 - y1) * rowPixelSpacing; - const length = _round(Math.sqrt(dx * dx + dy * dy), 1); + switch (type) { + case types.POLYLINE: + const { length } = measurement; - return `${length} ${unit} (S:${seriesNumber}, I:${instanceNumber})`; + 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) { diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index 4688a9849..ba9ddcf61 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -40,6 +40,10 @@ const MEASUREMENT_SCHEMA_KEYS = [ 'type', 'unit', 'area', // TODO: Add concept names instead (descriptor) + 'length', + 'shortestDiameter', + 'longestDiameter', + 'text', // NOTE: There is nothing like this in SR. 'points', 'source', ]; @@ -53,6 +57,7 @@ const EVENTS = { const VALUE_TYPES = { POLYLINE: 'value_type::polyline', POINT: 'value_type::point', + BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here. ELLIPSE: 'value_type::ellipse', MULTIPOINT: 'value_type::multipoint', CIRCLE: 'value_type::circle', diff --git a/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx b/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx index 95ba02982..9898c504d 100644 --- a/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx +++ b/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx @@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
{!!data.length && - data.map((measurementItem) => { + data.map(measurementItem => { const { id, label, displayText, isActive } = measurementItem; return (
{ {label} - - {displayText} - + {displayText.map(line => ( + + {line} + + ))} { right: 4, transform: isActive ? '' : 'translateX(100%)', }} - onClick={(e) => { + onClick={e => { // stopPropagation needed to avoid disable the current active item e.stopPropagation(); onEdit(id); @@ -108,7 +110,7 @@ MeasurementTable.propTypes = { PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), label: PropTypes.string, - displayText: PropTypes.string, + displayText: PropTypes.arrayOf(PropTypes.string), isActive: PropTypes.bool, }) ), From c01466762dc5373e57489515557fbe9c0028f3e8 Mon Sep 17 00:00:00 2001 From: James Petts Date: Thu, 25 Jun 2020 17:13:31 +0100 Subject: [PATCH 04/21] WIP Measurements service. (#1808) --- extensions/cornerstone/src/init.js | 47 +++-- .../ArrowAnnotate.js | 49 +++++ .../Bidirectional.js | 50 +++++ .../EllipticalRoi.js | 74 +++++++ .../measurementServiceMappings/Length.js | 79 ++++++++ .../constants/supportedTools.js | 1 + .../measurementServiceMappingsFactory.js | 190 +++++++----------- .../utils/getHandlesFromPoints.js | 5 + .../utils/getPointsFromHandles.js | 12 ++ .../utils/getSOPInstanceAttributes.js | 14 ++ .../PanelMeasurementTableTracking/index.js | 64 ++++-- .../MeasurementService/MeasurementService.js | 5 + .../MeasurementTable/MeasurementTable.jsx | 14 +- 13 files changed, 453 insertions(+), 151 deletions(-) create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/Length.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 48809b2b4..5f80a9d6f 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -90,6 +90,7 @@ export default function init({ servicesManager, configuration }) { /* Add extension tools configuration here. */ const internalToolsConfig = { + /* TODO ArrowAnnotate input ArrowAnnotate: { configuration: { getTextCallback: (callback, eventDetails) => @@ -98,6 +99,7 @@ export default function init({ servicesManager, configuration }) { callInputDialog(data, eventDetails, callback), }, }, + */ }; /* Abstract tools configuration using extension configuration. */ @@ -166,27 +168,48 @@ export default function init({ servicesManager, configuration }) { const _initMeasurementService = measurementService => { /* Initialization */ - const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory( - measurementService - ); + const { + Length, + Bidirectional, + EllipticalRoi, + ArrowAnnotate, + } = measurementServiceMappingsFactory(measurementService); const csToolsVer4MeasurementSource = measurementService.createSource( 'CornerstoneTools', '4' ); - /* Matching Criterias */ - const matchingCriteria = { - valueType: measurementService.VALUE_TYPES.POLYLINE, - points: 2, - }; - /* Mappings */ measurementService.addMapping( csToolsVer4MeasurementSource, 'Length', - matchingCriteria, - toAnnotation, - toMeasurement + Length.matchingCriteria, + Length.toAnnotation, + Length.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'Bidirectional', + Bidirectional.matchingCriteria, + Bidirectional.toAnnotation, + Bidirectional.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'EllipticalRoi', + EllipticalRoi.matchingCriteria, + EllipticalRoi.toAnnotation, + EllipticalRoi.toMeasurement + ); + + measurementService.addMapping( + csToolsVer4MeasurementSource, + 'ArrowAnnotate', + ArrowAnnotate.matchingCriteria, + ArrowAnnotate.toAnnotation, + ArrowAnnotate.toMeasurement ); return csToolsVer4MeasurementSource; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js new file mode 100644 index 000000000..b1d9f2f44 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -0,0 +1,49 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; +import getPointsFromHandles from './utils/getPointsFromHandles'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const ArrowAnnotate = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const points = []; + points.push(measurementData.handles); + + return { + id: measurementData._measurementServiceId, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + text: measurementData.text, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; + }, +}; + +export default ArrowAnnotate; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js new file mode 100644 index 000000000..5b6f3e893 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js @@ -0,0 +1,50 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const Bidirectional = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const { handles } = measurementData; + + const longAxis = [handles.start, handles.end]; + const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; + + return { + id: measurementData._measurementServiceId, + 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 }, + }; + }, +}; + +export default Bidirectional; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js new file mode 100644 index 000000000..dbd4d2403 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js @@ -0,0 +1,74 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const EllipticalRoi = { + toAnnotation: (measurement, definition) => { + // TODO -> Implement when this is needed. + }, + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + 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._measurementServiceId, + 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, + }; + }, +}; + +export default EllipticalRoi; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js new file mode 100644 index 000000000..5f9eda649 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -0,0 +1,79 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getHandlesFromPoints from './utils/getHandlesFromPoints'; +import getPointsFromHandles from './utils/getPointsFromHandles'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const Length = { + toAnnotation: (measurement, definition) => { + const { + id, + label, + description, + points, + unit, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID, + } = measurement; + + return { + toolName: definition, + measurementData: { + sopInstanceUid: SOPInstanceUID, + frameOfReferenceUID: FrameOfReferenceUID, + SeriesInstanceUID: referenceSeriesUID, + unit, + text: label, + description, + handles: getHandlesFromPoints(points), + _measurementServiceId: id, + }, + }; + }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => { + const { element, measurementData } = csToolsAnnotation; + const tool = + csToolsAnnotation.toolType || + csToolsAnnotation.toolName || + measurementData.toolType; + + const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType(tool)) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const points = []; + points.push(measurementData.handles); + + return { + id: measurementData._measurementServiceId, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; + }, +}; + +export default Length; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js new file mode 100644 index 000000000..a1ca5e83e --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -0,0 +1 @@ +export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate']; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index f279eaec0..6e325fb19 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -1,11 +1,7 @@ -import cornerstone from 'cornerstone-core'; - -const SUPPORTED_TOOLS = [ - 'Length', - 'EllipticalRoi', - 'RectangleRoi', - 'ArrowAnnotate', -]; +import Length from './Length'; +import Bidirectional from './Bidirectional'; +import ArrowAnnotate from './ArrowAnnotate'; +import EllipticalRoi from './EllipticalRoi'; const measurementServiceMappingsFactory = measurementService => { /** @@ -15,129 +11,87 @@ const measurementServiceMappingsFactory = measurementService => { * @param {string} definition The source definition * @return {Object} Cornerstone annotation data */ - const toAnnotation = (measurement, definition) => { - const { - id, - label, - description, - points, - unit, - SOPInstanceUID, - FrameOfReferenceUID, - referenceSeriesUID, - } = measurement; - - return { - toolName: definition, - measurementData: { - sopInstanceUid: SOPInstanceUID, - frameOfReferenceUID: FrameOfReferenceUID, - SeriesInstanceUID: referenceSeriesUID, - unit, - text: label, - description, - handles: _getHandlesFromPoints(points), - _measurementServiceId: id, - }, - }; - }; - - /** - * Maps cornerstone annotation event data to measurement service format. - * - * @param {Object} cornerstone Cornerstone event data - * @return {Measurement} Measurement instance - */ - const toMeasurement = csToolsAnnotation => { - const { element, measurementData } = csToolsAnnotation; - const tool = - csToolsAnnotation.toolType || - csToolsAnnotation.toolName || - measurementData.toolType; - - const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName); - - if (!validToolType(tool)) { - throw new Error('Tool not supported'); - } - - const { - SOPInstanceUID, - FrameOfReferenceUID, - SeriesInstanceUID, - StudyInstanceUID, - } = _getAttributes(element); - - const points = []; - points.push(measurementData.handles); - - return { - id: measurementData._measurementServiceId, - 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: _getPointsFromHandles(measurementData.handles), - }; - }; - - const _getAttributes = element => { - const enabledElement = cornerstone.getEnabledElement(element); - const imageId = enabledElement.image.imageId; - const instance = cornerstone.metaData.get('instance', imageId); - - return { - SOPInstanceUID: instance.SOPInstanceUID, - FrameOfReferenceUID: instance.FrameOfReferenceUID, - SeriesInstanceUID: instance.SeriesInstanceUID, - StudyInstanceUID: instance.StudyInstanceUID, - }; - }; const _getValueTypeFromToolType = toolType => { - const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES; + const { + POLYLINE, + ELLIPSE, + POINT, + BIDIRECTIONAL, + } = measurementService.VALUE_TYPES; - /* TODO: Relocate static 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.. const TOOL_TYPE_TO_VALUE_TYPE = { Length: POLYLINE, EllipticalRoi: ELLIPSE, - RectangleRoi: POLYLINE, + Bidirectional: BIDIRECTIONAL, ArrowAnnotate: POINT, }; return TOOL_TYPE_TO_VALUE_TYPE[toolType]; }; - const _getPointsFromHandles = handles => { - let 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 points; - }; - - const _getHandlesFromPoints = points => { - return points - .map((p, i) => (i % 10 === 0 ? { start: p } : { end: p })) - .reduce((obj, item) => Object.assign(obj, { ...item }), {}); - }; - return { - toAnnotation, - toMeasurement, + Length: { + toAnnotation: Length.toAnnotation, + toMeasurement: csToolsAnnotation => + Length.toMeasurement(csToolsAnnotation, _getValueTypeFromToolType), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + Bidirectional: { + toAnnotation: Bidirectional.toAnnotation, + toMeasurement: csToolsAnnotation => + Bidirectional.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + // TODO -> We should eventually do something like shortAxis + longAxis, + // But its still a little unclear how these automatic interpretations will work. + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + ArrowAnnotate: { + toAnnotation: ArrowAnnotate.toAnnotation, + toMeasurement: csToolsAnnotation => + ArrowAnnotate.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], + }, + EllipticalRoi: { + toAnnotation: EllipticalRoi.toAnnotation, + toMeasurement: csToolsAnnotation => + EllipticalRoi.toMeasurement( + csToolsAnnotation, + _getValueTypeFromToolType + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.ELLIPSE, + }, + ], + }, }; }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js new file mode 100644 index 000000000..1440405bc --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js @@ -0,0 +1,5 @@ +export default function getHandlesFromPoints(points) { + return points + .map((p, i) => (i % 10 === 0 ? { start: p } : { end: p })) + .reduce((obj, item) => Object.assign(obj, { ...item }), {}); +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js new file mode 100644 index 000000000..dfd4d5092 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getPointsFromHandles.js @@ -0,0 +1,12 @@ +export default function getPointsFromHandles(handles) { + let 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 points; +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js new file mode 100644 index 000000000..01ca8ffd9 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js @@ -0,0 +1,14 @@ +import cornerstone from 'cornerstone-core'; + +export default function getSOPInstanceAttributes(element) { + const enabledElement = cornerstone.getEnabledElement(element); + const imageId = enabledElement.image.imageId; + const instance = cornerstone.metaData.get('instance', imageId); + + return { + SOPInstanceUID: instance.SOPInstanceUID, + FrameOfReferenceUID: instance.FrameOfReferenceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + }; +} diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index ece666e80..edfd44aa1 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -42,7 +42,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { trackedSeries.includes(m.referenceSeriesUID) ); const mappedMeasurements = filteredMeasurements.map((m, index) => - _mapMeasurementToDisplay(m, index) + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) ); setDisplayMeasurements(mappedMeasurements); // eslint-ignore-next-line @@ -130,7 +130,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { PanelMeasurementTableTracking.propTypes = {}; // TODO: This could be a MeasurementService mapper -function _mapMeasurementToDisplay(measurement, index) { +function _mapMeasurementToDisplay(measurement, index, types) { const { id, label, @@ -153,12 +153,14 @@ function _mapMeasurementToDisplay(measurement, index) { return { id: index + 1, label: '(empty)', // 'Label short description', - displayText: _getDisplayText( - measurement.points, - PixelSpacing, - SeriesNumber, - InstanceNumber - ), + displayText: + _getDisplayText( + measurement, + PixelSpacing, + SeriesNumber, + InstanceNumber, + types + ) || [], // TODO: handle one layer down isActive: false, // activeMeasurementItem === i + 1, }; @@ -169,7 +171,13 @@ function _mapMeasurementToDisplay(measurement, index) { * @param {*} points * @param {*} pixelSpacing */ -function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { +function _getDisplayText( + measurement, + pixelSpacing, + seriesNumber, + 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)', @@ -177,6 +185,8 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { // Bidirectional? // Freehand? + const { type, points } = measurement; + const hasPixelSpacing = pixelSpacing !== undefined && Array.isArray(pixelSpacing) && @@ -186,13 +196,37 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) { : [1, 1]; const unit = hasPixelSpacing ? 'mm' : 'px'; - const { x: x1, y: y1 } = points[0]; - const { x: x2, y: y2 } = points[1]; - const dx = (x2 - x1) * colPixelSpacing; - const dy = (y2 - y1) * rowPixelSpacing; - const length = _round(Math.sqrt(dx * dx + dy * dy), 1); + switch (type) { + case types.POLYLINE: + const { length } = measurement; - return `${length} ${unit} (S:${seriesNumber}, I:${instanceNumber})`; + 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) { diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index 4688a9849..ba9ddcf61 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -40,6 +40,10 @@ const MEASUREMENT_SCHEMA_KEYS = [ 'type', 'unit', 'area', // TODO: Add concept names instead (descriptor) + 'length', + 'shortestDiameter', + 'longestDiameter', + 'text', // NOTE: There is nothing like this in SR. 'points', 'source', ]; @@ -53,6 +57,7 @@ const EVENTS = { const VALUE_TYPES = { POLYLINE: 'value_type::polyline', POINT: 'value_type::point', + BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here. ELLIPSE: 'value_type::ellipse', MULTIPOINT: 'value_type::multipoint', CIRCLE: 'value_type::circle', diff --git a/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx b/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx index 95ba02982..9898c504d 100644 --- a/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx +++ b/platform/ui/src/components/MeasurementTable/MeasurementTable.jsx @@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
{!!data.length && - data.map((measurementItem) => { + data.map(measurementItem => { const { id, label, displayText, isActive } = measurementItem; return (
{ {label} - - {displayText} - + {displayText.map(line => ( + + {line} + + ))} { right: 4, transform: isActive ? '' : 'translateX(100%)', }} - onClick={(e) => { + onClick={e => { // stopPropagation needed to avoid disable the current active item e.stopPropagation(); onEdit(id); @@ -108,7 +110,7 @@ MeasurementTable.propTypes = { PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), label: PropTypes.string, - displayText: PropTypes.string, + displayText: PropTypes.arrayOf(PropTypes.string), isActive: PropTypes.bool, }) ), From 58f10e9b097d25ea4ae8062f9cbdfbcae631630f Mon Sep 17 00:00:00 2001 From: dannyrb Date: Thu, 25 Jun 2020 12:18:23 -0400 Subject: [PATCH 05/21] fix: add missing `supportsFuzzyMatching` and `defaultDataSourceName` keys to appConfig --- platform/viewer/public/config/netlify.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js index 39b804d2a..6861a50ed 100644 --- a/platform/viewer/public/config/netlify.js +++ b/platform/viewer/public/config/netlify.js @@ -19,7 +19,9 @@ window.config = { imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, + supportsFuzzyMatching: true, }, }, ], + defaultDataSourceName: 'dicomweb', }; From 74c14622715acb36ec95141250742afc3511c0b1 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Thu, 25 Jun 2020 17:21:03 +0100 Subject: [PATCH 06/21] Working local download, STOW to PACS an view in SR Viewport. --- extensions/cornerstone/package.json | 2 +- .../ArrowAnnotate.js | 1 - extensions/default/package.json | 2 +- .../default/src/DicomWebDataSource/index.js | 33 ++++- .../src/Panels/WrappedPanelStudyBrowser.jsx | 2 +- extensions/dicom-html/package.json | 2 +- extensions/dicom-rt/package.json | 2 +- extensions/dicom-segmentation/package.json | 2 +- extensions/dicom-sr/package.json | 2 +- extensions/measurement-tracking/package.json | 5 +- .../ActionButtons.jsx | 16 ++- .../PanelMeasurementTableTracking/index.js | 43 ++++++- extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- platform/core/src/DICOMSR/dataExchange.js | 116 +++++++++++++++++- platform/core/src/DICOMSR/index.js | 7 +- .../core/src/DataSources/IWebApiDataSource.js | 12 +- platform/viewer/package.json | 2 +- yarn.lock | 8 +- 19 files changed, 232 insertions(+), 29 deletions(-) diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 4dd657d40..b09d30d55 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -34,7 +34,7 @@ "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js index b1d9f2f44..7d2c1c3b2 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -1,5 +1,4 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; -import getHandlesFromPoints from './utils/getHandlesFromPoints'; import getPointsFromHandles from './utils/getPointsFromHandles'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; diff --git a/extensions/default/package.json b/extensions/default/package.json index e9633cdc1..09342a611 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -32,7 +32,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "webpack": "^4.0.0", - "dcmjs": "^0.12.4" + "dcmjs": "0.14.0" }, "dependencies": { "@babel/runtime": "7.7.6" diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index c832ee4a9..33bce395d 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -12,9 +12,14 @@ import getImageId from './utils/getImageId'; import * as dcmjs from 'dcmjs'; import { retrieveStudyMetadata } from './retrieveStudyMetadata.js'; -const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; +const { DicomMetaDictionary, DicomDict } = dcmjs.data; + +const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const { urlUtil } = utils; +const VERSION_NAME = 'OHIF-0.1'; +const SR_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; + /** * * @param {string} name - Data source name @@ -128,6 +133,32 @@ function createDicomWebApi(dicomWebConfig) { }, }, }, + store: { + dicom: async dataset => { + const meta = { + FileMetaInformationVersion: + dataset._meta.FileMetaInformationVersion.Value, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: SR_TRANSFER_SYNTAX_UID, + ImplementationClassUID: DicomMetaDictionary.uid(), + ImplementationVersionName: VERSION_NAME, + }; + + const denaturalized = denaturalizeDataset(meta); + const dicomDict = new DicomDict(denaturalized); + + dicomDict.dict = denaturalizeDataset(dataset); + + const part10Buffer = dicomDict.write(); + + const options = { + datasets: [part10Buffer], + }; + + await wadoDicomWebClient.storeInstances(options); + }, + }, retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => { if (!StudyInstanceUID) { throw new Error( diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx index c3bb27d01..e79deea68 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.jsx @@ -20,7 +20,7 @@ function WrappedPanelStudyBrowser({ }) { // TODO: This should be made available a different way; route should have // already determined our datasource - const dataSource = extensionManager.getDataSources('dicomweb')[0]; + const dataSource = extensionManager.getDataSources()[0]; const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind( null, dataSource diff --git a/extensions/dicom-html/package.json b/extensions/dicom-html/package.json index d76e6fa11..eca2b16a3 100644 --- a/extensions/dicom-html/package.json +++ b/extensions/dicom-html/package.json @@ -28,7 +28,7 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.11.0", "react-dom": "^16.11.0" diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index fb67f9acf..55ce43430 100644 --- a/extensions/dicom-rt/package.json +++ b/extensions/dicom-rt/package.json @@ -31,7 +31,7 @@ "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index ede02d636..e9214776f 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -31,7 +31,7 @@ "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.2", + "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index 6b14984c9..c8d6e6839 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -34,7 +34,7 @@ "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", - "dcmjs": "^0.12.3", + "dcmjs": "0.14.0", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 7d08ecdbd..86bad62c0 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -28,11 +28,12 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", - "dcmjs": "^0.12.4", + "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "webpack": "^4.0.0" + "webpack": "^4.0.0", + "cornerstone-tools": "4.15.1" }, "dependencies": { "@babel/runtime": "7.7.6", diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx index 0e3e8a3b6..6d239cf81 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx @@ -2,10 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui'; -function ActionButtons() { +function ActionButtons({ onExportClick, onCreateReportClick }) { return ( - alert('Export')}> + @@ -34,4 +34,14 @@ function ActionButtons() { ); } +ActionButtons.propTypes = { + onExportClick: PropTypes.func, + onCreateReportClick: PropTypes.func, +}; + +ActionButtons.defaultProps = { + onExportClick: () => alert('Export'), + onCreateReportClick: () => alert('Create Report'), +}; + export default ActionButtons; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index edfd44aa1..a37240549 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -1,10 +1,13 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { StudySummary, MeasurementTable } from '@ohif/ui'; -import { DicomMetadataStore } from '@ohif/core'; +import { DicomMetadataStore, DICOMSR } from '@ohif/core'; import { useDebounce } from '@hooks'; import ActionButtons from './ActionButtons'; import { useTrackedMeasurements } from '../../getContextModule'; +import cornerstoneTools from 'cornerstone-tools'; +import cornerstone from 'cornerstone-core'; +import dcmjs from 'dcmjs'; const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { key: undefined, // @@ -13,7 +16,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST', }; -function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { +function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { const [measurementChangeTimestamp, setMeasurementsUpdated] = useState( Date.now().toString() ); @@ -102,6 +105,37 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { const activeMeasurementItem = 0; + const onExportClick = () => { + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); + + // TODO -> local download. + DICOMSR.downloadReport(trackedMeasurements, dataSource); + }; + + const onCreateReportClick = () => { + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); + + console.log(servicesManager); + console.log(extensionManager); + + 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); + }; + return ( <>
@@ -121,7 +155,10 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) { />
- +
); diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 422d61c6f..bbe03c172 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^2.0.0", "cornerstone-core": "^2.3.0", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.12.2", + "dcmjs": "0.14.0", "dicom-parser": "^1.8.3", "i18next": "^17.0.3", "i18next-browser-languagedetector": "^3.0.1", diff --git a/platform/core/package.json b/platform/core/package.json index 83320ad2f..608077d64 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -38,7 +38,7 @@ "dependencies": { "@babel/runtime": "7.7.6", "ajv": "^6.10.0", - "dcmjs": "^0.12.4", + "dcmjs": "0.14.0", "dicomweb-client": "^0.6.0", "immer": "6.0.2", "isomorphic-base64": "^1.0.2", diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index aab594da3..dac463a12 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -6,6 +6,10 @@ import { stowSRFromMeasurements, } from './handleStructuredReport'; import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport'; +import cornerstoneTools from 'cornerstone-tools'; +import dcmjs from 'dcmjs'; + +const { MeasurementReport } = dcmjs.adapters.Cornerstone; /** * @@ -47,7 +51,7 @@ const retrieveMeasurements = server => { * @param {serverType} server * @returns {Object} With message to be displayed on success */ -const storeMeasurements = async (measurementData, filter, server) => { +const storeMeasurementsOld = async (measurementData, filter, server) => { log.info('[DICOMSR] storeMeasurements'); if (!server || server.type !== 'dicomWeb') { @@ -78,4 +82,112 @@ const storeMeasurements = async (measurementData, filter, server) => { } }; -export { retrieveMeasurements, storeMeasurements }; +const downloadReport = measurementData => { + const srDataset = generateReport(measurementData); + const reportBlob = dcmjs.data.datasetToBlob(srDataset); + + //Create a URL for the binary. + var objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); +}; + +const generateReport = measurementData => { + const ids = measurementData.map(md => md.id); + const filteredToolState = _getFilteredCornerstoneToolState(ids); + + const report = MeasurementReport.generateReport( + filteredToolState, + cornerstone.metaData + ); + + return report.dataset; +}; + +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'); + + if (!dataSource || !dataSource.store || !dataSource.store.dicom) { + log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!'); + return Promise.reject({}); + } + + const naturalizedReport = generateReport(measurementData); + const { StudyInstanceUID } = naturalizedReport; + + try { + await dataSource.store.dicom(naturalizedReport); + + if (StudyInstanceUID) { + studies.deleteStudyMetadataPromise(StudyInstanceUID); + } + + return { + message: 'Measurements saved successfully', + }; + } catch (error) { + log.error( + `[DICOMSR] Error while saving the measurements: ${error.message}` + ); + throw new Error('Error while saving the measurements.'); + } +}; + +function _getFilteredCornerstoneToolState(uidFilter) { + const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState(); + const filteredToolState = {}; + + function addToFilteredToolState(imageId, toolType, toolDataI) { + if (!filteredToolState[imageId]) { + filteredToolState[imageId] = {}; + } + + const imageIdSpecificToolState = filteredToolState[imageId]; + + if (!imageIdSpecificToolState[toolType]) { + imageIdSpecificToolState[toolType] = { + data: [], + }; + } + + const toolData = imageIdSpecificToolState[toolType].data; + + toolData.push(toolDataI); + } + + const uids = uidFilter.slice(); + const imageIds = Object.keys(globalToolState); + + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[i]; + const imageIdSpecificToolState = globalToolState[imageId]; + + const toolTypes = Object.keys(imageIdSpecificToolState); + + for (let j = 0; j < toolTypes.length; j++) { + const toolType = toolTypes[j]; + const toolData = imageIdSpecificToolState[toolType].data; + + if (toolData) { + for (let k = 0; k < toolData.length; k++) { + const toolDataK = toolData[k]; + const uidIndex = uids.findIndex(uid => uid === toolDataK.id); + + if (uidIndex !== -1) { + addToFilteredToolState(imageId, toolType, toolDataK); + uids.splice(uidIndex, 1); + + if (!uids.length) { + return filteredToolState; + } + } + } + } + } + } + + return filteredToolState; +} + +export { retrieveMeasurements, storeMeasurements, downloadReport }; diff --git a/platform/core/src/DICOMSR/index.js b/platform/core/src/DICOMSR/index.js index 786202642..759f26cb1 100644 --- a/platform/core/src/DICOMSR/index.js +++ b/platform/core/src/DICOMSR/index.js @@ -1,9 +1,14 @@ -import { retrieveMeasurements, storeMeasurements } from './dataExchange'; +import { + retrieveMeasurements, + storeMeasurements, + downloadReport, +} from './dataExchange'; import isToolSupported from './utils/isToolSupported'; const DICOMSR = { retrieveMeasurements, storeMeasurements, + downloadReport, isToolSupported, }; diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index e10914e9e..10285d364 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -15,6 +15,7 @@ import { DicomMetadataStore } from '@ohif/core'; function create({ query, retrieve, + store, retrieveSeriesMetadata, getImageIdsForDisplaySet, }) { @@ -44,13 +45,20 @@ function create({ series: {}, }; + const defaultStore = { + dicom: async naturalizedDataset => { + throw new Error( + 'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.' + ); + }, + }; + return { query: query || defaultQuery, retrieve: retrieve || defaultRetrieve, + store: store || defaultStore, getImageIdsForDisplaySet, retrieveSeriesMetadata, - // then go get all series level metadata. - // Store this in the DICOM MetadataStore. }; } diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 52c3ff3b7..de94e0357 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -69,7 +69,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.15.1", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "^0.12.2", + "dcmjs": "0.14.0", "dicom-parser": "^1.8.3", "dicomweb-client": "^0.4.4", "dotenv-webpack": "^1.7.0", diff --git a/yarn.lock b/yarn.lock index eb88b95ac..71ee64509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7353,10 +7353,10 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@^0.12.2, dcmjs@^0.12.4: - version "0.12.4" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.4.tgz#82c24abdc357ea5281b78eb2cae8b781f7392aa3" - integrity sha512-N1ZsXqZIysirqdytb7h572TyIjmxpvCjrzdjtQsuPN8gC2EpxsUHQ598CPzaJBpBy9i1kfKuq4h2Jwt99cr/QQ== +dcmjs@0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c" + integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w== dependencies: "@babel/polyfill" "^7.8.3" "@babel/runtime" "^7.8.4" From 6aac69e1ba6323074c7bf118c1670bfe539f49f0 Mon Sep 17 00:00:00 2001 From: James Petts Date: Thu, 25 Jun 2020 17:46:17 +0100 Subject: [PATCH 07/21] OHIF-176: v2 - Render non-tracked measurements as a dashed lines. (#1805) * Render non-tracked measurements as a dashed line. * bump CST version. * Update extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js remove tracked series console log * Remove empty console log * Remove debugger * Remove debugger Co-authored-by: Danny Brown --- extensions/cornerstone/package.json | 2 +- extensions/dicom-rt/package.json | 2 +- extensions/dicom-sr/package.json | 2 +- .../viewports/TrackedCornerstoneViewport.js | 92 +++++++++++++++++++ extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- platform/viewer/package.json | 2 +- yarn.lock | 8 +- 8 files changed, 102 insertions(+), 10 deletions(-) diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 4dd657d40..d1b8588ad 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.15.1", + "cornerstone-tools": "4.16.0", "dcmjs": "^0.12.3", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index fb67f9acf..ddd9bfac0 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.15.1", + "cornerstone-tools": "4.16.0", "dcmjs": "^0.12.3", "prop-types": "^15.6.2", "react": "^16.8.6", diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index 6b14984c9..0757083bf 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.15.1", + "cornerstone-tools": "4.16.0", "dcmjs": "^0.12.3", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 327c49a77..c582bd929 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import cornerstone from 'cornerstone-core'; +import cornerstoneTools from 'cornerstone-tools'; import CornerstoneViewport from 'react-cornerstone-viewport'; import OHIF, { DicomMetadataStore } from '@ohif/core'; import { @@ -13,6 +14,18 @@ import debounce from 'lodash.debounce'; import throttle from 'lodash.throttle'; import { useTrackedMeasurements } from './../getContextModule'; +// TODO -> Get this list from the list of tracked measurements. +const { + ArrowAnnotateTool, + BidirectionalTool, + EllipticalRoiTool, + LengthTool, +} = cornerstoneTools; + +const BaseAnnotationTool = cornerstoneTools.importInternal( + 'base/BaseAnnotationTool' +); + // const cine = viewportSpecificData.cine; // isPlaying = cine.isPlaying === true; @@ -27,6 +40,7 @@ function TrackedCornerstoneViewport({ viewportIndex, }) { const [trackedMeasurements] = useTrackedMeasurements(); + const [ { activeViewportIndex, viewports }, dispatchViewportGrid, @@ -34,6 +48,8 @@ function TrackedCornerstoneViewport({ // viewportIndex, onSubmit const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [viewportData, setViewportData] = useState(null); + const [element, setElement] = useState(null); + const [isTracked, setIsTracked] = useState(false); // TODO: Still needed? Better way than import `OHIF` and destructure? // Why is this managed by `core`? useEffect(() => { @@ -42,6 +58,75 @@ function TrackedCornerstoneViewport({ }; }, []); + useEffect(() => { + if (!element) { + return; + } + const allTools = cornerstoneTools.store.state.tools; + const toolsForElement = allTools.filter(tool => tool.element === element); + + toolsForElement.forEach(tool => { + if ( + tool instanceof ArrowAnnotateTool || + tool instanceof BidirectionalTool || + tool instanceof EllipticalRoiTool || + tool instanceof LengthTool + ) { + const configuration = tool.configuration; + + configuration.renderDashed = !isTracked; + + tool.configuration = configuration; + } + }); + + const enabledElement = cornerstone.getEnabledElement(element); + + if (enabledElement.image) { + cornerstone.updateImage(element); + } + }, [isTracked]); + + const onElementEnabled = evt => { + const eventData = evt.detail; + const targetElement = eventData.element; + + const allTools = cornerstoneTools.store.state.tools; + + const toolsForElement = allTools.filter( + tool => tool.element === targetElement + ); + + toolsForElement.forEach(tool => { + if ( + tool instanceof ArrowAnnotateTool || + tool instanceof BidirectionalTool || + tool instanceof EllipticalRoiTool || + tool instanceof LengthTool + ) { + const configuration = tool.configuration; + + configuration.renderDashed = !isTracked; + + tool.configuration = configuration; + } else if (tool instanceof BaseAnnotationTool) { + const configuration = tool.configuration; + + configuration.renderDashed = true; + + tool.configuration = configuration; + } + }); + + const enabledElement = cornerstone.getEnabledElement(targetElement); + + if (enabledElement.image) { + cornerstone.updateImage(targetElement); + } + + setElement(targetElement); + }; + useEffect(() => { const { StudyInstanceUID, @@ -116,6 +201,7 @@ function TrackedCornerstoneViewport({ vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID ); const { trackedSeries } = trackedMeasurements.context; + const { Modality, SeriesDate, @@ -131,6 +217,11 @@ function TrackedCornerstoneViewport({ SliceThickness, } = displaySet.images[0]; + + if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { + setIsTracked(!isTracked); + } + return ( <> Date: Thu, 25 Jun 2020 12:48:42 -0400 Subject: [PATCH 08/21] remove console log --- .../src/panels/PanelMeasurementTableTracking/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index a37240549..16e506deb 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -126,7 +126,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { ); console.log(servicesManager); - console.log(extensionManager); const dataSources = extensionManager.getDataSources(); // TODO -> Eventually deal with multiple dataSources. From 0ae95adf348a5cdcb35c61a010cace9888db3446 Mon Sep 17 00:00:00 2001 From: Danny Brown Date: Thu, 25 Jun 2020 12:49:05 -0400 Subject: [PATCH 09/21] Remove console log --- .../src/panels/PanelMeasurementTableTracking/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 16e506deb..10184832a 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -125,7 +125,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { trackedSeries.includes(m.referenceSeriesUID) ); - console.log(servicesManager); const dataSources = extensionManager.getDataSources(); // TODO -> Eventually deal with multiple dataSources. From 44297af9656edc8ebf2d18b747b8133603a7c10e Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Thu, 25 Jun 2020 15:27:42 -0300 Subject: [PATCH 10/21] Create window level dropdown menu --- extensions/default/src/ViewerLayout/index.jsx | 9 +- modes/longitudinal/src/toolbarButtons.js | 50 ++++++++++- .../services/ToolBarService/ToolBarService.js | 15 ++-- platform/ui/index.js | 2 + .../ExpandableToolbarButton.css | 16 ++++ .../ExpandableToolbarButton.jsx | 86 +++++++++++++++++++ .../ExpandableToolbarButton.mdx | 58 +++++++++++++ .../ExpandableToolbarButton/index.js | 2 + .../src/components/IconButton/IconButton.jsx | 4 +- .../ui/src/components/ListMenu/ListMenu.jsx | 65 ++++++++++++++ .../ui/src/components/ListMenu/ListMenu.mdx | 52 +++++++++++ platform/ui/src/components/ListMenu/index.js | 2 + platform/ui/src/components/Select/Select.jsx | 2 +- .../ui/src/components/Tooltip/Tooltip.jsx | 6 +- platform/ui/src/components/index.js | 4 + platform/ui/tailwind.config.js | 7 ++ 16 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx create mode 100644 platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx create mode 100644 platform/ui/src/components/ExpandableToolbarButton/index.js create mode 100644 platform/ui/src/components/ListMenu/ListMenu.jsx create mode 100644 platform/ui/src/components/ListMenu/ListMenu.mdx create mode 100644 platform/ui/src/components/ListMenu/index.js diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 730ee515b..941a5487a 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -57,13 +57,20 @@ function ViewerLayout({ const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); + const onPrimaryClickHandler = (evt, btn) => { + if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) { + const { commandName, commandOptions } = btn.props.commands[evt.value]; + commandsManager.runCommand(commandName, commandOptions); + } + }; + useEffect(() => { const { unsubscribe } = ToolBarService.subscribe( ToolBarService.EVENTS.TOOL_BAR_MODIFIED, () => { console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary'), + primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler }), secondary: ToolBarService.getButtonSection('secondary'), }; setToolbars(updatedToolbars); diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index 3f4db8160..6fc5634ea 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -1,5 +1,8 @@ // TODO: torn, can either bake this here; or have to create a whole new button type // Only ways that you can pass in a custom React component for render :l +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +import React from 'react'; +import classnames from 'classnames'; export default [ // Divider @@ -29,13 +32,58 @@ export default [ config: { groupName: 'primaryTool', }, + component: ExpandableToolbarButton, props: { isActive: true, icon: 'tool-window-level', - label: 'Levels', commandName: 'setToolActive', commandOptions: { toolName: 'Wwwc' }, + commands: { + 'windowLevelPreset1': { + commandName: 'windowLevelPreset1', + commandOptions: {}, + }, + 'windowLevelPreset2': { + commandName: 'windowLevelPreset2', + commandOptions: {}, + }, + 'windowLevelPreset3': { + commandName: 'windowLevelPreset3', + commandOptions: {}, + }, + 'windowLevelPreset4': { + commandName: 'windowLevelPreset4', + commandOptions: {}, + }, + 'windowLevelPreset5': { + commandName: 'windowLevelPreset5', + commandOptions: {}, + } + }, type: 'primary', + content: ListMenu, + contentProps: { + options: [ + { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, + { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, + { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, + { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + ], + renderer: ({ title, subtitle, isActive, index }) => ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + ) + } }, }, { diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 95cc1b431..589839448 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -62,7 +62,7 @@ export default class ToolBarService { this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); } - getButtonSection(key) { + getButtonSection(key, props) { const buttonSectionIds = this.buttonSections[key]; const buttonsInSection = []; @@ -79,7 +79,7 @@ export default class ToolBarService { btnIds.forEach(nestedBtnId => { const nestedBtn = this.buttons[nestedBtnId]; - const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key); + const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, props); nestedButtons.push(mappedNestedBtn); }); @@ -90,7 +90,7 @@ export default class ToolBarService { } else { const btnId = btnIdOrArray; const btn = this.buttons[btnId]; - const mappedBtn = this._mapButtonToDisplay(btn, key); + const mappedBtn = this._mapButtonToDisplay(btn, key, props); buttonsInSection.push(mappedBtn); } @@ -136,8 +136,8 @@ export default class ToolBarService { * @param {*} btn * @param {*} btnSection */ - _mapButtonToDisplay(btn, btnSection) { - const { id, type, component, props } = btn; + _mapButtonToDisplay(btn, btnSection, props) { + const { id, type, component } = btn; const buttonType = this._buttonTypes()[type]; if (!buttonType) { @@ -154,12 +154,15 @@ export default class ToolBarService { if (btn.props.clickHandler) { btn.clickHandler(evt, btn, btnSection); } + if (props && props.onClick) { + props.onClick(evt, btn, btnSection, props); + } }; return { id, Component: component || buttonType.defaultComponent, - componentProps: Object.assign({}, props, { onClick }), // + componentProps: Object.assign({}, btn.props, props, { onClick }), // }; } } diff --git a/platform/ui/index.js b/platform/ui/index.js index db85039d9..2025cfb3b 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -32,6 +32,8 @@ export { DateRange, Dialog, EmptyStudies, + ExpandableToolbarButton, + ListMenu, Icon, IconButton, Input, diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css new file mode 100644 index 000000000..3a5a6c8be --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.css @@ -0,0 +1,16 @@ +.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after { + content: ""; + position: absolute; + bottom: -10px; + border-width: 10px 10px 0; + border-style: solid; + border-color:#5acce6 transparent; +} + +.ExpandableToolbarButton .ExpandableToolbarButton__content { + display: none; +} + +.ExpandableToolbarButton:hover .ExpandableToolbarButton__content { + display: block; +} diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx new file mode 100644 index 000000000..b343d9ed7 --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { IconButton, Icon } from '@ohif/ui'; + +import './ExpandableToolbarButton.css'; + +const ExpandableToolbarButton = ({ + type, + id, + isActive, + onClick, + icon, + className, + content: Content, + contentProps +}) => { + const classes = { + type: { + primary: isActive + ? 'text-black' + : 'text-common-bright hover:bg-primary-dark hover:text-primary-light', + secondary: isActive + ? 'text-black' + : 'text-white hover:bg-secondary-dark focus:bg-secondary-dark', + }, + }; + + const onChildClickHandler = (...args) => { + onClick(...args); + + if (contentProps.onClick) { + contentProps.onClick(...args); + } + }; + + const onClickHandler = (...args) => { + onClick(...args); + }; + + return ( +
+ + + +
+
+ +
+
+
+ ); +}; + +const noop = () => { }; + +ExpandableToolbarButton.defaultProps = { + isActive: false, + type: 'primary', + content: null, + onClick: noop, +}; + +ExpandableToolbarButton.propTypes = { + /* Influences background/hover styling */ + type: PropTypes.oneOf(['primary', 'secondary']), + id: PropTypes.string.isRequired, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + /** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */ + content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + contentProps: PropTypes.object, +}; + +export default ExpandableToolbarButton; diff --git a/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx new file mode 100644 index 000000000..b6867f370 --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/ExpandableToolbarButton.mdx @@ -0,0 +1,58 @@ +--- +name: Expandable Toolbar Button +menu: General +route: components/expandableToolbarButton +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; + +# Toolbar Button + +Expandable Toolbar Buttons are used to populate the Toolbar. + +## Import + +```javascript +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +``` + + + {() => { + const props = { + content: ListMenu, + contentProps: { + options: [ + { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, + { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, + { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, + { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + ], + renderer: ({ title, subtitle, isActive, index }) => ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + ) + } + }; + return ( +
+ +
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/ExpandableToolbarButton/index.js b/platform/ui/src/components/ExpandableToolbarButton/index.js new file mode 100644 index 000000000..2a5e6032c --- /dev/null +++ b/platform/ui/src/components/ExpandableToolbarButton/index.js @@ -0,0 +1,2 @@ +import ExpandableToolbarButton from './ExpandableToolbarButton'; +export default ExpandableToolbarButton; diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index 475e61620..d92a6b9ca 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; const baseClasses = - 'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none'; + 'text-center items-center justify-center outline-none font-bold focus:outline-none'; const roundedClasses = { none: '', @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => {}, + onClick: () => { }, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/ListMenu/ListMenu.jsx b/platform/ui/src/components/ListMenu/ListMenu.jsx new file mode 100644 index 000000000..56a693740 --- /dev/null +++ b/platform/ui/src/components/ListMenu/ListMenu.jsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; + +const ListMenu = ({ options = [], renderer, onClick }) => { + const [selectedIndex, setSelectedIndex] = useState(null); + + const ListItem = (props) => { + const flex = 'flex flex-row justify-between items-center'; + const theme = 'bg-indigo-dark'; + const hover = 'hover:bg-primary-dark'; + const spacing = 'p-3 h-8'; + + return ( +
+ {renderer && renderer(props)} +
+ ); + }; + + return ( +
+ {options.map((option, index) => { + const onClickHandler = () => { + setSelectedIndex(index); + onClick({ ...option, index }); + }; + + return ( + + ); + })} +
+ ); +}; + +const noop = () => { }; + +ListMenu.propTypes = { + options: PropTypes.array.isRequired, + renderer: PropTypes.func.isRequired, + onClick: PropTypes.func +}; + +ListMenu.defaultProps = { + onClick: noop +}; + +export default ListMenu; diff --git a/platform/ui/src/components/ListMenu/ListMenu.mdx b/platform/ui/src/components/ListMenu/ListMenu.mdx new file mode 100644 index 000000000..195361e21 --- /dev/null +++ b/platform/ui/src/components/ListMenu/ListMenu.mdx @@ -0,0 +1,52 @@ +--- +name: List Menu +menu: General +route: components/listMenu +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { ListMenu } from '@ohif/ui'; + +# List Menu + +List Menus are used to populate expandable Toolbar. + +## Import + +```javascript +import { ListMenu } from '@ohif/ui'; +``` + + + {() => { + return ( + ( + <> +
+ + {title} + + + {subtitle} + +
+ {index + 1} + + )} + /> + ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/ListMenu/index.js b/platform/ui/src/components/ListMenu/index.js new file mode 100644 index 000000000..b9d95c885 --- /dev/null +++ b/platform/ui/src/components/ListMenu/index.js @@ -0,0 +1,2 @@ +import ListMenu from './ListMenu'; +export default ListMenu; diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx index 3a47d8a6a..399edd541 100644 --- a/platform/ui/src/components/Select/Select.jsx +++ b/platform/ui/src/components/Select/Select.jsx @@ -76,7 +76,7 @@ const Select = ({ options={options} value={selectedOptions} onChange={(selectedOptions, { action }) => { - const newSelection = selectedOptions.reduce( + const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce( (acc, curr) => acc.concat([curr.value]), [] ); diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index fabfa18f6..85119f7c1 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -24,7 +24,7 @@ const arrowPositionStyle = { }, }; -const Tooltip = ({ content, isSticky, position, tight, children }) => { +const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => { const [isActive, setIsActive] = useState(false); const handleMouseOver = () => { @@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => { } }; - const isOpen = isSticky || isActive; + const isOpen = (isSticky || isActive) && !isDisabled; return (
Date: Thu, 25 Jun 2020 16:03:06 -0300 Subject: [PATCH 11/21] Update code to avoid intercepting onclick --- extensions/default/src/ViewerLayout/index.jsx | 10 +++++----- extensions/default/src/getToolbarModule.js | 10 +++++++--- .../src/services/ToolBarService/ToolBarService.js | 15 +++++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 8a92a2d93..0e9a58904 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -58,9 +58,9 @@ function ViewerLayout({ const defaultTool = { icon: 'tool-more-menu', label: 'More' }; const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); const [activeTool, setActiveTool] = useState(defaultTool); - const onSecondaryClickHandler = () => setActiveTool(defaultTool); - const onPrimaryClickHandler = (evt, btn) => { - setActiveTool(btn.props.isActive ? btn.props : defaultTool); + + const setActiveToolHandler = (tool, isNested) => { + setActiveTool(isNested ? tool : defaultTool); }; useEffect(() => { @@ -69,8 +69,8 @@ function ViewerLayout({ () => { console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler }), - secondary: ToolBarService.getButtonSection('secondary', { onClick: onSecondaryClickHandler }), + primary: ToolBarService.getButtonSection('primary', { setActiveTool: setActiveToolHandler }), + secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }), }; setToolbars(updatedToolbars); } diff --git a/extensions/default/src/getToolbarModule.js b/extensions/default/src/getToolbarModule.js index 4cc66fda3..293e5cc43 100644 --- a/extensions/default/src/getToolbarModule.js +++ b/extensions/default/src/getToolbarModule.js @@ -9,7 +9,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { { name: 'ohif.divider', defaultComponent: ToolbarDivider, - clickHandler: () => {}, + clickHandler: () => { }, }, { name: 'ohif.action', @@ -30,7 +30,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { optionalConfig: [], requiredProps: [], optionalProps: [], - clickHandler: (evt, clickedBtn, btnSectionName) => { + clickHandler: (evt, clickedBtn, btnSectionName, metadata, viewerProps) => { const { props } = clickedBtn; const allButtons = toolbarService.getButtons(); @@ -47,6 +47,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { clickedBtn.config.groupName === btn.config.groupName ) { btn.props.isActive = false; + + if (viewerProps.setActiveTool) { + viewerProps.setActiveTool(props, metadata.isNested); + } } }); @@ -63,7 +67,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { { name: 'ohif.layoutSelector', defaultComponent: ToolbarLayoutSelector, - clickHandler: (evt, clickedBtn, btnSectionName) => {}, + clickHandler: (evt, clickedBtn, btnSectionName) => { }, }, { name: 'ohif.toggle', diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 589839448..08cc1e3cb 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -79,7 +79,8 @@ export default class ToolBarService { btnIds.forEach(nestedBtnId => { const nestedBtn = this.buttons[nestedBtnId]; - const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, props); + const metadata = { isNested: true }; + const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, metadata, props); nestedButtons.push(mappedNestedBtn); }); @@ -90,7 +91,8 @@ export default class ToolBarService { } else { const btnId = btnIdOrArray; const btn = this.buttons[btnId]; - const mappedBtn = this._mapButtonToDisplay(btn, key, props); + const metadata = { isNested: false }; + const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props); buttonsInSection.push(mappedBtn); } @@ -136,7 +138,7 @@ export default class ToolBarService { * @param {*} btn * @param {*} btnSection */ - _mapButtonToDisplay(btn, btnSection, props) { + _mapButtonToDisplay(btn, btnSection, metadata, props) { const { id, type, component } = btn; const buttonType = this._buttonTypes()[type]; @@ -146,7 +148,7 @@ export default class ToolBarService { const onClick = evt => { if (buttonType.clickHandler) { - buttonType.clickHandler(evt, btn, btnSection); + buttonType.clickHandler(evt, btn, btnSection, metadata, props); } if (btn.props.onClick) { btn.onClick(evt, btn, btnSection); @@ -154,15 +156,12 @@ export default class ToolBarService { if (btn.props.clickHandler) { btn.clickHandler(evt, btn, btnSection); } - if (props && props.onClick) { - props.onClick(evt, btn, btnSection, props); - } }; return { id, Component: component || buttonType.defaultComponent, - componentProps: Object.assign({}, btn.props, props, { onClick }), // + componentProps: Object.assign({}, btn.props, { onClick }), // }; } } From 9c3159cc158a541d166d742917ad317981349182 Mon Sep 17 00:00:00 2001 From: Danny Brown Date: Thu, 25 Jun 2020 15:06:14 -0400 Subject: [PATCH 12/21] more descriptive comment for isDisabled prop --- platform/ui/src/components/Tooltip/Tooltip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index 85119f7c1..42ef4498b 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -88,7 +88,7 @@ Tooltip.defaultProps = { }; Tooltip.propTypes = { - // Allow null +/** prevents tooltip from rendering despite hover/active/sticky */ isDisabled: PropTypes.bool, content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), position: PropTypes.oneOf([ From 52521c5b56cb71360805d219c22abafbd93d7645 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Thu, 25 Jun 2020 16:49:46 -0300 Subject: [PATCH 13/21] Add active state --- .../default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx index c6d8f7a35..1b0bb5144 100644 --- a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx +++ b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx @@ -20,6 +20,8 @@ function NestedMenu({ children, label, icon }) { }; }, [isOpen]); + const isActive = isOpen || children.props.children.some(c => c.props.isActive); + return ( ); From 6eb474be88f258bd6bfd254995bbed2f171ce592 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Thu, 25 Jun 2020 16:51:48 -0300 Subject: [PATCH 14/21] Add active state --- .../default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx | 6 ++---- extensions/default/src/ViewerLayout/index.jsx | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx index 1b0bb5144..6e91c0418 100644 --- a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx +++ b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { ToolbarButton } from '@ohif/ui'; -function NestedMenu({ children, label, icon }) { +function NestedMenu({ children, label, icon, isActive }) { const [isOpen, setIsOpen] = useState(false); const toggleNestedMenu = () => setIsOpen(!isOpen); @@ -20,8 +20,6 @@ function NestedMenu({ children, label, icon }) { }; }, [isOpen]); - const isActive = isOpen || children.props.children.some(c => c.props.isActive); - return ( ); diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index e82352c57..cf2c11826 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -55,7 +55,7 @@ function ViewerLayout({ }; }; - const defaultTool = { icon: 'tool-more-menu', label: 'More' }; + const defaultTool = { icon: 'tool-more-menu', label: 'More', isActive: false }; const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); const [activeTool, setActiveTool] = useState(defaultTool); @@ -102,7 +102,7 @@ function ViewerLayout({ return ; } else { return ( - +
{toolDef.map(x => { const { id, Component, componentProps } = x; From 81a764a78f2cc7f3d68c57e82907bf8d8530f32d Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 26 Jun 2020 10:01:13 +0100 Subject: [PATCH 15/21] [OHIF-177] - JSDocs (#1818) * OHIF-177 JSDocs. * Add proxy for activeDataSource to extensionManager. --- platform/core/src/DICOMSR/dataExchange.js | 16 ++++++++++++++++ platform/core/src/extensions/ExtensionManager.js | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index dac463a12..2b3cfe172 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -82,6 +82,11 @@ const storeMeasurementsOld = async (measurementData, filter, server) => { } }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const downloadReport = measurementData => { const srDataset = generateReport(measurementData); const reportBlob = dcmjs.data.datasetToBlob(srDataset); @@ -91,6 +96,11 @@ const downloadReport = measurementData => { window.location.assign(objectUrl); }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const generateReport = measurementData => { const ids = measurementData.map(md => md.id); const filteredToolState = _getFilteredCornerstoneToolState(ids); @@ -103,6 +113,12 @@ const generateReport = measurementData => { return report.dataset; }; +/** + * + * @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. + */ 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. diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 14dfde227..0ce25d6b6 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -137,6 +137,7 @@ export default class ExtensionManager { getDataSources = dataSourceName => { if (dataSourceName === undefined) { + // Default to the activeDataSource dataSourceName = this.activeDataSource; } @@ -144,6 +145,10 @@ export default class ExtensionManager { return this.dataSourceMap[dataSourceName]; }; + getActiveDataSource = () => { + return this.activeDataSource; + }; + /** * @private * @param {string} moduleType From 8f05cf73c76bf7c7d0ef8c52cf1f85955aa1d960 Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 26 Jun 2020 10:24:19 +0100 Subject: [PATCH 16/21] Add authoring information to exported DICOMs using the DICOMWeb DataSource (#1819) * OHIF-177 JSDocs. * Add proxy for activeDataSource to extensionManager. * Add app authoring information. --- extensions/default/src/DicomWebDataSource/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 33bce395d..93a7b1b2f 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -17,8 +17,10 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data; const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const { urlUtil } = utils; -const VERSION_NAME = 'OHIF-0.1'; -const SR_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; +const ImplementationClassUID = + '2.25.270695996825855179949881587723571202391.2.0.0'; +const ImplementationVersionName = 'OHIF-VIEWER-2.0.0'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; /** * @@ -140,9 +142,9 @@ function createDicomWebApi(dicomWebConfig) { dataset._meta.FileMetaInformationVersion.Value, MediaStorageSOPClassUID: dataset.SOPClassUID, MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: SR_TRANSFER_SYNTAX_UID, - ImplementationClassUID: DicomMetaDictionary.uid(), - ImplementationVersionName: VERSION_NAME, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, }; const denaturalized = denaturalizeDataset(meta); From 1d8f5cfc4280c8f34672a1e823485ba60992ac89 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 10:40:03 -0300 Subject: [PATCH 17/21] Viewport grid load default as 1x1 and load first image of series by default (#1817) --- .../default/src/Panels/PanelStudyBrowser.jsx | 11 +++---- .../src/Toolbar/ToolbarLayoutSelector.jsx | 32 ++++++++++++++----- .../PanelStudyBrowserTracking.jsx | 9 +++--- .../viewports/TrackedCornerstoneViewport.js | 4 --- .../viewer/src/components/ViewportGrid.jsx | 27 +++++++++++++++- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index e2647aca6..221395926 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -127,7 +127,6 @@ function PanelStudyBrowser({ changedDisplaySets, thumbnailImageSrcMap ); - setDisplaySets(mappedDisplaySets); } ); @@ -152,11 +151,11 @@ function PanelStudyBrowser({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + [ + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index 36236b93b..b5a4253ab 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -5,22 +5,40 @@ import { useViewportGrid, } from '@ohif/ui'; +const DEFAULT_LAYOUT = { + type: 'SET_LAYOUT', + payload: { + numCols: 1, + numRows: 1, + }, +}; + function LayoutSelector() { const [isOpen, setIsOpen] = useState(false); const [viewportGridState, dispatch] = useViewportGrid(); - useEffect(() => { - function closeOnOutsideClick() { - if (isOpen) { - setIsOpen(false); - } + const closeOnOutsideClick = () => { + if (isOpen) { + setIsOpen(false); } + }; + + useEffect(() => { window.addEventListener('click', closeOnOutsideClick); return () => { window.removeEventListener('click', closeOnOutsideClick); }; }, [isOpen]); + useEffect(() => { + /* Reset to default layout when component unmounts */ + return () => { + dispatch(DEFAULT_LAYOUT); + }; + }, []); + + const onClickHandler = () => setIsOpen(!isOpen); + const DropdownContent = isOpen ? OHIFLayoutSelector : null; return ( @@ -28,9 +46,7 @@ function LayoutSelector() { id="Layout" label="Grid Layout" icon="tool-layout" - onClick={() => { - setIsOpen(!isOpen); - }} + onClick={onClickHandler} dropdownContent={ DropdownContent !== null && ( stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -285,7 +285,6 @@ function _mapDisplaySets( ) { const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; - displaySets.forEach(ds => { const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID]; const componentType = _getComponentType(ds.Modality); diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index c582bd929..dff5d940f 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -10,8 +10,6 @@ import { useViewportGrid, useViewportDialog, } from '@ohif/ui'; -import debounce from 'lodash.debounce'; -import throttle from 'lodash.throttle'; import { useTrackedMeasurements } from './../getContextModule'; // TODO -> Get this list from the list of tracked measurements. @@ -43,7 +41,6 @@ function TrackedCornerstoneViewport({ const [ { activeViewportIndex, viewports }, - dispatchViewportGrid, ] = useViewportGrid(); // viewportIndex, onSubmit const [viewportDialogState, viewportDialogApi] = useViewportDialog(); @@ -217,7 +214,6 @@ function TrackedCornerstoneViewport({ SliceThickness, } = displaySet.images[0]; - if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { setIsTracked(!isTracked); } diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index fa7a6c7e4..2f99f7b48 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,10 +1,12 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; +import { classes } from '@ohif/core'; +const { ImageSet } = classes; function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; @@ -20,6 +22,29 @@ function ViewerViewportGrid(props) { // TODO -> Need some way of selecting which displaySets hit the viewports. const { DisplaySetService } = servicesManager.services; + useEffect(() => { + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, + displaySets => { + displaySets.sort((a, b) => { + const isImageSet = x => x instanceof ImageSet; + return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1; + }); + dispatch({ + type: 'SET_DISPLAYSET_FOR_VIEWPORT', + payload: { + viewportIndex: 0, + displaySetInstanceUID: displaySets[0].displaySetInstanceUID, + }, + }); + }, + ); + + return () => { + unsubscribe(); + }; + }, []); + // TODO -> Make a HangingProtocolService const HangingProtocolService = displaySets => { let displaySetInstanceUID; From 101efa4cf4309afb0883d07e30de7b0011defc2b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 11:08:16 -0300 Subject: [PATCH 18/21] Add simple loading and default state to datasourcewrapper (#1815) --- .../components/EmptyStudies/EmptyStudies.js | 5 ++- .../viewer/src/routes/DataSourceWrapper.jsx | 23 +++++------ .../viewer/src/routes/WorkList/WorkList.jsx | 40 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/platform/ui/src/components/EmptyStudies/EmptyStudies.js b/platform/ui/src/components/EmptyStudies/EmptyStudies.js index 48e3baaa6..893ecf3e3 100644 --- a/platform/ui/src/components/EmptyStudies/EmptyStudies.js +++ b/platform/ui/src/components/EmptyStudies/EmptyStudies.js @@ -3,12 +3,13 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, Typography } from '@ohif/ui'; -const EmptyStudies = ({ className }) => { +// TODO: Add loading spinner to OHIF + use it here. +const EmptyStudies = ({ className, isLoading }) => { return (
- No studies available + {!isLoading ? 'No studies available' : 'Loading...'}
); diff --git a/platform/viewer/src/routes/DataSourceWrapper.jsx b/platform/viewer/src/routes/DataSourceWrapper.jsx index d0a0711e3..158594128 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.jsx +++ b/platform/viewer/src/routes/DataSourceWrapper.jsx @@ -46,14 +46,17 @@ function DataSourceWrapper(props) { // studies.processResults --> // But only for LayoutTemplate type of 'list'? // Or no data fetching here, and just hand down my source - const [data, setData] = useState(); + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { // 204: no content async function getData() { + setIsLoading(true); const searchResults = await dataSource.query.studies.search( queryFilterValues ); setData(searchResults); + setIsLoading(false); } try { @@ -61,23 +64,19 @@ function DataSourceWrapper(props) { } catch (ex) { console.warn(ex); } - console.log('DataSourceWrapper: useEffect'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [history.location.search]); // queryFilterValues // TODO: Better way to pass DataSource? return ( - - {data && ( - - )} - + ); } diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index 264bc87e2..e3610cf90 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { Suspense, useState, useEffect } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; @@ -28,15 +28,13 @@ const seriesInStudiesMap = new Map(); * TODO: * - debounce `setFilterValues` (150ms?) */ -function WorkList({ history, data: studies, dataSource }) { +function WorkList({ history, data: studies, isLoadingData, dataSource }) { // ~ Modes const [appConfig] = useAppConfig(); // ~ Filters const query = useQuery(); const queryFilterValues = _getQueryFilterValues(query); - const [filterValues, _setFilterValues] = useState( - Object.assign({}, defaultFilterValues, queryFilterValues) - ); + const [filterValues, _setFilterValues] = useState({ ...defaultFilterValues, ...queryFilterValues }); const debouncedFilterValues = useDebounce(filterValues, 200); const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues; @@ -80,6 +78,7 @@ function WorkList({ history, data: studies, dataSource }) { return 0; }); + // ~ Rows & Studies const [expandedRows, setExpandedRows] = useState([]); const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]); @@ -190,6 +189,7 @@ function WorkList({ history, data: studies, dataSource }) { return filterValues[name] !== defaultFilterValues[name]; }); }; + const tableDataSource = sortedStudies.map((study, key) => { const rowKey = key + 1; const isExpanded = expandedRows.some(k => k === rowKey); @@ -211,8 +211,8 @@ function WorkList({ history, data: studies, dataSource }) { content: patientName ? ( patientName ) : ( - (Empty) - ), + (Empty) + ), title: patientName, gridCol: 4, }, @@ -299,13 +299,13 @@ function WorkList({ history, data: studies, dataSource }) { seriesTableDataSource={ seriesInStudiesMap.has(studyInstanceUid) ? seriesInStudiesMap.get(studyInstanceUid).map(s => { - return { - description: s.description || '(empty)', - seriesNumber: s.seriesNumber || '', - modality: s.modality || '', - instances: s.numSeriesInstances || '', - }; - }) + return { + description: s.description || '(empty)', + seriesNumber: s.seriesNumber || '', + modality: s.modality || '', + instances: s.numSeriesInstances || '', + }; + }) : [] } > @@ -322,7 +322,7 @@ function WorkList({ history, data: studies, dataSource }) {
); } From 5180eb127dcefcbd618a28717e9e838fa8b93552 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 11:11:00 -0300 Subject: [PATCH 19/21] OHIF-163: Patient Name is displayed in correct format "Last Name, First Name" (#1820) * Add util to formatpn * Format patient name in patient information tab --- .../default/src/DicomWebDataSource/qido.js | 4 ++-- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 2 +- .../viewports/TrackedCornerstoneViewport.js | 2 +- platform/core/src/utils/formatPN.js | 18 ++++++++++++++++++ platform/core/src/utils/index.js | 2 ++ 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 platform/core/src/utils/formatPN.js diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js index 83613dc0c..00a390424 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -22,7 +22,7 @@ * | limit | {number} | * | offset | {number} | */ -import { DICOMWeb } from '@ohif/core'; +import { DICOMWeb, utils } from '@ohif/core'; const { getString, getName, getModalities } = DICOMWeb; @@ -50,7 +50,7 @@ function processResults(qidoStudies) { time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds) accession: getString(qidoStudy['00080050']) || '', // short string, probably a number? mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber - patientName: getName(qidoStudy['00100010']) || '', + patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '', instances: Number(getString(qidoStudy['00201208'])) || 0, // number description: getString(qidoStudy['00081030']) || '', modalities: diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index dfc685e43..22f343112 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -250,7 +250,7 @@ function OHIFCornerstoneSRViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? PatientName.Alphabetic || '' : '', + patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index dff5d940f..91e28bb84 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -233,7 +233,7 @@ function TrackedCornerstoneViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? PatientName.Alphabetic || '' : '', + patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', diff --git a/platform/core/src/utils/formatPN.js b/platform/core/src/utils/formatPN.js new file mode 100644 index 000000000..fb019ab5e --- /dev/null +++ b/platform/core/src/utils/formatPN.js @@ -0,0 +1,18 @@ +/** + * Formats a patient name for display purposes + */ +export default function formatPN(name) { + if (!name) { + return; + } + + // Convert the first ^ to a ', '. String.replace() only affects + // the first appearance of the character. + const commaBetweenFirstAndLast = name.replace('^', ', '); + + // Replace any remaining '^' characters with spaces + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // Trim any extraneous whitespace + return cleaned.trim(); +} diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 6e9ea3140..a69889262 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable'; import hotkeys from './hotkeys'; import Queue from './Queue'; import isDicomUid from './isDicomUid'; +import formatPN from './formatPN'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; @@ -26,6 +27,7 @@ const utils = { addServers, sortBy, writeScript, + formatPN, b64toBlob, StackManager, studyMetadataManager, From 554b9a0db4956647930130521e8a8b5db99dba5b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 12:05:29 -0300 Subject: [PATCH 20/21] OHIF-174: Patient Info Icon should be clickable and show the correct details (#1816) * Update patient info to be clickable and update fields in viewers * Add spacing * Remove log * round number * Add default --- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 9 +-- .../viewports/TrackedCornerstoneViewport.js | 8 +- .../ViewportActionBar/ViewportActionBar.jsx | 75 ++++++++++--------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 22f343112..c54203427 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -203,9 +203,11 @@ function OHIFCornerstoneSRViewport({ PatientSex, PatientAge, SliceThickness, + ManufacturerModelName, StudyDate, SeriesDescription, SeriesInstanceUID, + PixelSpacing, SeriesNumber, } = activeDisplaySetData; @@ -233,13 +235,10 @@ function OHIFCornerstoneSRViewport({ updateViewport(newMeasurementSelected); }; - console.log(currentImageIdIndex); - return ( <> diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 91e28bb84..eae34e279 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -206,12 +206,15 @@ function TrackedCornerstoneViewport({ SeriesInstanceUID, SeriesNumber, } = displaySet; + const { PatientID, PatientName, PatientSex, PatientAge, SliceThickness, + PixelSpacing, + ManufacturerModelName } = displaySet.images[0]; if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { @@ -222,7 +225,6 @@ function TrackedCornerstoneViewport({ <> alert(`Series ${direction}`)} - showPatientInfo={viewportIndex === activeViewportIndex} showNavArrows={viewportIndex === activeViewportIndex} studyData={{ label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid], @@ -238,8 +240,8 @@ function TrackedCornerstoneViewport({ patientAge: PatientAge || '', MRN: PatientID || '', thickness: `${SliceThickness}mm`, - spacing: '', - scanner: '', + spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + scanner: ManufacturerModelName || '', }, }} /> diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 9133b522c..9129d871c 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui'; @@ -13,9 +13,11 @@ const classes = { const ViewportActionBar = ({ studyData, showNavArrows, - showPatientInfo, + showPatientInfo: patientInfoVisibility, onSeriesChange, }) => { + const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + const { label, isTracked, @@ -37,6 +39,8 @@ const ViewportActionBar = ({ scanner, } = patientInformation; + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo) + const renderIconStatus = () => { if (modality === 'SR') { return ( @@ -60,29 +64,30 @@ const ViewportActionBar = ({ {!isTracked ? ( ) : ( - -
- -
-
- - Series is + +
+ +
+
+ + Series is tracked and can be viewed
in the measurement panel
+
-
- } - > - - - )} + } + > + + + )}
); }; + return (
@@ -131,19 +136,18 @@ const ViewportActionBar = ({
)} - {showPatientInfo && ( -
- -
- )} +
+ +
); }; @@ -174,7 +178,7 @@ ViewportActionBar.propTypes = { ViewportActionBar.defaultProps = { showNavArrows: true, - showPatientInfo: true, + showPatientInfo: false, }; function PatientInfo({ @@ -185,11 +189,14 @@ function PatientInfo({ thickness, spacing, scanner, + isOpen, }) { return (
@@ -236,7 +243,7 @@ function PatientInfo({
- } + )} >
From edd4a4118ceab6c205b41216533634e0907e9b15 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 26 Jun 2020 12:34:43 -0300 Subject: [PATCH 21/21] feat: Modal + ViewportDownloadForm (#1799) * feat: Modal + ViewportDownloadForm * styles for image preview box * modal update & close handler * open modal for Learn More (search info) * change modal default alignment * Update platform/ui/src/components/InputNumber/InputNumber.mdx * nuke inputNumber and use inputText with refactor * make input more reusable * revert inputText "number" stuff * replace inputText to Input * fix input spacing + label * fix modal overlay classes * fix inline styles * fix onclick learn more * fix close icon * revert transition * remove inline styles * remove unecessary stuff --- .../src/CornerstoneViewportDownloadForm.js | 6 +- .../core/src/services/UIModalService/index.js | 2 +- platform/ui/index.js | 1 + platform/ui/src/assets/icons/close.svg | 7 + platform/ui/src/assets/icons/link.svg | 11 + platform/ui/src/assets/icons/unlink.svg | 11 + platform/ui/src/components/Icon/getIcon.jsx | 6 + .../src/components/IconButton/IconButton.jsx | 6 +- platform/ui/src/components/Input/Input.jsx | 6 +- .../ui/src/components/InputText/InputText.jsx | 14 +- platform/ui/src/components/Modal/Modal.css | 3 + platform/ui/src/components/Modal/Modal.jsx | 54 ++- platform/ui/src/components/Select/Select.jsx | 7 +- .../StudyListFilter/StudyListFilter.jsx | 13 +- .../ViewportDownloadForm.jsx | 432 ++++++++++++++++++ .../components/ViewportDownloadForm/index.js | 1 + platform/ui/src/components/index.js | 2 + .../src/contextProviders/ModalComponent.jsx | 2 +- .../ui/src/contextProviders/ModalProvider.jsx | 4 +- platform/ui/tailwind.config.js | 5 +- 20 files changed, 542 insertions(+), 51 deletions(-) create mode 100644 platform/ui/src/assets/icons/close.svg create mode 100644 platform/ui/src/assets/icons/link.svg create mode 100644 platform/ui/src/assets/icons/unlink.svg create mode 100644 platform/ui/src/components/Modal/Modal.css create mode 100644 platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx create mode 100644 platform/ui/src/components/ViewportDownloadForm/index.js diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index babf3bf69..7582c0960 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -145,8 +145,4 @@ CornerstoneViewportDownloadForm.propTypes = { activeViewportIndex: PropTypes.number.isRequired, }; -// export default CornerstoneViewportDownloadForm; - -export default function HelloWorld() { - return
Hello World
; -} +export default CornerstoneViewportDownloadForm; diff --git a/platform/core/src/services/UIModalService/index.js b/platform/core/src/services/UIModalService/index.js index ed2eab96a..884e53500 100644 --- a/platform/core/src/services/UIModalService/index.js +++ b/platform/core/src/services/UIModalService/index.js @@ -33,7 +33,7 @@ const serviceImplementation = { function _show({ content = null, contentProps = null, - shouldCloseOnEsc = false, + shouldCloseOnEsc = true, isOpen = true, closeButton = true, title = null, diff --git a/platform/ui/index.js b/platform/ui/index.js index 2025cfb3b..12b086abd 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -75,6 +75,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, } from './src/components'; diff --git a/platform/ui/src/assets/icons/close.svg b/platform/ui/src/assets/icons/close.svg new file mode 100644 index 000000000..83b259fca --- /dev/null +++ b/platform/ui/src/assets/icons/close.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/link.svg b/platform/ui/src/assets/icons/link.svg new file mode 100644 index 000000000..7c99fc27c --- /dev/null +++ b/platform/ui/src/assets/icons/link.svg @@ -0,0 +1,11 @@ + + + + diff --git a/platform/ui/src/assets/icons/unlink.svg b/platform/ui/src/assets/icons/unlink.svg new file mode 100644 index 000000000..37c53bbb9 --- /dev/null +++ b/platform/ui/src/assets/icons/unlink.svg @@ -0,0 +1,11 @@ + + Unlink + + diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 145539060..8f0a7a534 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -4,6 +4,7 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; +import close from './../../assets/icons/close.svg'; import dottedCircle from './../../assets/icons/dotted-circle.svg'; import circledCheckmark from './../../assets/icons/circled-checkmark.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; @@ -16,6 +17,7 @@ import info from './../../assets/icons/info.svg'; import infoLink from './../../assets/icons/info-link.svg'; import launchArrow from './../../assets/icons/launch-arrow.svg'; import launchInfo from './../../assets/icons/launch-info.svg'; +import link from './../../assets/icons/link.svg'; import listBullets from './../../assets/icons/list-bullets.svg'; import lock from './../../assets/icons/lock.svg'; import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg'; @@ -30,6 +32,7 @@ import sorting from './../../assets/icons/sorting.svg'; import sortingActiveDown from './../../assets/icons/sorting-active-down.svg'; import sortingActiveUp from './../../assets/icons/sorting-active-up.svg'; import tracked from './../../assets/icons/tracked.svg'; +import unlink from './../../assets/icons/unlink.svg'; /** Tools */ import toolZoom from './../../assets/icons/tool-zoom.svg'; @@ -47,6 +50,7 @@ const ICONS = { 'arrow-down': arrowDown, calendar: calendar, cancel: cancel, + close: close, 'dotted-circle': dottedCircle, 'circled-checkmark': circledCheckmark, 'chevron-down': chevronDown, @@ -59,6 +63,7 @@ const ICONS = { 'info-link': infoLink, 'launch-arrow': launchArrow, 'launch-info': launchInfo, + link: link, 'list-bullets': listBullets, lock: lock, 'logo-ohif-small': logoOhifSmall, @@ -73,6 +78,7 @@ const ICONS = { 'sorting-active-up': sortingActiveUp, sorting: sorting, tracked: tracked, + unlink: unlink, /** Tools */ 'tool-zoom': toolZoom, diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index d92a6b9ca..290bfa273 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; const baseClasses = - 'text-center items-center justify-center outline-none font-bold focus:outline-none'; + 'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none'; const roundedClasses = { none: '', @@ -84,7 +84,7 @@ const IconButton = ({ }) => { const buttonElement = useRef(null); - const handleOnClick = (e) => { + const handleOnClick = e => { buttonElement.current.blur(); onClick(e); }; @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => { }, + onClick: () => {}, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/Input/Input.jsx b/platform/ui/src/components/Input/Input.jsx index 51b40f0e5..8f26ad4e7 100644 --- a/platform/ui/src/components/Input/Input.jsx +++ b/platform/ui/src/components/Input/Input.jsx @@ -4,11 +4,11 @@ import Label from '../Label'; import classnames from 'classnames'; const baseInputClasses = - 'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none'; + 'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none'; const transparentClasses = { true: 'bg-transparent', - false: '', + false: 'bg-black', }; const Input = ({ @@ -16,7 +16,7 @@ const Input = ({ containerClassName = '', labelClassName = '', className = '', - transparent = true, + transparent = false, type = 'text', value, onChange, diff --git a/platform/ui/src/components/InputText/InputText.jsx b/platform/ui/src/components/InputText/InputText.jsx index 2a244d7e8..2f6fbe70b 100644 --- a/platform/ui/src/components/InputText/InputText.jsx +++ b/platform/ui/src/components/InputText/InputText.jsx @@ -23,7 +23,7 @@ const InputText = ({ type="text" containerClassName="mr-2" value={value} - onChange={(event) => { + onChange={event => { onChange(event.target.value); }} /> @@ -33,15 +33,17 @@ const InputText = ({ InputText.defaultProps = { value: '', + isSortable: false, + onLabelClick: () => {}, + sortDirection: 'none', }; InputText.propTypes = { label: PropTypes.string.isRequired, - isSortable: PropTypes.bool.isRequired, - sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']) - .isRequired, - onLabelClick: PropTypes.func.isRequired, - value: PropTypes.string, + isSortable: PropTypes.bool, + sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), + onLabelClick: PropTypes.func, + value: PropTypes.any, onChange: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/Modal/Modal.css b/platform/ui/src/components/Modal/Modal.css new file mode 100644 index 000000000..f67d31616 --- /dev/null +++ b/platform/ui/src/components/Modal/Modal.css @@ -0,0 +1,3 @@ +.modal-content { + max-height: calc(100vh - theme('spacing.250px')); +} diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 4ef669cbf..3a245f8aa 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -1,22 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; -import classNames from 'classnames'; -const customStyle = { - overlay: { - zIndex: 1071, - backgroundColor: 'rgb(0, 0, 0, 0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, -}; +import './Modal.css'; + +import { Typography, useModal, IconButton, Icon } from '@ohif/ui'; ReactModal.setAppElement(document.getElementById('root')); const Modal = ({ - className, closeButton, shouldCloseOnEsc, isOpen, @@ -24,15 +16,27 @@ const Modal = ({ onClose, children, }) => { + const { hide } = useModal(); + + const handleClose = () => { + hide(); + }; + const renderHeader = () => { return ( title && ( -
-

{title}

+
+ {title} {closeButton && ( - + + + )}
) @@ -41,22 +45,26 @@ const Modal = ({ return ( - <> - {renderHeader()} -
{children}
- +
{renderHeader()}
+
+ {children} +
); }; +Modal.defaultProps = { + shouldCloseOnEsc: true, +}; + Modal.propTypes = { - className: PropTypes.string, closeButton: PropTypes.bool, shouldCloseOnEsc: PropTypes.bool, isOpen: PropTypes.bool, diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx index 399edd541..0cc410a46 100644 --- a/platform/ui/src/components/Select/Select.jsx +++ b/platform/ui/src/components/Select/Select.jsx @@ -76,10 +76,9 @@ const Select = ({ options={options} value={selectedOptions} onChange={(selectedOptions, { action }) => { - const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce( - (acc, curr) => acc.concat([curr.value]), - [] - ); + const newSelection = !selectedOptions.length + ? selectedOptions + : selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []); onChange(newSelection, action); }} > diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index 2d409a555..0d06bb2e3 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Icon, Typography, InputGroup } from '@ohif/ui'; +import { Button, Icon, Typography, InputGroup, useModal } from '@ohif/ui'; const StudyListFilter = ({ filtersMeta, @@ -20,6 +20,16 @@ const StudyListFilter = ({ }); }; const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100; + const { show } = useModal(); + + const showLearnMoreContent = () => { + const modalContent = () =>
Search Instructions
; + + show({ + content: modalContent, + title: 'Learn More', + }); + }; return ( @@ -38,6 +48,7 @@ const StudyListFilter = ({ color="inherit" className="text-primary-active" startIcon={} + onClick={showLearnMoreContent} > Learn more diff --git a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx new file mode 100644 index 000000000..06c682502 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx @@ -0,0 +1,432 @@ +import React, { + useCallback, + useEffect, + useState, + createRef, + useRef, +} from 'react'; + +import classnames from 'classnames'; + +import { + Typography, + Input, + Tooltip, + IconButton, + Icon, + Select, + InputLabelWrapper, + Button, +} from '@ohif/ui'; + +const FILE_TYPE_OPTIONS = [ + { + value: 'jpg', + label: 'jpg', + }, + { + value: 'png', + label: 'png', + }, +]; + +const DEFAULT_FILENAME = 'image'; +const REFRESH_VIEWPORT_TIMEOUT = 1000; + +const ViewportDownloadForm = ({ + activeViewport, + onClose, + updateViewportPreview, + enableViewport, + disableViewport, + toggleAnnotations, + loadImage, + downloadBlob, + defaultSize, + minimumSize, + maximumSize, + canvasClass, +}) => { + const [filename, setFilename] = useState(DEFAULT_FILENAME); + const [fileType, setFileType] = useState(['jpg']); + + const [dimensions, setDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [showAnnotations, setShowAnnotations] = useState(true); + + const [keepAspect, setKeepAspect] = useState(true); + const [aspectMultiplier, setAspectMultiplier] = useState({ + width: 1, + height: 1, + }); + + const [viewportElement, setViewportElement] = useState(); + const [viewportElementDimensions, setViewportElementDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [downloadCanvas, setDownloadCanvas] = useState({ + ref: createRef(), + width: defaultSize, + height: defaultSize, + }); + + const [viewportPreview, setViewportPreview] = useState({ + src: null, + width: defaultSize, + height: defaultSize, + }); + + const [error, setError] = useState({ + width: false, + height: false, + filename: false, + }); + + const hasError = Object.values(error).includes(true); + + const refreshViewport = useRef(null); + + const onKeepAspectToggle = () => { + const { width, height } = dimensions; + const aspectMultiplier = { ...aspectMultiplier }; + if (!keepAspect) { + const base = Math.min(width, height); + aspectMultiplier.width = width / base; + aspectMultiplier.height = height / base; + setAspectMultiplier(aspectMultiplier); + } + + setKeepAspect(!keepAspect); + }; + + const downloadImage = () => { + downloadBlob( + filename || DEFAULT_FILENAME, + fileType, + viewportElement, + downloadCanvas.ref.current + ); + }; + + /** + * @param {object} value - Input value + * @param {string} dimension - "height" | "width" + */ + const onDimensionsChange = (value, dimension) => { + const oppositeDimension = dimension === 'height' ? 'width' : 'height'; + const sanitizedTargetValue = value.replace(/\D/, ''); + const isEmpty = sanitizedTargetValue === ''; + const newDimensions = { ...dimensions }; + const updatedDimension = isEmpty + ? '' + : Math.min(sanitizedTargetValue, maximumSize); + + if (updatedDimension === dimensions[dimension]) { + return; + } + + newDimensions[dimension] = updatedDimension; + + if (keepAspect && newDimensions[oppositeDimension] !== '') { + newDimensions[oppositeDimension] = Math.round( + newDimensions[dimension] * aspectMultiplier[oppositeDimension] + ); + } + + // In current code, keepAspect is always `true` + // And we always start w/ a square width/height + setDimensions(newDimensions); + + // Only update if value is non-empty + if (!isEmpty) { + setViewportElementDimensions(newDimensions); + setDownloadCanvas(state => ({ + ...state, + ...newDimensions, + })); + } + }; + + const error_messages = { + width: 'The minimum valid width is 100px.', + height: 'The minimum valid height is 100px.', + filename: 'The file name cannot be empty.', + }; + + const renderErrorHandler = errorType => { + if (!error[errorType]) { + return null; + } + + return ( + + {error_messages[errorType]} + + ); + }; + + const validSize = useCallback( + value => (value >= minimumSize ? value : minimumSize), + [minimumSize] + ); + + const loadAndUpdateViewports = useCallback(async () => { + const { width: scaledWidth, height: scaledHeight } = await loadImage( + activeViewport, + viewportElement, + dimensions.width, + dimensions.height + ); + + toggleAnnotations(showAnnotations, viewportElement); + + const scaledDimensions = { + height: validSize(scaledHeight), + width: validSize(scaledWidth), + }; + + setViewportElementDimensions(scaledDimensions); + setDownloadCanvas(state => ({ + ...state, + ...scaledDimensions, + })); + + const { + dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + } = await updateViewportPreview( + viewportElement, + downloadCanvas.ref.current, + fileType + ); + + setViewportPreview(state => ({ + ...state, + src: dataUrl, + width: validSize(viewportElementWidth), + height: validSize(viewportElementHeight), + })); + }, [ + loadImage, + activeViewport, + viewportElement, + dimensions.width, + dimensions.height, + toggleAnnotations, + showAnnotations, + validSize, + updateViewportPreview, + downloadCanvas.ref, + fileType, + ]); + + useEffect(() => { + enableViewport(viewportElement); + + return () => { + disableViewport(viewportElement); + }; + }, [disableViewport, enableViewport, viewportElement]); + + useEffect(() => { + if (refreshViewport.current !== null) { + clearTimeout(refreshViewport.current); + } + + refreshViewport.current = setTimeout(() => { + refreshViewport.current = null; + loadAndUpdateViewports(); + }, REFRESH_VIEWPORT_TIMEOUT); + }, [ + activeViewport, + viewportElement, + showAnnotations, + dimensions, + loadImage, + toggleAnnotations, + updateViewportPreview, + fileType, + downloadCanvas.ref, + minimumSize, + maximumSize, + loadAndUpdateViewports, + ]); + + useEffect(() => { + const { width, height } = dimensions; + const hasError = { + width: width < minimumSize, + height: height < minimumSize, + filename: !filename, + }; + + setError({ ...hasError }); + }, [dimensions, filename, minimumSize]); + + return ( +
+ + Please specify the dimensions, filename, and desired type for the output + image. + + +
+
+ setFilename(value)} + label="File Name" + /> + {renderErrorHandler('filename')} +
+
+
+
+
+ onDimensionsChange(value, 'width')} + data-cy="image-width" + /> + {renderErrorHandler('width')} +
+
+ onDimensionsChange(value, 'height')} + data-cy="image-height" + /> + {renderErrorHandler('height')} +
+
+ +
+ + + + + +
+
+ +
+
+ {}} + > + setShowAnnotations(event.target.checked)} + /> + Show Annotations + +
+
+
+
+ +
+
setViewportElement(ref)} + > + +
+ + {viewportPreview.src ? ( +
+ Image preview + Preview +
+ ) : ( +
+ Loading Image Preview... +
+ )} +
+ +
+ + +
+
+ ); +}; + +export default ViewportDownloadForm; diff --git a/platform/ui/src/components/ViewportDownloadForm/index.js b/platform/ui/src/components/ViewportDownloadForm/index.js new file mode 100644 index 000000000..d630f3a19 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/index.js @@ -0,0 +1 @@ +export { default } from './ViewportDownloadForm'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 04b5270cc..77247ac4d 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -45,6 +45,7 @@ import Tooltip from './Tooltip'; import Typography from './Typography'; import Viewport from './Viewport'; import ViewportActionBar from './ViewportActionBar'; +import ViewportDownloadForm from './ViewportDownloadForm'; import ViewportGrid from './ViewportGrid'; import ViewportPane from './ViewportPane'; @@ -97,6 +98,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, }; diff --git a/platform/ui/src/contextProviders/ModalComponent.jsx b/platform/ui/src/contextProviders/ModalComponent.jsx index b3b6602a7..cf7965812 100644 --- a/platform/ui/src/contextProviders/ModalComponent.jsx +++ b/platform/ui/src/contextProviders/ModalComponent.jsx @@ -16,7 +16,7 @@ const ModalComponent = ({ ModalComponent.defaultProps = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/src/contextProviders/ModalProvider.jsx b/platform/ui/src/contextProviders/ModalProvider.jsx index e8d65635d..ff9224bca 100644 --- a/platform/ui/src/contextProviders/ModalProvider.jsx +++ b/platform/ui/src/contextProviders/ModalProvider.jsx @@ -19,7 +19,7 @@ export const useModal = () => useContext(ModalContext); * @typedef {Object} ModalProps * @property {ReactElement|HTMLElement} [content=null] Modal content. * @property {Object} [contentProps=null] Modal content props. - * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [shouldCloseOnEsc=true] Modal is dismissible via the esc key. * @property {boolean} [isOpen=true] Make the Modal visible or hidden. * @property {boolean} [closeButton=true] Should the modal body render the close button. * @property {string} [title=null] Should the modal render the title independently of the body content. @@ -30,7 +30,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { const DEFAULT_OPTIONS = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index 5cdbcdf50..3e32e4955 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -10,6 +10,7 @@ module.exports = { xl: '1280px', }, colors: { + overlay: 'rgba(0, 0, 0, 0.8)', transparent: 'transparent', black: '#000', white: '#fff', @@ -17,10 +18,10 @@ module.exports = { inherit: 'inherit', indigo: { - dark: '#0b1a42' + dark: '#0b1a42', }, aqua: { - pale: '#7bb2ce' + pale: '#7bb2ce', }, primary: {