Merge branch 'feat/v2-main' of github.com:OHIF/Viewers into feat/ohif-151

This commit is contained in:
igoroctaviano 2020-06-25 16:13:47 -03:00
commit 5d89460326
38 changed files with 919 additions and 169 deletions

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",

View File

@ -90,6 +90,7 @@ export default function init({ servicesManager, configuration }) {
/* Add extension tools configuration here. */
const internalToolsConfig = {
/* TODO ArrowAnnotate input
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
@ -98,6 +99,7 @@ export default function init({ servicesManager, configuration }) {
callInputDialog(data, eventDetails, callback),
},
},
*/
};
/* Abstract tools configuration using extension configuration. */
@ -166,27 +168,48 @@ export default function init({ servicesManager, configuration }) {
const _initMeasurementService = measurementService => {
/* Initialization */
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(
measurementService
);
const {
Length,
Bidirectional,
EllipticalRoi,
ArrowAnnotate,
} = measurementServiceMappingsFactory(measurementService);
const csToolsVer4MeasurementSource = measurementService.createSource(
'CornerstoneTools',
'4'
);
/* Matching Criterias */
const matchingCriteria = {
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
};
/* Mappings */
measurementService.addMapping(
csToolsVer4MeasurementSource,
'Length',
matchingCriteria,
toAnnotation,
toMeasurement
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
measurementService.addMapping(
csToolsVer4MeasurementSource,
'Bidirectional',
Bidirectional.matchingCriteria,
Bidirectional.toAnnotation,
Bidirectional.toMeasurement
);
measurementService.addMapping(
csToolsVer4MeasurementSource,
'EllipticalRoi',
EllipticalRoi.matchingCriteria,
EllipticalRoi.toAnnotation,
EllipticalRoi.toMeasurement
);
measurementService.addMapping(
csToolsVer4MeasurementSource,
'ArrowAnnotate',
ArrowAnnotate.matchingCriteria,
ArrowAnnotate.toAnnotation,
ArrowAnnotate.toMeasurement
);
return csToolsVer4MeasurementSource;

View File

@ -0,0 +1,49 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const ArrowAnnotate = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const points = [];
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
text: measurementData.text,
type: getValueTypeFromToolType(tool),
points: getPointsFromHandles(measurementData.handles),
};
},
};
export default ArrowAnnotate;

View File

@ -0,0 +1,50 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const Bidirectional = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const { handles } = measurementData;
const longAxis = [handles.start, handles.end];
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
return {
id: measurementData._measurementServiceId,
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 },
};
},
};
export default Bidirectional;

View File

@ -0,0 +1,74 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const EllipticalRoi = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
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._measurementServiceId,
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,
};
},
};
export default EllipticalRoi;

View File

@ -0,0 +1,79 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const Length = {
toAnnotation: (measurement, definition) => {
const {
id,
label,
description,
points,
unit,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID,
} = measurement;
return {
toolName: definition,
measurementData: {
sopInstanceUid: SOPInstanceUID,
frameOfReferenceUID: FrameOfReferenceUID,
SeriesInstanceUID: referenceSeriesUID,
unit,
text: label,
description,
handles: getHandlesFromPoints(points),
_measurementServiceId: id,
},
};
},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const points = [];
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
length: measurementData.length,
type: getValueTypeFromToolType(tool),
points: getPointsFromHandles(measurementData.handles),
};
},
};
export default Length;

View File

@ -0,0 +1 @@
export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate'];

View File

