From 14005d61f3ee9111e2b3d145fd5b0f505e614f45 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Mon, 29 Jun 2020 18:22:19 +0100 Subject: [PATCH 01/13] WIP rehydration. --- extensions/cornerstone/package.json | 2 +- extensions/cornerstone/src/init.js | 4 + .../ArrowAnnotate.js | 2 +- .../Bidirectional.js | 2 +- .../EllipticalRoi.js | 2 +- .../measurementServiceMappings/Length.js | 5 +- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 144 +++++++++++-- .../dicom-sr/src/getSopClassHandlerModule.js | 70 +++++- extensions/dicom-sr/src/index.js | 11 +- .../dicom-sr/src/utils/addMeasurement.js | 6 + ...ToolStateToCornerstoneMeasurementSchema.js | 201 ++++++++++++++++++ .../viewports/TrackedCornerstoneViewport.js | 17 +- .../MeasurementService/MeasurementService.js | 115 ++++++++++ .../ViewportActionBar/ViewportActionBar.jsx | 172 ++++++++------- 14 files changed, 649 insertions(+), 104 deletions(-) create mode 100644 extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 6865c3beb..12dd14100 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone", - "version": "2.7.3", + "version": "3.0.0", "description": "OHIF extension for Cornerstone", "author": "OHIF", "license": "MIT", diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index e7b321935..ac1a629ee 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -246,10 +246,14 @@ const _connectToolsToMeasurementService = measurementService => { function addMeasurement(csToolsEvent) { console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail); + try { const evtDetail = csToolsEvent.detail; const { toolName, toolType, measurementData } = evtDetail; const csToolName = toolName || measurementData.toolType || toolType; + + debugger; + const measurementId = addOrUpdate(csToolName, evtDetail); if (measurementId) { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js index 7d2c1c3b2..f36c64ad8 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -30,7 +30,7 @@ const ArrowAnnotate = { points.push(measurementData.handles); return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js index 5b6f3e893..25ca59b12 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js @@ -31,7 +31,7 @@ const Bidirectional = { const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js index dbd4d2403..b2a24187e 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js @@ -53,7 +53,7 @@ const EllipticalRoi = { } return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js index 5f9eda649..a7f9b1882 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -26,7 +26,7 @@ const Length = { text: label, description, handles: getHandlesFromPoints(points), - _measurementServiceId: id, + idid, }, }; }, @@ -57,9 +57,6 @@ const Length = { StudyInstanceUID, } = getSOPInstanceAttributes(element); - const points = []; - points.push(measurementData.handles); - return { id: measurementData._measurementServiceId, SOPInstanceUID: SOPInstanceUID, diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 35fe5d621..45eb4bf10 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -2,35 +2,35 @@ import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; - import CornerstoneViewport from 'react-cornerstone-viewport'; -import OHIF from '@ohif/core'; +import OHIF, { DicomMetadataStore } from '@ohif/core'; import { ViewportActionBar, useViewportGrid } from '@ohif/ui'; import TOOL_NAMES from './constants/toolNames'; +import { adapters } from 'dcmjs'; +import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema'; import id from './id'; const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); +const globalImageIdSpecificToolStateManager = + cornerstoneTools.globalImageIdSpecificToolStateManager; -// const cine = viewportSpecificData.cine; - -// isPlaying = cine.isPlaying === true; -// frameRate = cine.cineFrameRate || frameRate; - -const { StackManager } = OHIF.utils; +const { StackManager, guid } = OHIF.utils; function OHIFCornerstoneSRViewport({ children, dataSource, displaySet, viewportIndex, - DisplaySetService, + servicesManager, }) { + const { DisplaySetService, MeasurementService } = servicesManager.services; const [viewportGrid, viewportGridService] = useViewportGrid(); const [measurementSelected, setMeasurementSelected] = useState(0); const [measurementCount, setMeasurementCount] = useState(1); const [viewportData, setViewportData] = useState(null); const [activeDisplaySetData, setActiveDisplaySetData] = useState({}); const [element, setElement] = useState(null); + const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; @@ -195,8 +195,6 @@ function OHIFCornerstoneSRViewport({ const { Modality } = displaySet; - // TODO -> Get this from the associated stack. - const { PatientID, PatientName, @@ -228,33 +226,122 @@ function OHIFCornerstoneSRViewport({ } } - if (newMeasurementSelected === measurementSelected) { - // TODO -> Jump to image in this case. - } - updateViewport(newMeasurementSelected); }; + function hydrateMeasurementService() { + // TODO -> We should define a strict versioning somewhere. + const mappings = MeasurementService.getSourceMappings( + 'CornerstoneTools', + '4' + ); + + if (!mappings || !mappings.length) { + throw new Error( + `Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.` + ); + } + + const instance = DicomMetadataStore.getInstance( + displaySet.StudyInstanceUID, + displaySet.SeriesInstanceUID, + displaySet.SOPInstanceUID + ); + + const { MeasurementReport } = adapters.Cornerstone; + + const sopInstanceUIDToImageId = {}; + + displaySet.measurements.forEach(measurement => { + const { ReferencedSOPInstanceUID, imageId } = measurement; + if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) { + sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId; + } + }); + + // Use dcmjs to generate toolState. + const storedMeasurementByToolType = MeasurementReport.generateToolState( + instance + ); + + // Filter what is found by DICOM SR to measurements we support. + const mappingDefinitions = mappings.map(m => m.definition); + const hydratableMeasurementsInSR = {}; + + Object.keys(storedMeasurementByToolType).forEach(key => { + if (mappingDefinitions.includes(key)) { + hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key]; + } + }); + + Object.keys(hydratableMeasurementsInSR).forEach(toolType => { + const toolDataForToolType = hydratableMeasurementsInSR[toolType]; + + toolDataForToolType.forEach(data => { + // Add the measurement to toolState + const imageId = sopInstanceUIDToImageId[data.sopInstanceUid]; + + data.id = guid(); + + _addToolDataToCornerstoneTools(data, toolType, imageId); + + // Let the measurement service know we added to toolState + const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema( + toolType, + MeasurementService, + imageId + ); + + const source = MeasurementService.getSource('CornerstoneTools', '4'); + + MeasurementService.addRawMeasurement( + source, + toolType, + data, + toMeasurementSchema + ); + }); + }); + + displaySet.isHydrated = true; + + setIsHydrated(true); + + // TODO -> Switch to cornerstone viewport. + // TODO -> Tell measurement service to track the series on which the measurements were added. + // TODO -> set displaySet as inactive. + debugger; + } + return ( <> { _checkIfCanAddMeasurementsToDisplaySet( @@ -119,6 +130,49 @@ function _getDisplaySetsFromSeries( return [displaySet]; } +function _isRehydratable(displaySet, MeasurementService) { + const mappings = MeasurementService.getSourceMappings( + 'CornerstoneTools', + '4' + ); + + if (!mappings || !mappings.length) { + return false; + } + + const mappingDefinitions = mappings.map(m => m.definition); + const { measurements } = displaySet; + + const adapterKeys = Object.keys(cornerstoneAdapters).filter( + adapterKey => + typeof cornerstoneAdapters[adapterKey] + .isValidCornerstoneTrackingIdentifier === 'function' + ); + + const adapters = []; + + adapterKeys.forEach(key => { + if (mappingDefinitions.includes(key)) { + // Must have both a dcmjs adapter and a MeasurementService + // Definition in order to be a candidate for import. + adapters.push(cornerstoneAdapters[key]); + } + }); + + for (let i = 0; i < measurements.length; i++) { + const TrackingIdentifier = measurements[i].TrackingIdentifier; + const hydratable = adapters.some(adapter => + adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier) + ); + + if (hydratable) { + return true; + } + } + + return false; +} + function _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, newDisplaySet, @@ -345,6 +399,12 @@ function _processTID1410Measurement(mergedContentSequence) { group => group.ValueType === 'UIDREF' ); + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.TrackingIdentifier + ); + if (!graphicItem) { console.warn( `graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.` @@ -361,6 +421,7 @@ function _processTID1410Measurement(mergedContentSequence) { labels: [], coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)], TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; NUMContentItems.forEach(item => { @@ -388,11 +449,18 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { group => group.ValueType === 'UIDREF' ); + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.TrackingIdentifier + ); + const measurement = { loaded: false, labels: [], coords: [], TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; NUMContentItems.forEach(item => { diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index 8a2a6d98a..5bf7ea5f4 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -23,6 +23,13 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, + dependencies: [ + // TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future. + { + id: 'org.ohif.cornerstone', + version: '3.0.0', + }, + ], preRegistration({ servicesManager, configuration = {} }) { init({ servicesManager, configuration }); @@ -36,11 +43,9 @@ export default { */ getViewportModule({ servicesManager }) { const ExtendedOHIFCornerstoneSRViewport = props => { - const { DisplaySetService } = servicesManager.services; - return ( ); diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index 8ad925b2c..f3d28c317 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -62,6 +62,12 @@ export default function addMeasurement( measurement.loaded = true; measurement.imageId = imageId; measurement.displaySetInstanceUID = displaySetInstanceUID; + + // Remove the unneeded coord now its processed, but keep the SOPInstanceUID. + // NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame, + // It'd be super werid if it didn't anyway as a SCOORD. + measurement.ReferencedSOPInstanceUID = + measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; delete measurement.coords; } diff --git a/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js b/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js new file mode 100644 index 000000000..2bdd3d16b --- /dev/null +++ b/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js @@ -0,0 +1,201 @@ +export default function getToolStateToCornerstoneMeasurementSchema( + toolType, + MeasurementService, + imageId +) { + const _getValueTypeFromToolType = toolType => { + const { + POLYLINE, + ELLIPSE, + POINT, + BIDIRECTIONAL, + } = MeasurementService.VALUE_TYPES; + + // TODO -> I get why this was attemped, but its not nearly flexible enough. + // A single measurement may have an ellipse + a bidirectional measurement, for instances. + // You can't define a bidirectional tool as a single type.. + const TOOL_TYPE_TO_VALUE_TYPE = { + Length: POLYLINE, + EllipticalRoi: ELLIPSE, + Bidirectional: BIDIRECTIONAL, + ArrowAnnotate: POINT, + }; + + return TOOL_TYPE_TO_VALUE_TYPE[toolType]; + }; + + switch (toolType) { + case 'Length': + return measurementData => + Length(measurementData, imageId, _getValueTypeFromToolType); + case 'Bidirectional': + return measurementData => + Bidirectional(measurementData, imageId, _getValueTypeFromToolType); + case 'EllipticalRoi': + return measurementData => + EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType); + case 'ArrowAnnotate': + return measurementData => + ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType); + } +} + +function Length(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const points = []; + Object.keys(handles).map(handle => { + if (['start', 'end'].includes(handle)) { + let point = {}; + if (handles[handle].x) point.x = handles[handle].x; + if (handles[handle].y) point.y = handles[handle].y; + points.push(point); + } + }); + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: _getValueTypeFromToolType(tool), + points, + }; +} + +function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const longAxis = [handles.start, handles.end]; + const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + shortestDiameter: measurementData.shortestDiameter, + longestDiameter: measurementData.longestDiameter, + type: _getValueTypeFromToolType(tool), + points: { longAxis, shortAxis }, + }; +} + +function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { start, end } = measurementData.handles; + + const halfXLength = Math.abs(start.x - end.x) / 2; + const halfYLength = Math.abs(start.y - end.y) / 2; + + const points = []; + const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; + + // To store similar to SR. + if (halfXLength > halfYLength) { + // X-axis major + // Major axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + // Minor axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + } else { + // Y-axis major + // Major axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + // Minor axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + } + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + area: + measurementData.cachedStats && + measurementData.cachedStats + .area /* TODO: Add concept names instead (descriptor) */, + type: _getValueTypeFromToolType(tool), + points, + }; +} + +function ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const points = []; + Object.keys(handles).map(handle => { + if (['start', 'end'].includes(handle)) { + let point = {}; + if (handles[handle].x) point.x = handles[handle].x; + if (handles[handle].y) point.y = handles[handle].y; + points.push(point); + } + }); + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + text: measurementData.text, + type: _getValueTypeFromToolType(tool), + points, + }; +} diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 79138339a..7058bb506 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -13,6 +13,10 @@ import { import { useTrackedMeasurements } from './../getContextModule'; // TODO -> Get this list from the list of tracked measurements. +// TODO -> We can now get a list of tool names from the measurement service. +// Use the toolnames to check which tools we have instead, using the +// Classes isn't really extensible unless we add the classes to the measurement +// Service definition, which feels wrong. const { ArrowAnnotateTool, BidirectionalTool, @@ -223,7 +227,7 @@ function TrackedCornerstoneViewport({ PatientAge, SliceThickness, PixelSpacing, - ManufacturerModelName + ManufacturerModelName, } = displaySet.images[0]; if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { @@ -244,12 +248,19 @@ function TrackedCornerstoneViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', + patientName: PatientName + ? OHIF.utils.formatPN(PatientName.Alphabetic) + : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', thickness: `${SliceThickness}mm`, - spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + spacing: + PixelSpacing && PixelSpacing.length + ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( + 2 + )}mm` + : '', scanner: ManufacturerModelName || '', }, }} diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index ba9ddcf61..dbaabfc63 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -155,6 +155,32 @@ class MeasurementService { return source; } + getSource(name, version) { + const { sources } = this; + const id = this._getSourceId(name, version); + + return sources[id]; + } + + getSourceMappings(name, version) { + const { mappings } = this; + const id = this._getSourceId(name, version); + + return mappings[id]; + } + + _getSourceId(name, version) { + const { sources } = this; + + const sourceId = Object.keys(sources).find(sourceId => { + const source = sources[sourceId]; + + return source.name === name && source.version === version; + }); + + return sourceId; + } + /** * Add a new measurement matching criteria along with mapping functions. * @@ -256,6 +282,95 @@ class MeasurementService { } } + /** + * Add a raw measurement into a source so that it may be + * Converted to/from annotation in the same way. E.g. import serialized data + * Of the same form as the measurement source. + * @param {MeasurementSource} source The measurement source instance. + * @param {string} definition The source definition you want to add the measuremnet to. + * @param {object} data The data you wish to add to the source. + * @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition. + */ + addRawMeasurement(source, definition, data, toMeasurementSchema) { + if (!this._isValidSource(source)) { + log.warn('Invalid source. Exiting early.'); + return; + } + + debugger; + + const sourceInfo = this._getSourceInfo(source); + + if (!definition) { + log.warn('No source definition provided. Exiting early.'); + return; + } + + if (!this._sourceHasMappings(source)) { + log.warn( + `No measurement mappings found for '${sourceInfo}' source. Exiting early.` + ); + return; + } + + let measurement = {}; + try { + /* Convert measurement */ + measurement = toMeasurementSchema(data); + + /* Assign measurement source instance */ + measurement.source = source; + } catch (error) { + log.warn( + `Failed to map '${sourceInfo}' measurement for definition ${definition}:`, + error.message + ); + return; + } + + if (!this._isValidMeasurement(measurement)) { + log.warn( + `Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.` + ); + return; + } + + let internalId = data.id; + if (!internalId) { + internalId = guid(); + log.warn(`Measurement ID not found. Generating UID: ${internalId}`); + } + + const newMeasurement = { + ...measurement, + modifiedTimestamp: Math.floor(Date.now() / 1000), + id: internalId, + }; + + if (this.measurements[internalId]) { + log.info( + `Measurement already defined. Updating measurement.`, + newMeasurement + ); + this.measurements[internalId] = newMeasurement; + this._broadcastChange( + this.EVENTS.MEASUREMENT_UPDATED, + source, + newMeasurement + ); + } else { + log.info(`Measurement added.`, newMeasurement); + this.measurements[internalId] = newMeasurement; + this._broadcastChange( + this.EVENTS.MEASUREMENT_ADDED, + source, + newMeasurement + ); + } + + return newMeasurement.id; + } + /** * Adds or update persisted measurements. * diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 12f052e76..77dddce1b 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -15,13 +15,21 @@ const ViewportActionBar = ({ showNavArrows, showPatientInfo: patientInfoVisibility, onSeriesChange, + onHydrationClick, }) => { const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + // TODO -> Remake this component with a bunch of generic slots that can be filled, + // Its not generic at all, isTracked etc shouldn't be parts of this component. + // It shouldn't care that a tracking mode or SR exists. + // Things like the right/left buttons should be made into smaller + // Components you can compose. + const { label, isTracked, isLocked, + isHydrated, modality, studyDate, currentSeries, @@ -39,23 +47,35 @@ const ViewportActionBar = ({ scanner, } = patientInformation; - const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo) + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo); const renderIconStatus = () => { if (modality === 'SR') { return ( -
- - SR - - {isLocked && ( - + <> +
+ + SR + + {isLocked && ( + + )} +
+ {!isLocked && !isHydrated && ( +
+ + Edit + +
)} -
+ ); } @@ -64,26 +84,26 @@ const ViewportActionBar = ({ {!isTracked ? ( ) : ( - -
- -
-
- - Series is + +
+ +
+
+ + Series is tracked and can be viewed
in the measurement panel
-
- } - > - -
- )} + + } + > + + + )} ); }; @@ -196,54 +216,64 @@ function PatientInfo({ isSticky isDisabled={!isOpen} position="bottom-right" - content={isOpen && ( -
-
- -
-
- - {patientName} - -
-
- Sex - - {patientSex} - -
-
- Age - - {patientAge} - -
-
- MRN - {MRN} -
+ content={ + isOpen && ( +
+
+
-
-
- - Thickness - - - {thickness} - +
+ + {patientName} + +
+
+ Sex + + {patientSex} + +
+
+ Age + + {patientAge} + +
+
+ MRN + {MRN} +
-
- Spacing - {spacing} -
-
- Scanner - {scanner} +
+
+ + Thickness + + + {thickness} + +
+
+ + Spacing + + + {spacing} + +
+
+ + Scanner + + + {scanner} + +
-
- )} + ) + } >
From baedf243bc377fbbc4a93c755d5c2e356002f7bd Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 10:28:46 +0100 Subject: [PATCH 02/13] WIP OHIF-190 rehydrate. --- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 82 ++++++++++++++++++- extensions/dicom-sr/src/index.js | 7 +- .../measurementTrackingMachine.js | 13 +++ .../MeasurementService/MeasurementService.js | 2 - .../viewer/src/components/ViewportGrid.jsx | 4 +- 5 files changed, 100 insertions(+), 8 deletions(-) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 45eb4bf10..a228ea220 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; @@ -16,12 +16,15 @@ const globalImageIdSpecificToolStateManager = const { StackManager, guid } = OHIF.utils; +const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking'; + function OHIFCornerstoneSRViewport({ children, dataSource, displaySet, viewportIndex, servicesManager, + extensionManager, }) { const { DisplaySetService, MeasurementService } = servicesManager.services; const [viewportGrid, viewportGridService] = useViewportGrid(); @@ -31,9 +34,29 @@ function OHIFCornerstoneSRViewport({ const [activeDisplaySetData, setActiveDisplaySetData] = useState({}); const [element, setElement] = useState(null); const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); - const { viewports, activeViewportIndex } = viewportGrid; + // Optional hook into tracking extension, if present. + let trackedMeasurements; + let sendTrackedMeasurementsEvent; + + if ( + extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ) + ) { + const contextModule = extensionManager.getModuleEntry( + 'org.ohif.measurement-tracking.contextModule.TrackedMeasurementsContext' + ); + + const useTrackedMeasurements = () => useContext(contextModule.context); + + [ + trackedMeasurements, + sendTrackedMeasurementsEvent, + ] = useTrackedMeasurements(); + } + const onElementEnabled = evt => { const eventData = evt.detail; const targetElement = eventData.element; @@ -207,6 +230,7 @@ function OHIFCornerstoneSRViewport({ SeriesInstanceUID, PixelSpacing, SeriesNumber, + displaySetInstanceUID, } = activeDisplaySetData; const onMeasurementChange = direction => { @@ -274,6 +298,8 @@ function OHIFCornerstoneSRViewport({ } }); + const imageIds = []; + Object.keys(hydratableMeasurementsInSR).forEach(toolType => { const toolDataForToolType = hydratableMeasurementsInSR[toolType]; @@ -300,6 +326,10 @@ function OHIFCornerstoneSRViewport({ data, toMeasurementSchema ); + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } }); }); @@ -309,8 +339,53 @@ function OHIFCornerstoneSRViewport({ // TODO -> Switch to cornerstone viewport. // TODO -> Tell measurement service to track the series on which the measurements were added. - // TODO -> set displaySet as inactive. + + // Deal with optional extensions + + if ( + extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ) + ) { + // Set the series touched as tracked. + + let targetStudyInstanceUID; + const SeriesInstanceUIDs = []; + + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[0]; + const { + SeriesInstanceUID, + StudyInstanceUID, + } = cornerstone.metaData.get('instance', imageId); + + if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { + SeriesInstanceUIDs.push(SeriesInstanceUID); + } + + if (!targetStudyInstanceUID) { + targetStudyInstanceUID = StudyInstanceUID; + } else if (targetStudyInstanceUID !== StudyInstanceUID) { + console.warn( + 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.' + ); + } + } + + debugger; + + sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', { + StudyInstanceUID: targetStudyInstanceUID, + SeriesInstanceUID: SeriesInstanceUIDs[0], + }); + } + debugger; + + viewportGridService.setDisplaysetForViewport({ + viewportIndex: activeViewportIndex, + displaySetInstanceUID: activeDisplaySetData.displaySetInstanceUID, + }); } return ( @@ -460,6 +535,7 @@ async function _getViewportAndActiveDisplaySetData( SeriesDescription: image0.SeriesDescription, SeriesInstanceUID: image0.SeriesInstanceUID, SeriesNumber: image0.SeriesNumber, + displaySetInstanceUID, }; return { viewportData, activeDisplaySetData }; diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index 5bf7ea5f4..879478388 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -29,6 +29,10 @@ export default { id: 'org.ohif.cornerstone', version: '3.0.0', }, + { + id: 'org.ohif.measurement-tracking', + version: '^0.0.1', + }, ], preRegistration({ servicesManager, configuration = {} }) { @@ -41,11 +45,12 @@ export default { * @param {object} [configuration={}] * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` */ - getViewportModule({ servicesManager }) { + getViewportModule({ servicesManager, extensionManager }) { const ExtendedOHIFCornerstoneSRViewport = props => { return ( ); diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 680fabf49..5ff597b9a 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -15,6 +15,18 @@ const machineConfiguration = { entry: 'clearContext', on: { TRACK_SERIES: 'promptBeginTracking', + SET_TRACKED_SERIES: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndSeries'], + cond: 'isNewStudy', + }, + { + target: 'tracking', + actions: ['addTrackedSeries'], + cond: 'isNewSeries', + }, + ], }, }, promptBeginTracking: { @@ -51,6 +63,7 @@ const machineConfiguration = { cond: 'isNewSeries', }, ], + UNTRACK_SERIES: [ { target: 'tracking', diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index dbaabfc63..b97c09c55 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -297,8 +297,6 @@ class MeasurementService { return; } - debugger; - const sourceInfo = this._getSourceInfo(source); if (!definition) { diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index e6384b82e..950b04ee7 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -28,13 +28,13 @@ function ViewerViewportGrid(props) { displaySets => { displaySets.sort((a, b) => { const isImageSet = x => x instanceof ImageSet; - return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1; + return isImageSet(a) === isImageSet(b) ? 0 : isImageSet(a) ? -1 : 1; }); viewportGridService.setDisplaysetForViewport({ viewportIndex: 0, displaySetInstanceUID: displaySets[0].displaySetInstanceUID, }); - }, + } ); return () => { From b99488ad1b5298ea3ebea271d34c3e23dbc6a93f Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 16:09:54 +0100 Subject: [PATCH 03/13] Remove debugger statements. --- extensions/cornerstone/src/init.js | 2 -- extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index ac1a629ee..319a4c484 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -252,8 +252,6 @@ const _connectToolsToMeasurementService = measurementService => { const { toolName, toolType, measurementData } = evtDetail; const csToolName = toolName || measurementData.toolType || toolType; - debugger; - const measurementId = addOrUpdate(csToolName, evtDetail); if (measurementId) { diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index cc80065be..5f71ce062 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -343,14 +343,10 @@ function OHIFCornerstoneSRViewport({ } } - debugger; - sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', { StudyInstanceUID: targetStudyInstanceUID, SeriesInstanceUIDs, }); - - debugger; } Object.keys(hydratableMeasurementsInSR).forEach(toolType => { @@ -390,13 +386,6 @@ function OHIFCornerstoneSRViewport({ setIsHydrated(true); - // TODO -> Switch to cornerstone viewport. - // TODO -> Tell measurement service to track the series on which the measurements were added. - - // Deal with optional extensions - - debugger; - viewportGridService.setDisplaysetForViewport({ viewportIndex: activeViewportIndex, displaySetInstanceUID: activeDisplaySetData.displaySetInstanceUID, From 4ec07dba0bb35a2ccee631da8dc8498f67c654c3 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 16:35:32 +0100 Subject: [PATCH 04/13] Merge in and fix after OHIF-197 and OHIF-198 --- extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 5f71ce062..7e3e85253 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -37,6 +37,8 @@ function OHIFCornerstoneSRViewport({ const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; + console.log(isHydrated); + // Optional hook into tracking extension, if present. let trackedMeasurements; let sendTrackedMeasurementsEvent; From ef8683b8ea83ec02e39250b517e1dc2c9d270900 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 16:36:09 +0100 Subject: [PATCH 05/13] Remove console.log --- extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 7e3e85253..5f71ce062 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -37,8 +37,6 @@ function OHIFCornerstoneSRViewport({ const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; - console.log(isHydrated); - // Optional hook into tracking extension, if present. let trackedMeasurements; let sendTrackedMeasurementsEvent; From 690442555c2931e5a5de1d26b03e0fe40342776e Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 16:58:20 +0100 Subject: [PATCH 06/13] Fix typo --- .../src/utils/measurementServiceMappings/Length.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js index a7f9b1882..d2021adb5 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -26,7 +26,7 @@ const Length = { text: label, description, handles: getHandlesFromPoints(points), - idid, + id, }, }; }, @@ -58,7 +58,7 @@ const Length = { } = getSOPInstanceAttributes(element); return { - id: measurementData._measurementServiceId, + id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, From 2704bd7cbc6ca149b72bf5aac667a77f3b8a0042 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 17:20:20 +0100 Subject: [PATCH 07/13] fix length tracking bug --- .../cornerstone/src/utils/measurementServiceMappings/Length.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js index d2021adb5..a5b7c1e9d 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -58,7 +58,7 @@ const Length = { } = getSOPInstanceAttributes(element); return { - id, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, From 56358ad95fb1fef52adf525e686bb14abef353a2 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Tue, 30 Jun 2020 18:00:37 +0100 Subject: [PATCH 08/13] WIP callout in SR viewport. --- .../dicom-sr/src/tools/DICOMSRDisplayTool.js | 76 ++++++++++++++++++- .../dicom-sr/src/utils/addMeasurement.js | 1 + 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index f0dc64b4e..f744c5c91 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -12,6 +12,7 @@ const drawEllipse = importInternal('drawing/drawEllipse'); const drawHandles = importInternal('drawing/drawHandles'); const getNewContext = importInternal('drawing/getNewContext'); const BaseTool = importInternal('base/BaseTool'); +const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox'); /** * @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay). @@ -61,16 +62,16 @@ export default class DICOMSRDisplayTool extends BaseTool { for (let i = 0; i < filteredToolData.length; i++) { const data = filteredToolData[i]; - const { renderableData } = data; + const { renderableData, labels } = data; const color = data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier ? toolColors.getActiveColor() : toolColors.getToolColor(); - + const lineWidth = 2; const options = { color, - lineWidth: 2, + lineWidth, handleRadius: 6, }; @@ -105,6 +106,43 @@ export default class DICOMSRDisplayTool extends BaseTool { break; } }); + + const { element } = eventData; + const context = getNewContext(eventData.canvasContext.canvas); + + debugger; + + const text = _getTextBoxLinesFromLabels(labels); + + debugger; + + function textBoxAnchorPoints() { + return [{ x: 1000, y: 1000 }]; + } + + draw(context, context => { + drawLinkedTextBox( + context, + element, + { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true, + x: 256, + y: 256, + }, + text, + null, + textBoxAnchorPoints, + color, + lineWidth, + 10, + true + ); + }); } } @@ -161,3 +199,35 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } } + +function _getTextBoxLinesFromLabels(labels) { + // TODO -> max 2 for now, need a generic solution for this! + + const labelLength = Math.min(labels.length, 2); + + const lines = []; + + for (let i = 0; i < labelLength; i++) { + const labelEntry = labels[i]; + lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`); + } + + return lines; +} + +const SHORT_HAND_MAP = { + 'Short Axis': 'S ', + 'Long Axis': 'L ', + AREA: 'Area ', + Length: '', +}; + +function _labelToShorthand(label) { + const shortHand = SHORT_HAND_MAP[label]; + + if (shortHand !== undefined) { + return shortHand; + } + + return label; +} diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index f3d28c317..dc2211244 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -26,6 +26,7 @@ export default function addMeasurement( const measurementData = { TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier, renderableData: {}, + labels: measurement.labels, }; measurement.coords.forEach(coord => { From e3f2e26e3bff79857a29b54ee8ec3bc81150c34e Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 11:59:29 +0100 Subject: [PATCH 09/13] Render text properly, render arrow + freetext. Need to move textboxes appropriately. --- .../dicom-sr/src/getSopClassHandlerModule.js | 73 +++----- .../dicom-sr/src/tools/DICOMSRDisplayTool.js | 168 ++++++++++++++++-- .../dicom-sr/src/utils/addMeasurement.js | 5 - yarn.lock | 21 +++ 4 files changed, 200 insertions(+), 67 deletions(-) diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index ec91a1bc5..51ebd5963 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -26,12 +26,16 @@ const CodeNameCodeSequenceValues = { ImageLibraryGroup: '126200', TrackingUniqueIdentifier: '112040', TrackingIdentifier: '112039', + Finding: '121071', }; const RELATIONSHIP_TYPE = { INFERRED_FROM: 'INFERRED FROM', }; +const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4'; +const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; + /** * Basic SOPClassHandler: * - For all Image types that are stackable, create @@ -68,8 +72,9 @@ function _getDisplaySetsFromSeries( const { ConceptNameCodeSequence, ContentSequence } = instance; if ( + !ConceptNameCodeSequence || ConceptNameCodeSequence.CodeValue !== - CodeNameCodeSequenceValues.ImagingMeasurementReport + CodeNameCodeSequenceValues.ImagingMeasurementReport ) { console.warn( 'Only support Imaging Measurement Report SRs (TID1500) for now' @@ -134,49 +139,6 @@ function _getDisplaySetsFromSeries( return [displaySet]; } -function _isRehydratable(displaySet, MeasurementService) { - const mappings = MeasurementService.getSourceMappings( - 'CornerstoneTools', - '4' - ); - - if (!mappings || !mappings.length) { - return false; - } - - const mappingDefinitions = mappings.map(m => m.definition); - const { measurements } = displaySet; - - const adapterKeys = Object.keys(cornerstoneAdapters).filter( - adapterKey => - typeof cornerstoneAdapters[adapterKey] - .isValidCornerstoneTrackingIdentifier === 'function' - ); - - const adapters = []; - - adapterKeys.forEach(key => { - if (mappingDefinitions.includes(key)) { - // Must have both a dcmjs adapter and a MeasurementService - // Definition in order to be a candidate for import. - adapters.push(cornerstoneAdapters[key]); - } - }); - - for (let i = 0; i < measurements.length; i++) { - const TrackingIdentifier = measurements[i].TrackingIdentifier; - const hydratable = adapters.some(adapter => - adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier) - ); - - if (hydratable) { - return true; - } - } - - return false; -} - function _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, newDisplaySet, @@ -459,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { CodeNameCodeSequenceValues.TrackingIdentifier ); + const Findings = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.Finding + ); + const measurement = { loaded: false, labels: [], @@ -467,6 +435,23 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; + if (Findings.length) { + // TODO -> Pull in labels when we have them, just free text for now. + const cornerstoneFreeTextFinding = Findings.find( + Finding => + Finding.ConceptCodeSequence.CodingSchemeDesignator === + CORNERSTONE_CODING_SCHEME_DESIGNATOR && + Finding.ConceptCodeSequence.CodeValue === + CORNERSTONE_FREETEXT_CODE_VALUE + ); + if (cornerstoneFreeTextFinding) { + measurement.labels.push({ + label: CORNERSTONE_FREETEXT_CODE_VALUE, + value: cornerstoneFreeTextFinding.ConceptCodeSequence.CodeMeaning, + }); + } + } + NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index f744c5c91..f78c053dc 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -1,4 +1,5 @@ import { importInternal, getToolState, toolColors } from 'cornerstone-tools'; +import { pixelToCanvas } from 'cornerstone-core'; import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; @@ -10,6 +11,7 @@ const drawJoinedLines = importInternal('drawing/drawJoinedLines'); const drawCircle = importInternal('drawing/drawCircle'); const drawEllipse = importInternal('drawing/drawEllipse'); const drawHandles = importInternal('drawing/drawHandles'); +const drawArrow = importInternal('drawing/drawArrow'); const getNewContext = importInternal('drawing/getNewContext'); const BaseTool = importInternal('base/BaseTool'); const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox'); @@ -60,6 +62,12 @@ export default class DICOMSRDisplayTool extends BaseTool { trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier) ); + // First: Render annotations with textboxes moved to closest side + // TODO: Render all annotations and get their locations. + // TODO: Find suitable places for textboxes that aren't covered. + + let shouldRepositionTextBoxes = false; + for (let i = 0; i < filteredToolData.length; i++) { const data = filteredToolData[i]; const { renderableData, labels } = data; @@ -80,8 +88,10 @@ export default class DICOMSRDisplayTool extends BaseTool { switch (GraphicType) { case SCOORD_TYPES.POINT: + this.renderPoint(renderableDataForGraphicType, eventData, options); + break; case SCOORD_TYPES.MULTIPOINT: - this.renderPointOrMultipoint( + this.renderMultipoint( renderableDataForGraphicType, eventData, options @@ -110,36 +120,53 @@ export default class DICOMSRDisplayTool extends BaseTool { const { element } = eventData; const context = getNewContext(eventData.canvasContext.canvas); - debugger; + if (!data.handles || !data.handles.textBox) { + const textBox = { + active: false, + hasMoved: true, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true, + }; + + const anchorPoints = _getTextBoxAnchorPointsForRenderableData( + renderableData, + eventData + ); + textBox.anchorPoints = anchorPoints; + + const bottomRight = { + x: Math.max(...anchorPoints.map(point => point.x)), + y: Math.max(...anchorPoints.map(point => point.y)), + }; + + textBox.x = bottomRight.x; + textBox.y = bottomRight.y; + + data.handles = {}; + data.handles.textBox = textBox; + + shouldRepositionTextBoxes = true; + } const text = _getTextBoxLinesFromLabels(labels); - debugger; - function textBoxAnchorPoints() { - return [{ x: 1000, y: 1000 }]; + return data.handles.textBox.anchorPoints; } draw(context, context => { drawLinkedTextBox( context, element, - { - active: false, - hasMoved: false, - movesIndependently: false, - drawnIndependently: true, - allowedOutsideImage: true, - hasBoundingBox: true, - x: 256, - y: 256, - }, + data.handles.textBox, text, - null, + data.handles, textBoxAnchorPoints, color, lineWidth, - 10, + 0, true ); }); @@ -157,7 +184,7 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } - renderPointOrMultipoint(renderableData, eventData, options) { + renderMultipoint(renderableData, eventData, options) { const context = getNewContext(eventData.canvasContext.canvas); renderableData.forEach(points => { @@ -167,6 +194,46 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } + renderPoint(renderableData, eventData, options) { + // Render single point as an arrow. + debugger; + const { element, image } = eventData; + const { rows, columns } = image; + const context = getNewContext(eventData.canvasContext.canvas); + + const { color, lineWidth } = options; + + // Find a suitable length for the image size. + + const xOffset = columns / 10; + const yOffset = rows / 10; + + debugger; + + renderableData.forEach(points => { + const point = points[0]; // The SCOORD type is POINT so the array length is 1. + draw(context, context => { + // Draw the arrow + const handleStartCanvas = pixelToCanvas(element, point); + const handleEndCanvas = pixelToCanvas(element, { + x: point.x + xOffset, + y: point.y + yOffset, + }); + + debugger; + + drawArrow( + context, + handleEndCanvas, + handleStartCanvas, + color, + lineWidth, + false + ); + }); + }); + } + renderCircle(renderableData, eventData, options) { const { element } = eventData; @@ -220,6 +287,7 @@ const SHORT_HAND_MAP = { 'Long Axis': 'L ', AREA: 'Area ', Length: '', + CORNERSTONEFREETEXT: '', }; function _labelToShorthand(label) { @@ -231,3 +299,67 @@ function _labelToShorthand(label) { return label; } + +function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) { + let anchorPoints = []; + + Object.keys(renderableData).forEach(GraphicType => { + const renderableDataForGraphicType = renderableData[GraphicType]; + + switch (GraphicType) { + case SCOORD_TYPES.POINT: + renderableDataForGraphicType.forEach(points => { + anchorPoints = [...anchorPoints, ...points]; + + // Add other arrow point based on image size. + const { image } = eventData; + const { rows, columns } = image; + + const xOffset = columns / 10; + const yOffset = rows / 10; + const point = points[0]; + + anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset }); + }); + + break; + case SCOORD_TYPES.MULTIPOINT: + case SCOORD_TYPES.POLYLINE: + renderableDataForGraphicType.forEach(points => { + anchorPoints = [...anchorPoints, ...points]; + }); + break; + case SCOORD_TYPES.CIRCLE: + renderableDataForGraphicType.forEach(circle => { + const { center, radius } = circle; + + anchorPoints.push({ x: center.x + radius, y: center.y }); + anchorPoints.push({ x: center.x - radius, y: center.y }); + anchorPoints.push({ x: center.x, y: center.y + radius }); + anchorPoints.push({ x: center.x, y: center.y - radius }); + }); + + break; + case SCOORD_TYPES.ELLIPSE: + renderableDataForGraphicType.forEach(ellipse => { + const { corner1, corner2 } = ellipse; + + const halfWidth = Math.abs(corner1.x - corner2.x) / 2; + const halfHeight = Math.abs(corner1.y - corner2.y) / 2; + + const center = { + x: (corner1.x + corner2.x) / 2, + y: (corner1.y + corner2.y) / 2, + }; + + anchorPoints.push({ x: center.x + halfWidth, y: center.y }); + anchorPoints.push({ x: center.x - halfWidth, y: center.y }); + anchorPoints.push({ x: center.x, y: center.y + halfHeight }); + anchorPoints.push({ x: center.x, y: center.y - halfHeight }); + }); + break; + } + }); + + return anchorPoints; +} diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index dc2211244..b7ea21440 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -14,11 +14,6 @@ export default function addMeasurement( imageId, displaySetInstanceUID ) { - console.log('== ADD MEASUREMENT TO CST =='); - console.log(measurement); - console.log(imageId); - console.log('============================'); - // TODO -> Render rotated ellipse . const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; diff --git a/yarn.lock b/yarn.lock index ee59610f2..46de652dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,6 +2679,17 @@ dependencies: "@types/node" ">= 8" +"@ohif/extension-cornerstone@^2.4.0": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.8.2.tgz#b8d95610b91eb43c05f84adcdd699183918a57ff" + integrity sha512-fIkPPQdDCkoVHCYTUFB4UhmdL9VJRyYawa6FxXiKXjj9OcUsrkXj+BORYgzcEkLWTLmrB+66qtIqpQ7FyXlzJQ== + dependencies: + "@babel/runtime" "^7.5.5" + classnames "^2.2.6" + lodash.merge "^4.6.2" + lodash.throttle "^4.1.1" + react-cornerstone-viewport "2.3.9" + "@ohif/extension-lesion-tracker@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f" @@ -17889,6 +17900,16 @@ 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 7092fc858eca1c3f82ad9f99558080e50e2080d2 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 14:32:54 +0100 Subject: [PATCH 10/13] Don't reposition textboxes for now. --- .../dicom-sr/src/getSopClassHandlerModule.js | 7 ++- .../dicom-sr/src/tools/DICOMSRDisplayTool.js | 51 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index 51ebd5963..cd1467510 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -523,7 +523,12 @@ function _getLabelFromMeasuredValueSequence( const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; - return { label: CodeMeaning, value: `${NumericValue} ${CodeValue}` }; // E.g. Long Axis: 31.0 mm + const formatedNumericValue = NumericValue ? NumericValue.toFixed(2) : ''; + + return { + label: CodeMeaning, + value: `${formatedNumericValue} ${CodeValue}`, + }; // E.g. Long Axis: 31.0 mm } function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index f78c053dc..3400f0d21 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -62,10 +62,6 @@ export default class DICOMSRDisplayTool extends BaseTool { trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier) ); - // First: Render annotations with textboxes moved to closest side - // TODO: Render all annotations and get their locations. - // TODO: Find suitable places for textboxes that aren't covered. - let shouldRepositionTextBoxes = false; for (let i = 0; i < filteredToolData.length; i++) { @@ -171,6 +167,30 @@ export default class DICOMSRDisplayTool extends BaseTool { ); }); } + + // TOOD -> text boxes may overlap with other annotations at the moment. + // To be fixed after we get requirements. + // if (shouldRepositionTextBoxes) { + // this.repositionTextBox(filteredToolData, eventData); + // } + } + + repositionTextBox(toolData, eventData) { + debugger; + + const toolBoundingBoxes = []; + + for (let i = 0; i < toolData.length; i++) { + const toolDataI = toolData[i]; + + const { textBox } = toolDataI.handles; + const { anchorPoints } = textBox; + + const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints); + // Get the textbox bounding locations. + // Get the tool extents. + debugger; + } } renderPolyLine(renderableData, eventData, options) { @@ -363,3 +383,26 @@ function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) { return anchorPoints; } + +function _getBoundingBoxFromAnchorPoints(anchorPoints) { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + anchorPoints.forEach(point => { + const { x, y } = point; + + if (x > maxX) { + maxX = x; + } else if (x < minX) { + minX = x; + } + + if (y > maxX) { + maxY = y; + } else if (y < minY) { + minY = y; + } + }); +} From 413f5c9003725544722cafe129b197d3240ca6ad Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 14:35:29 +0100 Subject: [PATCH 11/13] Remove debugger statements. --- .../dicom-sr/src/tools/DICOMSRDisplayTool.js | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index 3400f0d21..4efca058e 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -175,23 +175,20 @@ export default class DICOMSRDisplayTool extends BaseTool { // } } - repositionTextBox(toolData, eventData) { - debugger; + // repositionTextBox(toolData, eventData) { + // const toolBoundingBoxes = []; - const toolBoundingBoxes = []; + // for (let i = 0; i < toolData.length; i++) { + // const toolDataI = toolData[i]; - for (let i = 0; i < toolData.length; i++) { - const toolDataI = toolData[i]; + // const { textBox } = toolDataI.handles; + // const { anchorPoints } = textBox; - const { textBox } = toolDataI.handles; - const { anchorPoints } = textBox; - - const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints); - // Get the textbox bounding locations. - // Get the tool extents. - debugger; - } - } + // const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints); + // // Get the textbox bounding locations. + // // Get the tool extents. + // } + // } renderPolyLine(renderableData, eventData, options) { const { element } = eventData; @@ -216,7 +213,6 @@ export default class DICOMSRDisplayTool extends BaseTool { renderPoint(renderableData, eventData, options) { // Render single point as an arrow. - debugger; const { element, image } = eventData; const { rows, columns } = image; const context = getNewContext(eventData.canvasContext.canvas); @@ -228,8 +224,6 @@ export default class DICOMSRDisplayTool extends BaseTool { const xOffset = columns / 10; const yOffset = rows / 10; - debugger; - renderableData.forEach(points => { const point = points[0]; // The SCOORD type is POINT so the array length is 1. draw(context, context => { @@ -240,8 +234,6 @@ export default class DICOMSRDisplayTool extends BaseTool { y: point.y + yOffset, }); - debugger; - drawArrow( context, handleEndCanvas, From 7c3c717d33dcc84496ea018f197ba8f9999a4468 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 14:56:25 +0100 Subject: [PATCH 12/13] Update dcmjs --- 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 | 2 +- extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- platform/viewer/package.json | 2 +- yarn.lock | 8 ++++---- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extensions/dicom-html/package.json b/extensions/dicom-html/package.json index eca2b16a3..a56b3fd5f 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.14.0", + "dcmjs": "0.14.1", "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 323ea25d8..30cc11a22 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.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "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 9306becf7..b09a4d289 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.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "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 731b7d39c..99e378bd9 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -35,7 +35,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 1ed3dbb5b..6b39b6eae 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -29,7 +29,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-tools": "4.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 3e33aa678..d5e309444 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.14.0", + "dcmjs": "0.14.1", "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 3a16b5b95..239876138 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.14.0", + "dcmjs": "0.14.1", "dicomweb-client": "^0.6.0", "immer": "6.0.2", "isomorphic-base64": "^1.0.2", diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 629e5e304..c79f11e3a 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -69,7 +69,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicom-parser": "^1.8.3", "dicomweb-client": "^0.4.4", "dotenv-webpack": "^1.7.0", diff --git a/yarn.lock b/yarn.lock index 46de652dc..19aeaf498 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7364,10 +7364,10 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -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== +dcmjs@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.1.tgz#4a4f18d1e5332d38429e2d77340a6ad0052ec71e" + integrity sha512-GGGQt4zjX7Fp0rLO5rcIQgmxZdLsEwBSZ1T74iXeJG5W32P1yiodJfBW900tNZCKj3zWamO3f5jDC1Xkv81fdg== dependencies: "@babel/polyfill" "^7.8.3" "@babel/runtime" "^7.8.4" From 44c292796376fbe6d39d68ffcd56e57c9cf7675f Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Wed, 1 Jul 2020 15:12:49 +0100 Subject: [PATCH 13/13] Fix export --- extensions/dicom-sr/src/getSopClassHandlerModule.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index cd1467510..e0d79765b 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -523,7 +523,11 @@ function _getLabelFromMeasuredValueSequence( const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; - const formatedNumericValue = NumericValue ? NumericValue.toFixed(2) : ''; + debugger; + + const formatedNumericValue = NumericValue + ? Number(NumericValue).toFixed(2) + : ''; return { label: CodeMeaning,