WIP rehydration.
This commit is contained in:
parent
aa402183f4
commit
14005d61f3
@ -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,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) {
|
||||
|
||||
@ -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,
|
||||
idid,
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -57,9 +57,6 @@ const Length = {
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(element);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onSeriesChange={onMeasurementChange}
|
||||
onHydrationClick={hydrateMeasurementService}
|
||||
showNavArrows={viewportIndex === activeViewportIndex}
|
||||
studyData={{
|
||||
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
||||
isTracked: false,
|
||||
isLocked: false,
|
||||
isLocked: displaySet.isLocked,
|
||||
isHydrated,
|
||||
studyDate: StudyDate,
|
||||
currentSeries: SeriesNumber,
|
||||
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 || '',
|
||||
},
|
||||
}}
|
||||
@ -378,4 +465,25 @@ async function _getViewportAndActiveDisplaySetData(
|
||||
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,9 @@
|
||||
import id from './id';
|
||||
import { utils, classes } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement.js';
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const cornerstoneAdapters = adapters.Cornerstone;
|
||||
|
||||
const { ImageSet } = classes;
|
||||
|
||||
@ -24,6 +27,7 @@ const CodeNameCodeSequenceValues = {
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
@ -49,7 +53,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 +95,13 @@ function _getDisplaySetsFromSeries(
|
||||
sopClassUids,
|
||||
};
|
||||
|
||||
if (_isRehydratable(displaySet, MeasurementService)) {
|
||||
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(
|
||||
@ -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 => {
|
||||
|
||||
@ -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 (
|
||||
<OHIFCornerstoneSRViewport
|
||||
DisplaySetService={DisplaySetService}
|
||||
servicesManager={servicesManager}
|
||||
{...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,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,
|
||||
};
|
||||
}
|
||||
@ -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 || '',
|
||||
},
|
||||
}}
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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 (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -64,26 +84,26 @@ const ViewportActionBar = ({
|
||||
{!isTracked ? (
|
||||
<Icon name="dotted-circle" className="w-6 text-primary-light" />
|
||||
) : (
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<span className="font-bold text-white"> tracked</span> and
|
||||
can be viewed <br /> in the measurement panel
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -196,54 +216,64 @@ function PatientInfo({
|
||||
isSticky
|
||||
isDisabled={!isOpen}
|
||||
position="bottom-right"
|
||||
content={isOpen && (
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-2">
|
||||
<span className="text-base font-bold text-white">
|
||||
{patientName}
|
||||
</span>
|
||||
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>Sex</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Age</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>MRN</span>
|
||||
<span className={classnames(classes.infoText)}>{MRN}</span>
|
||||
</div>
|
||||
content={
|
||||
isOpen && (
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Thickness
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{thickness}
|
||||
</span>
|
||||
<div className="flex flex-col ml-2">
|
||||
<span className="text-base font-bold text-white">
|
||||
{patientName}
|
||||
</span>
|
||||
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>Sex</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Age</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>MRN</span>
|
||||
<span className={classnames(classes.infoText)}>{MRN}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Spacing</span>
|
||||
<span className={classnames(classes.infoText)}>{spacing}</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Scanner</span>
|
||||
<span className={classnames(classes.infoText)}>{scanner}</span>
|
||||
<div className="flex">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Thickness
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{thickness}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Spacing
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{spacing}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Scanner
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{scanner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-end cursor-pointer">
|
||||
<div className="relative">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user