OHIF-190 - Rehydrate PR (#1835)
* WIP rehydration. * WIP OHIF-190 rehydrate. * Remove debugger statements. * Merge in and fix after OHIF-197 and OHIF-198 * Remove console.log * Fix typo * fix length tracking bug * Respond to reviewer comments.
This commit is contained in:
parent
25de9046b3
commit
bc2bde890b
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.7.3",
|
"version": "3.0.0",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -246,10 +246,12 @@ const _connectToolsToMeasurementService = measurementService => {
|
|||||||
|
|
||||||
function addMeasurement(csToolsEvent) {
|
function addMeasurement(csToolsEvent) {
|
||||||
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
|
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const evtDetail = csToolsEvent.detail;
|
const evtDetail = csToolsEvent.detail;
|
||||||
const { toolName, toolType, measurementData } = evtDetail;
|
const { toolName, toolType, measurementData } = evtDetail;
|
||||||
const csToolName = toolName || measurementData.toolType || toolType;
|
const csToolName = toolName || measurementData.toolType || toolType;
|
||||||
|
|
||||||
const measurementId = addOrUpdate(csToolName, evtDetail);
|
const measurementId = addOrUpdate(csToolName, evtDetail);
|
||||||
|
|
||||||
if (measurementId) {
|
if (measurementId) {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const ArrowAnnotate = {
|
|||||||
points.push(measurementData.handles);
|
points.push(measurementData.handles);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData._measurementServiceId,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const Bidirectional = {
|
|||||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData._measurementServiceId,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
|||||||
@ -53,7 +53,7 @@ const EllipticalRoi = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData._measurementServiceId,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const Length = {
|
|||||||
text: label,
|
text: label,
|
||||||
description,
|
description,
|
||||||
handles: getHandlesFromPoints(points),
|
handles: getHandlesFromPoints(points),
|
||||||
_measurementServiceId: id,
|
id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -57,11 +57,8 @@ const Length = {
|
|||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
} = getSOPInstanceAttributes(element);
|
} = getSOPInstanceAttributes(element);
|
||||||
|
|
||||||
const points = [];
|
|
||||||
points.push(measurementData.handles);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData._measurementServiceId,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
|||||||
@ -1,40 +1,63 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
|
import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
|
||||||
import TOOL_NAMES from './constants/toolNames';
|
import TOOL_NAMES from './constants/toolNames';
|
||||||
|
import { adapters } from 'dcmjs';
|
||||||
|
import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
|
||||||
import id from './id';
|
import id from './id';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
// const cine = viewportSpecificData.cine;
|
const { StackManager, guid } = OHIF.utils;
|
||||||
|
|
||||||
// isPlaying = cine.isPlaying === true;
|
const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking';
|
||||||
// frameRate = cine.cineFrameRate || frameRate;
|
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
|
||||||
|
|
||||||
function OHIFCornerstoneSRViewport({
|
function OHIFCornerstoneSRViewport({
|
||||||
children,
|
children,
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySet,
|
displaySet,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
DisplaySetService,
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
}) {
|
}) {
|
||||||
|
const { DisplaySetService, MeasurementService } = servicesManager.services;
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||||
const [measurementCount, setMeasurementCount] = useState(1);
|
const [measurementCount, setMeasurementCount] = useState(1);
|
||||||
const [viewportData, setViewportData] = useState(null);
|
const [viewportData, setViewportData] = useState(null);
|
||||||
const [activeDisplaySetData, setActiveDisplaySetData] = useState({});
|
const [activeDisplaySetData, setActiveDisplaySetData] = useState({});
|
||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
|
const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated);
|
||||||
const { viewports, activeViewportIndex } = viewportGrid;
|
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 onElementEnabled = evt => {
|
||||||
const eventData = evt.detail;
|
const eventData = evt.detail;
|
||||||
const targetElement = eventData.element;
|
const targetElement = eventData.element;
|
||||||
@ -196,8 +219,6 @@ function OHIFCornerstoneSRViewport({
|
|||||||
|
|
||||||
const { Modality } = displaySet;
|
const { Modality } = displaySet;
|
||||||
|
|
||||||
// TODO -> Get this from the associated stack.
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PatientID,
|
PatientID,
|
||||||
PatientName,
|
PatientName,
|
||||||
@ -210,6 +231,7 @@ function OHIFCornerstoneSRViewport({
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
PixelSpacing,
|
PixelSpacing,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
displaySetInstanceUID,
|
||||||
} = activeDisplaySetData;
|
} = activeDisplaySetData;
|
||||||
|
|
||||||
const onMeasurementChange = direction => {
|
const onMeasurementChange = direction => {
|
||||||
@ -229,22 +251,158 @@ function OHIFCornerstoneSRViewport({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newMeasurementSelected === measurementSelected) {
|
|
||||||
// TODO -> Jump to image in this case.
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViewport(newMeasurementSelected);
|
updateViewport(newMeasurementSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hydrateMeasurementService() {
|
||||||
|
// TODO -> We should define a strict versioning somewhere.
|
||||||
|
const mappings = MeasurementService.getSourceMappings(
|
||||||
|
'CornerstoneTools',
|
||||||
|
'4'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mappings || !mappings.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = DicomMetadataStore.getInstance(
|
||||||
|
displaySet.StudyInstanceUID,
|
||||||
|
displaySet.SeriesInstanceUID,
|
||||||
|
displaySet.SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const { MeasurementReport } = adapters.Cornerstone;
|
||||||
|
|
||||||
|
const sopInstanceUIDToImageId = {};
|
||||||
|
|
||||||
|
displaySet.measurements.forEach(measurement => {
|
||||||
|
const { ReferencedSOPInstanceUID, imageId } = measurement;
|
||||||
|
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
|
||||||
|
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use dcmjs to generate toolState.
|
||||||
|
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
||||||
|
instance
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter what is found by DICOM SR to measurements we support.
|
||||||
|
const mappingDefinitions = mappings.map(m => m.definition);
|
||||||
|
const hydratableMeasurementsInSR = {};
|
||||||
|
|
||||||
|
Object.keys(storedMeasurementByToolType).forEach(key => {
|
||||||
|
if (mappingDefinitions.includes(key)) {
|
||||||
|
hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
extensionManager.registeredExtensionIds.includes(
|
||||||
|
MEASUREMENT_TRACKING_EXTENSION_ID
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Set the series touched as tracked.
|
||||||
|
const imageIds = [];
|
||||||
|
|
||||||
|
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
|
||||||
|
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
|
||||||
|
|
||||||
|
toolDataForToolType.forEach(data => {
|
||||||
|
// Add the measurement to toolState
|
||||||
|
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
|
||||||
|
|
||||||
|
if (!imageIds.includes(imageId)) {
|
||||||
|
imageIds.push(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let targetStudyInstanceUID;
|
||||||
|
const SeriesInstanceUIDs = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < imageIds.length; i++) {
|
||||||
|
const imageId = imageIds[0];
|
||||||
|
const {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = cornerstone.metaData.get('instance', imageId);
|
||||||
|
|
||||||
|
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
||||||
|
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetStudyInstanceUID) {
|
||||||
|
targetStudyInstanceUID = StudyInstanceUID;
|
||||||
|
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
||||||
|
console.warn(
|
||||||
|
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', {
|
||||||
|
StudyInstanceUID: targetStudyInstanceUID,
|
||||||
|
SeriesInstanceUIDs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
|
||||||
|
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
|
||||||
|
|
||||||
|
toolDataForToolType.forEach(data => {
|
||||||
|
// Add the measurement to toolState
|
||||||
|
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
|
||||||
|
|
||||||
|
data.id = guid();
|
||||||
|
|
||||||
|
_addToolDataToCornerstoneTools(data, toolType, imageId);
|
||||||
|
|
||||||
|
// Let the measurement service know we added to toolState
|
||||||
|
const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
|
||||||
|
toolType,
|
||||||
|
MeasurementService,
|
||||||
|
imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
||||||
|
|
||||||
|
MeasurementService.addRawMeasurement(
|
||||||
|
source,
|
||||||
|
toolType,
|
||||||
|
data,
|
||||||
|
toMeasurementSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!imageIds.includes(imageId)) {
|
||||||
|
imageIds.push(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySet.isHydrated = true;
|
||||||
|
|
||||||
|
setIsHydrated(true);
|
||||||
|
|
||||||
|
viewportGridService.setDisplaysetForViewport({
|
||||||
|
viewportIndex: activeViewportIndex,
|
||||||
|
displaySetInstanceUID: activeDisplaySetData.displaySetInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewportActionBar
|
<ViewportActionBar
|
||||||
onSeriesChange={onMeasurementChange}
|
onSeriesChange={onMeasurementChange}
|
||||||
|
onHydrationClick={hydrateMeasurementService}
|
||||||
showNavArrows={viewportIndex === activeViewportIndex}
|
showNavArrows={viewportIndex === activeViewportIndex}
|
||||||
studyData={{
|
studyData={{
|
||||||
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
||||||
isTracked: false,
|
isTracked: false,
|
||||||
isLocked: false,
|
isLocked: displaySet.isLocked,
|
||||||
|
isHydrated,
|
||||||
studyDate: formatDate(StudyDate),
|
studyDate: formatDate(StudyDate),
|
||||||
currentSeries: SeriesNumber,
|
currentSeries: SeriesNumber,
|
||||||
seriesDescription: SeriesDescription,
|
seriesDescription: SeriesDescription,
|
||||||
@ -381,9 +539,31 @@ async function _getViewportAndActiveDisplaySetData(
|
|||||||
SeriesDescription: image0.SeriesDescription,
|
SeriesDescription: image0.SeriesDescription,
|
||||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||||
SeriesNumber: image0.SeriesNumber,
|
SeriesNumber: image0.SeriesNumber,
|
||||||
|
displaySetInstanceUID,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { viewportData, activeDisplaySetData };
|
return { viewportData, activeDisplaySetData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _addToolDataToCornerstoneTools(data, toolType, imageId) {
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
if (toolState[imageId] === undefined) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState[toolType] === undefined) {
|
||||||
|
imageIdToolState[toolType] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdToolState[toolType];
|
||||||
|
|
||||||
|
toolData.data.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
export default OHIFCornerstoneSRViewport;
|
export default OHIFCornerstoneSRViewport;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import id from './id';
|
import id from './id';
|
||||||
import { utils, classes } from '@ohif/core';
|
import { utils, classes } from '@ohif/core';
|
||||||
import addMeasurement from './utils/addMeasurement.js';
|
import addMeasurement from './utils/addMeasurement';
|
||||||
|
import isRehydratable from './utils/isRehydratable';
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
const { ImageSet } = classes;
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ const CodeNameCodeSequenceValues = {
|
|||||||
MeasurementGroup: '125007',
|
MeasurementGroup: '125007',
|
||||||
ImageLibraryGroup: '126200',
|
ImageLibraryGroup: '126200',
|
||||||
TrackingUniqueIdentifier: '112040',
|
TrackingUniqueIdentifier: '112040',
|
||||||
|
TrackingIdentifier: '112039',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_TYPE = {
|
const RELATIONSHIP_TYPE = {
|
||||||
@ -49,7 +51,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
throw new Error('No instances were provided');
|
throw new Error('No instances were provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { DisplaySetService } = servicesManager.services;
|
const { DisplaySetService, MeasurementService } = servicesManager.services;
|
||||||
const dataSources = extensionManager.getDataSources();
|
const dataSources = extensionManager.getDataSources();
|
||||||
const dataSource = dataSources[0];
|
const dataSource = dataSources[0];
|
||||||
|
|
||||||
@ -91,6 +93,18 @@ function _getDisplaySetsFromSeries(
|
|||||||
sopClassUids,
|
sopClassUids,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mappings = MeasurementService.getSourceMappings(
|
||||||
|
'CornerstoneTools',
|
||||||
|
'4'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRehydratable(displaySet, mappings)) {
|
||||||
|
displaySet.isLocked = false;
|
||||||
|
displaySet.isHydrated = false;
|
||||||
|
} else {
|
||||||
|
displaySet.isLocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check currently added displaySets and add measurements if the sources exist.
|
// Check currently added displaySets and add measurements if the sources exist.
|
||||||
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||||
_checkIfCanAddMeasurementsToDisplaySet(
|
_checkIfCanAddMeasurementsToDisplaySet(
|
||||||
@ -346,6 +360,12 @@ function _processTID1410Measurement(mergedContentSequence) {
|
|||||||
group => group.ValueType === 'UIDREF'
|
group => group.ValueType === 'UIDREF'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
if (!graphicItem) {
|
if (!graphicItem) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
||||||
@ -362,6 +382,7 @@ function _processTID1410Measurement(mergedContentSequence) {
|
|||||||
labels: [],
|
labels: [],
|
||||||
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
||||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||||
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
NUMContentItems.forEach(item => {
|
NUMContentItems.forEach(item => {
|
||||||
@ -389,11 +410,18 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
group => group.ValueType === 'UIDREF'
|
group => group.ValueType === 'UIDREF'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
const measurement = {
|
const measurement = {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
labels: [],
|
labels: [],
|
||||||
coords: [],
|
coords: [],
|
||||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||||
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
NUMContentItems.forEach(item => {
|
NUMContentItems.forEach(item => {
|
||||||
|
|||||||
@ -23,6 +23,18 @@ export default {
|
|||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
|
dependencies: [
|
||||||
|
// TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future.
|
||||||
|
// OHIF-229
|
||||||
|
{
|
||||||
|
id: 'org.ohif.cornerstone',
|
||||||
|
version: '3.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'org.ohif.measurement-tracking',
|
||||||
|
version: '^0.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
@ -34,13 +46,12 @@ export default {
|
|||||||
* @param {object} [configuration={}]
|
* @param {object} [configuration={}]
|
||||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
*/
|
*/
|
||||||
getViewportModule({ servicesManager }) {
|
getViewportModule({ servicesManager, extensionManager }) {
|
||||||
const ExtendedOHIFCornerstoneSRViewport = props => {
|
const ExtendedOHIFCornerstoneSRViewport = props => {
|
||||||
const { DisplaySetService } = servicesManager.services;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneSRViewport
|
<OHIFCornerstoneSRViewport
|
||||||
DisplaySetService={DisplaySetService}
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -62,6 +62,12 @@ export default function addMeasurement(
|
|||||||
measurement.loaded = true;
|
measurement.loaded = true;
|
||||||
measurement.imageId = imageId;
|
measurement.imageId = imageId;
|
||||||
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
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;
|
delete measurement.coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,202 @@
|
|||||||
|
export default function getToolStateToCornerstoneMeasurementSchema(
|
||||||
|
toolType,
|
||||||
|
MeasurementService,
|
||||||
|
imageId
|
||||||
|
) {
|
||||||
|
const _getValueTypeFromToolType = toolType => {
|
||||||
|
const {
|
||||||
|
POLYLINE,
|
||||||
|
ELLIPSE,
|
||||||
|
POINT,
|
||||||
|
BIDIRECTIONAL,
|
||||||
|
} = MeasurementService.VALUE_TYPES;
|
||||||
|
|
||||||
|
// TODO -> I get why this was attemped, but its not nearly flexible enough.
|
||||||
|
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
||||||
|
// You can't define a bidirectional tool as a single type..
|
||||||
|
// OHIF-230
|
||||||
|
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||||
|
Length: POLYLINE,
|
||||||
|
EllipticalRoi: ELLIPSE,
|
||||||
|
Bidirectional: BIDIRECTIONAL,
|
||||||
|
ArrowAnnotate: POINT,
|
||||||
|
};
|
||||||
|
|
||||||
|
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (toolType) {
|
||||||
|
case 'Length':
|
||||||
|
return measurementData =>
|
||||||
|
Length(measurementData, imageId, _getValueTypeFromToolType);
|
||||||
|
case 'Bidirectional':
|
||||||
|
return measurementData =>
|
||||||
|
Bidirectional(measurementData, imageId, _getValueTypeFromToolType);
|
||||||
|
case 'EllipticalRoi':
|
||||||
|
return measurementData =>
|
||||||
|
EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType);
|
||||||
|
case 'ArrowAnnotate':
|
||||||
|
return measurementData =>
|
||||||
|
ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Length(measurementData, imageId, _getValueTypeFromToolType) {
|
||||||
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
|
const instance = cornerstone.metaData.get('instance', imageId);
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const { handles } = measurementData;
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
Object.keys(handles).map(handle => {
|
||||||
|
if (['start', 'end'].includes(handle)) {
|
||||||
|
let point = {};
|
||||||
|
if (handles[handle].x) point.x = handles[handle].x;
|
||||||
|
if (handles[handle].y) point.y = handles[handle].y;
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData.id,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
length: measurementData.length,
|
||||||
|
type: _getValueTypeFromToolType(tool),
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) {
|
||||||
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
|
const instance = cornerstone.metaData.get('instance', imageId);
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const { handles } = measurementData;
|
||||||
|
|
||||||
|
const longAxis = [handles.start, handles.end];
|
||||||
|
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData.id,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
shortestDiameter: measurementData.shortestDiameter,
|
||||||
|
longestDiameter: measurementData.longestDiameter,
|
||||||
|
type: _getValueTypeFromToolType(tool),
|
||||||
|
points: { longAxis, shortAxis },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) {
|
||||||
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
|
const instance = cornerstone.metaData.get('instance', imageId);
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const { start, end } = measurementData.handles;
|
||||||
|
|
||||||
|
const halfXLength = Math.abs(start.x - end.x) / 2;
|
||||||
|
const halfYLength = Math.abs(start.y - end.y) / 2;
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||||
|
|
||||||
|
// To store similar to SR.
|
||||||
|
if (halfXLength > halfYLength) {
|
||||||
|
// X-axis major
|
||||||
|
// Major axis
|
||||||
|
points.push({ x: center.x - halfXLength, y: center.y });
|
||||||
|
points.push({ x: center.x + halfXLength, y: center.y });
|
||||||
|
// Minor axis
|
||||||
|
points.push({ x: center.x, y: center.y - halfYLength });
|
||||||
|
points.push({ x: center.x, y: center.y + halfYLength });
|
||||||
|
} else {
|
||||||
|
// Y-axis major
|
||||||
|
// Major axis
|
||||||
|
points.push({ x: center.x, y: center.y - halfYLength });
|
||||||
|
points.push({ x: center.x, y: center.y + halfYLength });
|
||||||
|
// Minor axis
|
||||||
|
points.push({ x: center.x - halfXLength, y: center.y });
|
||||||
|
points.push({ x: center.x + halfXLength, y: center.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData.id,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
area:
|
||||||
|
measurementData.cachedStats &&
|
||||||
|
measurementData.cachedStats
|
||||||
|
.area /* TODO: Add concept names instead (descriptor) */,
|
||||||
|
type: _getValueTypeFromToolType(tool),
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType) {
|
||||||
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
|
const instance = cornerstone.metaData.get('instance', imageId);
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const { handles } = measurementData;
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
Object.keys(handles).map(handle => {
|
||||||
|
if (['start', 'end'].includes(handle)) {
|
||||||
|
let point = {};
|
||||||
|
if (handles[handle].x) point.x = handles[handle].x;
|
||||||
|
if (handles[handle].y) point.y = handles[handle].y;
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData.id,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
text: measurementData.text,
|
||||||
|
type: _getValueTypeFromToolType(tool),
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
}
|
||||||
48
extensions/dicom-sr/src/utils/isRehydratable.js
Normal file
48
extensions/dicom-sr/src/utils/isRehydratable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { adapters } from 'dcmjs';
|
||||||
|
|
||||||
|
const cornerstoneAdapters = adapters.Cornerstone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given `displySet`can be rehydrated into the `MeasurementService`.
|
||||||
|
*
|
||||||
|
* @param {object} displaySet The SR `displaySet` to check.
|
||||||
|
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`.
|
||||||
|
* @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`.
|
||||||
|
*/
|
||||||
|
export default function isRehydratable(displaySet, mappings) {
|
||||||
|
if (!mappings || !mappings.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingDefinitions = mappings.map(m => m.definition);
|
||||||
|
const { measurements } = displaySet;
|
||||||
|
|
||||||
|
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||||
|
adapterKey =>
|
||||||
|
typeof cornerstoneAdapters[adapterKey]
|
||||||
|
.isValidCornerstoneTrackingIdentifier === 'function'
|
||||||
|
);
|
||||||
|
|
||||||
|
const adapters = [];
|
||||||
|
|
||||||
|
adapterKeys.forEach(key => {
|
||||||
|
if (mappingDefinitions.includes(key)) {
|
||||||
|
// Must have both a dcmjs adapter and a MeasurementService
|
||||||
|
// Definition in order to be a candidate for import.
|
||||||
|
adapters.push(cornerstoneAdapters[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < measurements.length; i++) {
|
||||||
|
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
||||||
|
const hydratable = adapters.some(adapter =>
|
||||||
|
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hydratable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -23,6 +23,12 @@ const machineConfiguration = {
|
|||||||
entry: 'clearContext',
|
entry: 'clearContext',
|
||||||
on: {
|
on: {
|
||||||
TRACK_SERIES: 'promptBeginTracking',
|
TRACK_SERIES: 'promptBeginTracking',
|
||||||
|
SET_TRACKED_SERIES: [
|
||||||
|
{
|
||||||
|
target: 'tracking',
|
||||||
|
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
promptBeginTracking: {
|
promptBeginTracking: {
|
||||||
@ -69,6 +75,12 @@ const machineConfiguration = {
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
SET_TRACKED_SERIES: [
|
||||||
|
{
|
||||||
|
target: 'tracking',
|
||||||
|
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
promptTrackNewSeries: {
|
promptTrackNewSeries: {
|
||||||
@ -139,6 +151,10 @@ const defaultOptions = {
|
|||||||
trackedStudy: evt.data.StudyInstanceUID,
|
trackedStudy: evt.data.StudyInstanceUID,
|
||||||
trackedSeries: [evt.data.SeriesInstanceUID],
|
trackedSeries: [evt.data.SeriesInstanceUID],
|
||||||
})),
|
})),
|
||||||
|
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({
|
||||||
|
trackedStudy: evt.StudyInstanceUID,
|
||||||
|
trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs],
|
||||||
|
})),
|
||||||
addTrackedSeries: assign((ctx, evt) => ({
|
addTrackedSeries: assign((ctx, evt) => ({
|
||||||
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -17,6 +17,10 @@ import ViewportOverlay from './ViewportOverlay';
|
|||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
// TODO -> Get this list from the list of tracked measurements.
|
// 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 {
|
const {
|
||||||
ArrowAnnotateTool,
|
ArrowAnnotateTool,
|
||||||
BidirectionalTool,
|
BidirectionalTool,
|
||||||
@ -40,7 +44,7 @@ function TrackedCornerstoneViewport({
|
|||||||
dataSource,
|
dataSource,
|
||||||
displaySet,
|
displaySet,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
ToolBarService
|
ToolBarService,
|
||||||
}) {
|
}) {
|
||||||
const [trackedMeasurements] = useTrackedMeasurements();
|
const [trackedMeasurements] = useTrackedMeasurements();
|
||||||
const [{ activeViewportIndex, viewports }] = useViewportGrid();
|
const [{ activeViewportIndex, viewports }] = useViewportGrid();
|
||||||
|
|||||||
@ -155,6 +155,32 @@ class MeasurementService {
|
|||||||
return source;
|
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.
|
* Add a new measurement matching criteria along with mapping functions.
|
||||||
*
|
*
|
||||||
@ -256,6 +282,93 @@ class MeasurementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a raw measurement into a source so that it may be
|
||||||
|
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||||
|
* Of the same form as the measurement source.
|
||||||
|
* @param {MeasurementSource} source The measurement source instance.
|
||||||
|
* @param {string} definition The source definition you want to add the measuremnet to.
|
||||||
|
* @param {object} data The data you wish to add to the source.
|
||||||
|
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
|
||||||
|
*/
|
||||||
|
addRawMeasurement(source, definition, data, toMeasurementSchema) {
|
||||||
|
if (!this._isValidSource(source)) {
|
||||||
|
log.warn('Invalid source. Exiting early.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = this._getSourceInfo(source);
|
||||||
|
|
||||||
|
if (!definition) {
|
||||||
|
log.warn('No source definition provided. Exiting early.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._sourceHasMappings(source)) {
|
||||||
|
log.warn(
|
||||||
|
`No measurement mappings found for '${sourceInfo}' source. Exiting early.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let measurement = {};
|
||||||
|
try {
|
||||||
|
/* Convert measurement */
|
||||||
|
measurement = toMeasurementSchema(data);
|
||||||
|
|
||||||
|
/* Assign measurement source instance */
|
||||||
|
measurement.source = source;
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(
|
||||||
|
`Failed to map '${sourceInfo}' measurement for definition ${definition}:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isValidMeasurement(measurement)) {
|
||||||
|
log.warn(
|
||||||
|
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let internalId = data.id;
|
||||||
|
if (!internalId) {
|
||||||
|
internalId = guid();
|
||||||
|
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMeasurement = {
|
||||||
|
...measurement,
|
||||||
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
|
id: internalId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.measurements[internalId]) {
|
||||||
|
log.info(
|
||||||
|
`Measurement already defined. Updating measurement.`,
|
||||||
|
newMeasurement
|
||||||
|
);
|
||||||
|
this.measurements[internalId] = newMeasurement;
|
||||||
|
this._broadcastChange(
|
||||||
|
this.EVENTS.MEASUREMENT_UPDATED,
|
||||||
|
source,
|
||||||
|
newMeasurement
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info(`Measurement added.`, newMeasurement);
|
||||||
|
this.measurements[internalId] = newMeasurement;
|
||||||
|
this._broadcastChange(
|
||||||
|
this.EVENTS.MEASUREMENT_ADDED,
|
||||||
|
source,
|
||||||
|
newMeasurement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMeasurement.id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or update persisted measurements.
|
* Adds or update persisted measurements.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -15,13 +15,22 @@ const ViewportActionBar = ({
|
|||||||
showNavArrows,
|
showNavArrows,
|
||||||
showPatientInfo: patientInfoVisibility,
|
showPatientInfo: patientInfoVisibility,
|
||||||
onSeriesChange,
|
onSeriesChange,
|
||||||
|
onHydrationClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||||
|
|
||||||
|
// TODO -> Remake this component with a bunch of generic slots that can be filled,
|
||||||
|
// Its not generic at all, isTracked etc shouldn't be parts of this component.
|
||||||
|
// It shouldn't care that a tracking mode or SR exists.
|
||||||
|
// Things like the right/left buttons should be made into smaller
|
||||||
|
// Components you can compose.
|
||||||
|
// OHIF-200 ticket.
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
isTracked,
|
isTracked,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isHydrated,
|
||||||
modality,
|
modality,
|
||||||
studyDate,
|
studyDate,
|
||||||
currentSeries,
|
currentSeries,
|
||||||
@ -44,18 +53,30 @@ const ViewportActionBar = ({
|
|||||||
const renderIconStatus = () => {
|
const renderIconStatus = () => {
|
||||||
if (modality === 'SR') {
|
if (modality === 'SR') {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex p-1 border rounded border-primary-light">
|
<>
|
||||||
<span className="text-sm font-bold leading-none text-primary-light">
|
<div className="relative flex p-1 border rounded border-primary-light">
|
||||||
SR
|
<span className="text-sm font-bold leading-none text-primary-light">
|
||||||
</span>
|
SR
|
||||||
{isLocked && (
|
</span>
|
||||||
<Icon
|
{isLocked && (
|
||||||
name="lock"
|
<Icon
|
||||||
className="absolute w-3 text-white"
|
name="lock"
|
||||||
style={{ top: -6, right: -6 }}
|
className="absolute w-3 text-white"
|
||||||
/>
|
style={{ top: -6, right: -6 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isLocked && !isHydrated && (
|
||||||
|
<div className="relative flex p-1 border rounded border-primary-light">
|
||||||
|
<span
|
||||||
|
className="text-sm font-bold leading-none text-primary-light"
|
||||||
|
onClick={onHydrationClick}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,13 +28,13 @@ function ViewerViewportGrid(props) {
|
|||||||
displaySets => {
|
displaySets => {
|
||||||
displaySets.sort((a, b) => {
|
displaySets.sort((a, b) => {
|
||||||
const isImageSet = x => x instanceof ImageSet;
|
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({
|
viewportGridService.setDisplaysetForViewport({
|
||||||
viewportIndex: 0,
|
viewportIndex: 0,
|
||||||
displaySetInstanceUID: displaySets[0].displaySetInstanceUID,
|
displaySetInstanceUID: displaySets[0].displaySetInstanceUID,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user