From 14005d61f3ee9111e2b3d145fd5b0f505e614f45 Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Mon, 29 Jun 2020 18:22:19 +0100 Subject: [PATCH] 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} + +
-
- )} + ) + } >