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/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/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/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/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 ( <> 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, }) ), diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 52c3ff3b7..5a88f296b 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -67,7 +67,7 @@ "classnames": "^2.2.6", "core-js": "^3.2.1", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.15.1", + "cornerstone-tools": "4.16.0", "cornerstone-wado-image-loader": "^3.1.2", "dcmjs": "^0.12.2", "dicom-parser": "^1.8.3", 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', }; diff --git a/yarn.lock b/yarn.lock index eb88b95ac..a045e8f39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6755,10 +6755,10 @@ cornerstone-math@^0.1.8: resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5" integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw== -cornerstone-tools@4.15.1: - version "4.15.1" - resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.15.1.tgz#f0b1026da9c7758defc088bb2f1e78426a23d91e" - integrity sha512-fJuTUJW/NDSD520jPB++tX//Kq80jPDngChHZrhx2xjj/9dCnexD9kmTX30Z0WUq0a2JEHZ6FlcOX38kSmw1hQ== +cornerstone-tools@4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.0.tgz#af3d32d13722b97bec258492642e622312280196" + integrity sha512-kUhuSb2Ixpd2hgbdem+740rnN4hmoxzcOBNaUcsizFRWjMAsgc0yUxBFwLl0mIs811mefq79JOL+juwRA8T3Tg== dependencies: "@babel/runtime" "7.1.2" cornerstone-math "0.1.7"