WIP rehydration.

This commit is contained in:
James A. Petts 2020-06-29 18:22:19 +01:00
parent aa402183f4
commit 14005d61f3
14 changed files with 649 additions and 104 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,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) {

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,
idid,
},
};
},
@ -57,9 +57,6 @@ const Length = {
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const points = [];
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
SOPInstanceUID: SOPInstanceUID,

View File

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

View File

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

View File

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

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

View File

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

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

View File

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