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",
|
||||
"version": "2.7.3",
|
||||
"version": "3.0.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -246,10 +246,12 @@ 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;
|
||||
|
||||
const measurementId = addOrUpdate(csToolName, evtDetail);
|
||||
|
||||
if (measurementId) {
|
||||
|
||||
@ -30,7 +30,7 @@ const ArrowAnnotate = {
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
|
||||
@ -31,7 +31,7 @@ const Bidirectional = {
|
||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
|
||||
@ -53,7 +53,7 @@ const EllipticalRoi = {
|
||||
}
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
|
||||
@ -26,7 +26,7 @@ const Length = {
|
||||
text: label,
|
||||
description,
|
||||
handles: getHandlesFromPoints(points),
|
||||
_measurementServiceId: id,
|
||||
id,
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -57,11 +57,8 @@ const Length = {
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(element);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
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 cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
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 TOOL_NAMES from './constants/toolNames';
|
||||
import { adapters } from 'dcmjs';
|
||||
import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
|
||||
import id from './id';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
|
||||
// const cine = viewportSpecificData.cine;
|
||||
const { StackManager, guid } = OHIF.utils;
|
||||
|
||||
// isPlaying = cine.isPlaying === true;
|
||||
// frameRate = cine.cineFrameRate || frameRate;
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking';
|
||||
|
||||
function OHIFCornerstoneSRViewport({
|
||||
children,
|
||||
dataSource,
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
DisplaySetService,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
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;
|
||||
|
||||
// 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;
|
||||
@ -196,8 +219,6 @@ function OHIFCornerstoneSRViewport({
|
||||
|
||||
const { Modality } = displaySet;
|
||||
|
||||
// TODO -> Get this from the associated stack.
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
@ -210,6 +231,7 @@ function OHIFCornerstoneSRViewport({
|
||||
SeriesInstanceUID,
|
||||
PixelSpacing,
|
||||
SeriesNumber,
|
||||
displaySetInstanceUID,
|
||||
} = activeDisplaySetData;
|
||||
|
||||
const onMeasurementChange = direction => {
|
||||
@ -229,22 +251,158 @@ 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];
|
||||
}
|
||||
});
|
||||
|
||||
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 (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onSeriesChange={onMeasurementChange}
|
||||
onHydrationClick={hydrateMeasurementService}
|
||||
showNavArrows={viewportIndex === activeViewportIndex}
|
||||
studyData={{
|
||||
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
||||
isTracked: false,
|
||||
isLocked: false,
|
||||
isLocked: displaySet.isLocked,
|
||||
isHydrated,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription,
|
||||
@ -381,9 +539,31 @@ async function _getViewportAndActiveDisplaySetData(
|
||||
SeriesDescription: image0.SeriesDescription,
|
||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||
SeriesNumber: image0.SeriesNumber,
|
||||
displaySetInstanceUID,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import id from './id';
|
||||
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;
|
||||
|
||||
@ -24,6 +25,7 @@ const CodeNameCodeSequenceValues = {
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
@ -49,7 +51,7 @@ function _getDisplaySetsFromSeries(
|
||||
throw new Error('No instances were provided');
|
||||
}
|
||||
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const { DisplaySetService, MeasurementService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
@ -91,6 +93,18 @@ function _getDisplaySetsFromSeries(
|
||||
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.
|
||||
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
@ -346,6 +360,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.`
|
||||
@ -362,6 +382,7 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
labels: [],
|
||||
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
};
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
@ -389,11 +410,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 => {
|
||||
|
||||
@ -23,6 +23,18 @@ 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.
|
||||
// OHIF-229
|
||||
{
|
||||
id: 'org.ohif.cornerstone',
|
||||
version: '3.0.0',
|
||||
},
|
||||
{
|
||||
id: 'org.ohif.measurement-tracking',
|
||||
version: '^0.0.1',
|
||||
},
|
||||
],
|
||||
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
@ -34,13 +46,12 @@ export default {
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
getViewportModule({ servicesManager }) {
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneSRViewport = props => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
|
||||
return (
|
||||
<OHIFCornerstoneSRViewport
|
||||
DisplaySetService={DisplaySetService}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
on: {
|
||||
TRACK_SERIES: 'promptBeginTracking',
|
||||
SET_TRACKED_SERIES: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
promptBeginTracking: {
|
||||
@ -69,6 +75,12 @@ const machineConfiguration = {
|
||||
target: 'idle',
|
||||
},
|
||||
],
|
||||
SET_TRACKED_SERIES: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
promptTrackNewSeries: {
|
||||
@ -139,6 +151,10 @@ const defaultOptions = {
|
||||
trackedStudy: evt.data.StudyInstanceUID,
|
||||
trackedSeries: [evt.data.SeriesInstanceUID],
|
||||
})),
|
||||
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({
|
||||
trackedStudy: evt.StudyInstanceUID,
|
||||
trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs],
|
||||
})),
|
||||
addTrackedSeries: assign((ctx, evt) => ({
|
||||
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
||||
})),
|
||||
|
||||
@ -17,6 +17,10 @@ import ViewportOverlay from './ViewportOverlay';
|
||||
const { formatDate } = utils;
|
||||
|
||||
// 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,
|
||||
@ -40,7 +44,7 @@ function TrackedCornerstoneViewport({
|
||||
dataSource,
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
ToolBarService
|
||||
ToolBarService,
|
||||
}) {
|
||||
const [trackedMeasurements] = useTrackedMeasurements();
|
||||
const [{ activeViewportIndex, viewports }] = useViewportGrid();
|
||||
|
||||
@ -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,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.
|
||||
*
|
||||
|
||||
@ -15,13 +15,22 @@ 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.
|
||||
// OHIF-200 ticket.
|
||||
|
||||
const {
|
||||
label,
|
||||
isTracked,
|
||||
isLocked,
|
||||
isHydrated,
|
||||
modality,
|
||||
studyDate,
|
||||
currentSeries,
|
||||
@ -44,18 +53,30 @@ const ViewportActionBar = ({
|
||||
const renderIconStatus = () => {
|
||||
if (modality === 'SR') {
|
||||
return (
|
||||
<div className="relative flex p-1 border rounded border-primary-light">
|
||||
<span className="text-sm font-bold leading-none text-primary-light">
|
||||
SR
|
||||
</span>
|
||||
{isLocked && (
|
||||
<Icon
|
||||
name="lock"
|
||||
className="absolute w-3 text-white"
|
||||
style={{ top: -6, right: -6 }}
|
||||
/>
|
||||
<>
|
||||
<div className="relative flex p-1 border rounded border-primary-light">
|
||||
<span className="text-sm font-bold leading-none text-primary-light">
|
||||
SR
|
||||
</span>
|
||||
{isLocked && (
|
||||
<Icon
|
||||
name="lock"
|
||||
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.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 () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user