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:
James Petts 2020-07-01 10:08:56 +01:00 committed by GitHub
parent 25de9046b3
commit bc2bde890b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 674 additions and 46 deletions

View File

@ -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",

View File

@ -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) {

View File

@ -30,7 +30,7 @@ const ArrowAnnotate = {
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,

View File

@ -31,7 +31,7 @@ const Bidirectional = {
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
return {
id: measurementData._measurementServiceId,
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,

View File

@ -53,7 +53,7 @@ const EllipticalRoi = {
}
return {
id: measurementData._measurementServiceId,
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,

View File

@ -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,

View File

@ -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;

View File

@ -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 => {

View File

@ -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}
/>
);

View File

@ -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;
}

View File

@ -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,
};
}

View 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;
}

View File

@ -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],
})),

View File

@ -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();

View File

@ -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.
*

View File

@ -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>
</>
);
}

View File

@ -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 () => {