@ -1,11 +1,7 @@
import cornerstone from 'cornerstone-core';
const SUPPORTED_TOOLS = [
'Length',
'EllipticalRoi',
'RectangleRoi',
'ArrowAnnotate',
];
import Length from './Length';
import Bidirectional from './Bidirectional';
import ArrowAnnotate from './ArrowAnnotate';
import EllipticalRoi from './EllipticalRoi';
const measurementServiceMappingsFactory = measurementService => {
/**
@ -15,129 +11,87 @@ const measurementServiceMappingsFactory = measurementService => {
* @param {string} definition The source definition
* @return {Object} Cornerstone annotation data
*/
const toAnnotation = (measurement, definition) => {
const {
id,
label,
description,
points,
unit,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID,
} = measurement;
return {
toolName: definition,
measurementData: {
sopInstanceUid: SOPInstanceUID,
frameOfReferenceUID: FrameOfReferenceUID,
SeriesInstanceUID: referenceSeriesUID,
unit,
text: label,
description,
handles: _getHandlesFromPoints(points),
_measurementServiceId: id,
},
};
};
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
const toMeasurement = csToolsAnnotation => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
if (!validToolType(tool)) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = _getAttributes(element);
const points = [];
points.push(measurementData.handles);
return {
id: measurementData._measurementServiceId,
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: _getPointsFromHandles(measurementData.handles),
};
};
const _getAttributes = element => {
const enabledElement = cornerstone.getEnabledElement(element);
const imageId = enabledElement.image.imageId;
const instance = cornerstone.metaData.get('instance', imageId);
return {
SOPInstanceUID: instance.SOPInstanceUID,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
};
};
const _getValueTypeFromToolType = toolType => {
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
const {
POLYLINE,
ELLIPSE,
POINT,
BIDIRECTIONAL,
} = measurementService.VALUE_TYPES;
/* TODO: Relocate static 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,
RectangleRoi: POLYLINE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
};
const _getPointsFromHandles = handles => {
let 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 points;
};
const _getHandlesFromPoints = points => {
return points
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
};
return {
toAnnotation,
toMeasurement,
Length: {
toAnnotation: Length.toAnnotation,
toMeasurement: csToolsAnnotation =>
Length.toMeasurement(csToolsAnnotation, _getValueTypeFromToolType),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
Bidirectional: {
toAnnotation: Bidirectional.toAnnotation,
toMeasurement: csToolsAnnotation =>
Bidirectional.toMeasurement(
csToolsAnnotation,
_getValueTypeFromToolType
),
matchingCriteria: [
// TODO -> We should eventually do something like shortAxis + longAxis,
// But its still a little unclear how these automatic interpretations will work.
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
ArrowAnnotate: {
toAnnotation: ArrowAnnotate.toAnnotation,
toMeasurement: csToolsAnnotation =>
ArrowAnnotate.toMeasurement(
csToolsAnnotation,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.POINT,
points: 1,
},
],
},
EllipticalRoi: {
toAnnotation: EllipticalRoi.toAnnotation,
toMeasurement: csToolsAnnotation =>
EllipticalRoi.toMeasurement(
csToolsAnnotation,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.ELLIPSE,
},
],
},
};
};

View File

@ -0,0 +1,5 @@
export default function getHandlesFromPoints(points) {
return points
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
}

View File

@ -0,0 +1,12 @@
export default function getPointsFromHandles(handles) {
let 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 points;
}

View File

@ -0,0 +1,14 @@
import cornerstone from 'cornerstone-core';
export default function getSOPInstanceAttributes(element) {
const enabledElement = cornerstone.getEnabledElement(element);
const imageId = enabledElement.image.imageId;
const instance = cornerstone.metaData.get('instance', imageId);
return {
SOPInstanceUID: instance.SOPInstanceUID,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
};
}

View File

@ -63,13 +63,20 @@ function ViewerLayout({
setActiveTool(isNested ? tool : defaultTool);
};
const onPrimaryClickHandler = (evt, btn) => {
if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) {
const { commandName, commandOptions } = btn.props.commands[evt.value];
commandsManager.runCommand(commandName, commandOptions);
}
};
useEffect(() => {
const { unsubscribe } = ToolBarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
() => {
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
const updatedToolbars = {
primary: ToolBarService.getButtonSection('primary', { setActiveTool: setActiveToolHandler }),
primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler, setActiveTool: setActiveToolHandler }),
secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }),
};
setToolbars(updatedToolbars);

View File

@ -30,7 +30,7 @@
"peerDependencies": {
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.15.1",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"prop-types": "^15.6.2",
"react": "^16.8.6",

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",

View File

@ -42,7 +42,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
trackedSeries.includes(m.referenceSeriesUID)
);
const mappedMeasurements = filteredMeasurements.map((m, index) =>
_mapMeasurementToDisplay(m, index)
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
);
setDisplayMeasurements(mappedMeasurements);
// eslint-ignore-next-line
@ -130,7 +130,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
PanelMeasurementTableTracking.propTypes = {};
// TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, index) {
function _mapMeasurementToDisplay(measurement, index, types) {
const {
id,
label,
@ -153,12 +153,14 @@ function _mapMeasurementToDisplay(measurement, index) {
return {
id: index + 1,
label: '(empty)', // 'Label short description',
displayText: _getDisplayText(
measurement.points,
PixelSpacing,
SeriesNumber,
InstanceNumber
),
displayText:
_getDisplayText(
measurement,
PixelSpacing,
SeriesNumber,
InstanceNumber,
types
) || [],
// TODO: handle one layer down
isActive: false, // activeMeasurementItem === i + 1,
};
@ -169,7 +171,13 @@ function _mapMeasurementToDisplay(measurement, index) {
* @param {*} points
* @param {*} pixelSpacing
*/
function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
function _getDisplayText(
measurement,
pixelSpacing,
seriesNumber,
instanceNumber,
types
) {
// TODO: determination of shape influences text
// Length: 'xx.x unit (S:x, I:x)'
// Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
@ -177,6 +185,8 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
// Bidirectional?
// Freehand?
const { type, points } = measurement;
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
@ -186,13 +196,37 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
: [1, 1];
const unit = hasPixelSpacing ? 'mm' : 'px';
const { x: x1, y: y1 } = points[0];
const { x: x2, y: y2 } = points[1];
const dx = (x2 - x1) * colPixelSpacing;
const dy = (y2 - y1) * rowPixelSpacing;
const length = _round(Math.sqrt(dx * dx + dy * dy), 1);
switch (type) {
case types.POLYLINE:
const { length } = measurement;
return `${length} ${unit} (S:${seriesNumber}, I:${instanceNumber})`;
const roundedLength = _round(length, 1);
return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
];
case types.BIDIRECTIONAL:
const { shortestDiameter, longestDiameter } = measurement;
const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1);
return [
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`s: ${roundedShortestDiameter} ${unit}`,
];
case types.ELLIPSE:
const { area } = measurement;
const roundedArea = _round(area, 1);
return [
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
];
case types.POINT:
const { text } = measurement;
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
}
}
function _round(value, decimals) {

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import {
@ -13,6 +14,18 @@ import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
import { useTrackedMeasurements } from './../getContextModule';
// TODO -> Get this list from the list of tracked measurements.
const {
ArrowAnnotateTool,
BidirectionalTool,
EllipticalRoiTool,
LengthTool,
} = cornerstoneTools;
const BaseAnnotationTool = cornerstoneTools.importInternal(
'base/BaseAnnotationTool'
);
// const cine = viewportSpecificData.cine;
// isPlaying = cine.isPlaying === true;
@ -27,6 +40,7 @@ function TrackedCornerstoneViewport({
viewportIndex,
}) {
const [trackedMeasurements] = useTrackedMeasurements();
const [
{ activeViewportIndex, viewports },
dispatchViewportGrid,
@ -34,6 +48,8 @@ function TrackedCornerstoneViewport({
// viewportIndex, onSubmit
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [viewportData, setViewportData] = useState(null);
const [element, setElement] = useState(null);
const [isTracked, setIsTracked] = useState(false);
// TODO: Still needed? Better way than import `OHIF` and destructure?
// Why is this managed by `core`?
useEffect(() => {
@ -42,6 +58,75 @@ function TrackedCornerstoneViewport({
};
}, []);
useEffect(() => {
if (!element) {
return;
}
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(tool => tool.element === element);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.image) {
cornerstone.updateImage(element);
}
}, [isTracked]);
const onElementEnabled = evt => {
const eventData = evt.detail;
const targetElement = eventData.element;
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(
tool => tool.element === targetElement
);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
} else if (tool instanceof BaseAnnotationTool) {
const configuration = tool.configuration;
configuration.renderDashed = true;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(targetElement);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
setElement(targetElement);
};
useEffect(() => {
const {
StudyInstanceUID,
@ -116,6 +201,7 @@ function TrackedCornerstoneViewport({
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
);
const { trackedSeries } = trackedMeasurements.context;
const {
Modality,
SeriesDate,
@ -131,6 +217,11 @@ function TrackedCornerstoneViewport({
SliceThickness,
} = displaySet.images[0];
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
return (
<>
<ViewportActionBar
@ -159,6 +250,7 @@ function TrackedCornerstoneViewport({
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full">
<CornerstoneViewport
onElementEnabled={onElementEnabled}
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}

View File

@ -55,7 +55,7 @@
},
"devDependencies": {
"@ohif/core": "^2.9.6",
"cornerstone-tools": "4.15.1",
"cornerstone-tools": "4.16.0",
"@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.3.0",
"cornerstone-wado-image-loader": "^3.1.2",

View File

@ -1,5 +1,8 @@
// TODO: torn, can either bake this here; or have to create a whole new button type
// Only ways that you can pass in a custom React component for render :l
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
import React from 'react';
import classnames from 'classnames';
export default [
// Divider
@ -29,13 +32,58 @@ export default [
config: {
groupName: 'primaryTool',
},
component: ExpandableToolbarButton,
props: {
isActive: true,
icon: 'tool-window-level',
label: 'Levels',
commandName: 'setToolActive',
commandOptions: { toolName: 'Wwwc' },
commands: {
'windowLevelPreset1': {
commandName: 'windowLevelPreset1',
commandOptions: {},
},
'windowLevelPreset2': {
commandName: 'windowLevelPreset2',
commandOptions: {},
},
'windowLevelPreset3': {
commandName: 'windowLevelPreset3',
commandOptions: {},
},
'windowLevelPreset4': {
commandName: 'windowLevelPreset4',
commandOptions: {},
},
'windowLevelPreset5': {
commandName: 'windowLevelPreset5',
commandOptions: {},
}
},
type: 'primary',
content: ListMenu,
contentProps: {
options: [
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
],
renderer: ({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)
}
},
},
{

View File

@ -31,7 +31,7 @@
},
"peerDependencies": {
"cornerstone-core": "^2.3.0",
"cornerstone-tools": "^4.12.0",
"cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3"
},

View File

@ -40,6 +40,10 @@ const MEASUREMENT_SCHEMA_KEYS = [
'type',
'unit',
'area', // TODO: Add concept names instead (descriptor)
'length',
'shortestDiameter',
'longestDiameter',
'text', // NOTE: There is nothing like this in SR.
'points',
'source',
];
@ -53,6 +57,7 @@ const EVENTS = {
const VALUE_TYPES = {
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
ELLIPSE: 'value_type::ellipse',
MULTIPOINT: 'value_type::multipoint',
CIRCLE: 'value_type::circle',

View File

@ -156,6 +156,9 @@ export default class ToolBarService {
if (btn.props.clickHandler) {
btn.clickHandler(evt, btn, btnSection);
}
if (props && props.onClick) {
props.onClick(evt, btn, btnSection, props);
}
};
return {

View File

@ -32,6 +32,8 @@ export {
DateRange,
Dialog,
EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon,
IconButton,
Input,

View File

@ -0,0 +1,16 @@
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
content: "";
position: absolute;
bottom: -10px;
border-width: 10px 10px 0;
border-style: solid;
border-color:#5acce6 transparent;
}
.ExpandableToolbarButton .ExpandableToolbarButton__content {
display: none;
}
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
display: block;
}

View File

@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { IconButton, Icon } from '@ohif/ui';
import './ExpandableToolbarButton.css';
const ExpandableToolbarButton = ({
type,
id,
isActive,
onClick,
icon,
className,
content: Content,
contentProps
}) => {
const classes = {
type: {
primary: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
secondary: isActive
? 'text-black'
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
},
};
const onChildClickHandler = (...args) => {
onClick(...args);
if (contentProps.onClick) {
contentProps.onClick(...args);
}
};
const onClickHandler = (...args) => {
onClick(...args);
};
return (
<div key={id} className="ExpandableToolbarButton">
<IconButton
variant={isActive ? 'contained' : 'text'}
className={classnames(
'mx-1',
classes.type[type],
isActive && 'ExpandableToolbarButton__arrow'
)}
onClick={onClickHandler}
key={id}
>
<Icon name={icon} />
</IconButton>
<div className="absolute z-10 pt-4">
<div className={classnames("ExpandableToolbarButton__content w-48", className)}>
<Content {...contentProps} onClick={onChildClickHandler} />
</div>
</div>
</div>
);
};
const noop = () => { };
ExpandableToolbarButton.defaultProps = {
isActive: false,
type: 'primary',
content: null,
onClick: noop,
};
ExpandableToolbarButton.propTypes = {
/* Influences background/hover styling */
type: PropTypes.oneOf(['primary', 'secondary']),
id: PropTypes.string.isRequired,
isActive: PropTypes.bool,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
contentProps: PropTypes.object,
};
export default ExpandableToolbarButton;

View File

@ -0,0 +1,58 @@
---
name: Expandable Toolbar Button
menu: General
route: components/expandableToolbarButton
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
# Toolbar Button
Expandable Toolbar Buttons are used to populate the Toolbar.
## Import
```javascript
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
const props = {
content: ListMenu,
contentProps: {
options: [
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
],
renderer: ({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)
}
};
return (
<div className="py-10 flex justify-center">
<ExpandableToolbarButton {...props} />
</div>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />

View File

@ -0,0 +1,2 @@
import ExpandableToolbarButton from './ExpandableToolbarButton';
export default ExpandableToolbarButton;

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseClasses =
'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none';
'text-center items-center justify-center outline-none font-bold focus:outline-none';
const roundedClasses = {
none: '',
@ -113,7 +113,7 @@ const IconButton = ({
};
IconButton.defaultProps = {
onClick: () => {},
onClick: () => { },
color: 'default',
disabled: false,
fullWidth: false,

View File

@ -0,0 +1,65 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
const ListMenu = ({ options = [], renderer, onClick }) => {
const [selectedIndex, setSelectedIndex] = useState(null);
const ListItem = (props) => {
const flex = 'flex flex-row justify-between items-center';
const theme = 'bg-indigo-dark';
const hover = 'hover:bg-primary-dark';
const spacing = 'p-3 h-8';
return (
<div
className={classnames(
flex,
theme,
spacing,
'cursor-pointer',
!props.isActive && hover,
props.isActive && 'bg-primary-light',
)}
onClick={props.onClick}
>
{renderer && renderer(props)}
</div>
);
};
return (
<div className="flex flex-col rounded-md bg-secondary-dark pt-2 pb-2">
{options.map((option, index) => {
const onClickHandler = () => {
setSelectedIndex(index);
onClick({ ...option, index });
};
return (
<ListItem
key={`ListItem${index}`}
{...option}
index={index}
isActive={selectedIndex === index}
onClick={onClickHandler}
/>
);
})}
</div>
);
};
const noop = () => { };
ListMenu.propTypes = {
options: PropTypes.array.isRequired,
renderer: PropTypes.func.isRequired,
onClick: PropTypes.func
};
ListMenu.defaultProps = {
onClick: noop
};
export default ListMenu;

View File

@ -0,0 +1,52 @@
---
name: List Menu
menu: General
route: components/listMenu
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ListMenu } from '@ohif/ui';
# List Menu
List Menus are used to populate expandable Toolbar.
## Import
```javascript
import { ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
return (
<ToolbarButton options={[
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
]}
renderer={
({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)}
/>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />

View File

@ -0,0 +1,2 @@
import ListMenu from './ListMenu';
export default ListMenu;

View File

@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
</div>
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-112">
{!!data.length &&
data.map((measurementItem) => {
data.map(measurementItem => {
const { id, label, displayText, isActive } = measurementItem;
return (
<div
@ -45,9 +45,11 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
<span className="text-base text-primary-light mb-1">
{label}
</span>
<span className="pl-2 border-l border-primary-light text-base text-white">
{displayText}
</span>
{displayText.map(line => (
<span className="pl-2 border-l border-primary-light text-base text-white">
{line}
</span>
))}
<Icon
className={classnames(
'text-white w-4 absolute cursor-pointer transition duration-300',
@ -61,7 +63,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
right: 4,
transform: isActive ? '' : 'translateX(100%)',
}}
onClick={(e) => {
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(id);
@ -108,7 +110,7 @@ MeasurementTable.propTypes = {
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.string,
displayText: PropTypes.string,
displayText: PropTypes.arrayOf(PropTypes.string),
isActive: PropTypes.bool,
})
),

View File

@ -76,7 +76,7 @@ const Select = ({
options={options}
value={selectedOptions}
onChange={(selectedOptions, { action }) => {
const newSelection = selectedOptions.reduce(
const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce(
(acc, curr) => acc.concat([curr.value]),
[]
);

View File

@ -24,7 +24,7 @@ const arrowPositionStyle = {
},
};
const Tooltip = ({ content, isSticky, position, tight, children }) => {
const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => {
const [isActive, setIsActive] = useState(false);
const handleMouseOver = () => {
@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => {
}
};
const isOpen = isSticky || isActive;
const isOpen = (isSticky || isActive) && !isDisabled;
return (
<div
@ -84,10 +84,12 @@ Tooltip.defaultProps = {
tight: false,
isSticky: false,
position: 'bottom',
isDisabled: false
};
Tooltip.propTypes = {
// Allow null
/** prevents tooltip from rendering despite hover/active/sticky */
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf([
'bottom',

View File

@ -39,6 +39,8 @@ import ThumbnailNoImage from './ThumbnailNoImage';
import ThumbnailTracked from './ThumbnailTracked';
import ThumbnailList from './ThumbnailList';
import ToolbarButton from './ToolbarButton';
import ExpandableToolbarButton from './ExpandableToolbarButton';
import ListMenu from './ListMenu';
import Tooltip from './Tooltip';
import Typography from './Typography';
import Viewport from './Viewport';
@ -52,6 +54,8 @@ export {
DateRange,
Dialog,
EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon,
IconButton,
Input,

View File

@ -16,6 +16,13 @@ module.exports = {
initial: 'initial',
inherit: 'inherit',
indigo: {
dark: '#0b1a42'
},
aqua: {
pale: '#7bb2ce'
},
primary: {
light: '#5acce6',
main: '#0944b3',

View File

@ -67,7 +67,7 @@
"classnames": "^2.2.6",
"core-js": "^3.2.1",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1",
"cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "^0.12.2",
"dicom-parser": "^1.8.3",

View File

@ -19,7 +19,9 @@ window.config = {
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
},
},
],
defaultDataSourceName: 'dicomweb',
};

View File

@ -6755,10 +6755,10 @@ cornerstone-math@^0.1.8:
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
cornerstone-tools@4.15.1:
version "4.15.1"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.15.1.tgz#f0b1026da9c7758defc088bb2f1e78426a23d91e"
integrity sha512-fJuTUJW/NDSD520jPB++tX//Kq80jPDngChHZrhx2xjj/9dCnexD9kmTX30Z0WUq0a2JEHZ6FlcOX38kSmw1hQ==
cornerstone-tools@4.16.0:
version "4.16.0"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.0.tgz#af3d32d13722b97bec258492642e622312280196"
integrity sha512-kUhuSb2Ixpd2hgbdem+740rnN4hmoxzcOBNaUcsizFRWjMAsgc0yUxBFwLl0mIs811mefq79JOL+juwRA8T3Tg==
dependencies:
"@babel/runtime" "7.1.2"
cornerstone-math "0.1.7"