Render text properly, render arrow + freetext. Need to move textboxes appropriately.
This commit is contained in:
parent
d956c3008c
commit
e3f2e26e3b
@ -26,12 +26,16 @@ const CodeNameCodeSequenceValues = {
|
|||||||
ImageLibraryGroup: '126200',
|
ImageLibraryGroup: '126200',
|
||||||
TrackingUniqueIdentifier: '112040',
|
TrackingUniqueIdentifier: '112040',
|
||||||
TrackingIdentifier: '112039',
|
TrackingIdentifier: '112039',
|
||||||
|
Finding: '121071',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_TYPE = {
|
const RELATIONSHIP_TYPE = {
|
||||||
INFERRED_FROM: 'INFERRED FROM',
|
INFERRED_FROM: 'INFERRED FROM',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4';
|
||||||
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic SOPClassHandler:
|
* Basic SOPClassHandler:
|
||||||
* - For all Image types that are stackable, create
|
* - For all Image types that are stackable, create
|
||||||
@ -68,8 +72,9 @@ function _getDisplaySetsFromSeries(
|
|||||||
const { ConceptNameCodeSequence, ContentSequence } = instance;
|
const { ConceptNameCodeSequence, ContentSequence } = instance;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!ConceptNameCodeSequence ||
|
||||||
ConceptNameCodeSequence.CodeValue !==
|
ConceptNameCodeSequence.CodeValue !==
|
||||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Only support Imaging Measurement Report SRs (TID1500) for now'
|
'Only support Imaging Measurement Report SRs (TID1500) for now'
|
||||||
@ -134,49 +139,6 @@ function _getDisplaySetsFromSeries(
|
|||||||
return [displaySet];
|
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(
|
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||||
srDisplaySet,
|
srDisplaySet,
|
||||||
newDisplaySet,
|
newDisplaySet,
|
||||||
@ -459,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const Findings = mergedContentSequence.filter(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.Finding
|
||||||
|
);
|
||||||
|
|
||||||
const measurement = {
|
const measurement = {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
labels: [],
|
labels: [],
|
||||||
@ -467,6 +435,23 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Findings.length) {
|
||||||
|
// TODO -> Pull in labels when we have them, just free text for now.
|
||||||
|
const cornerstoneFreeTextFinding = Findings.find(
|
||||||
|
Finding =>
|
||||||
|
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||||
|
CORNERSTONE_CODING_SCHEME_DESIGNATOR &&
|
||||||
|
Finding.ConceptCodeSequence.CodeValue ===
|
||||||
|
CORNERSTONE_FREETEXT_CODE_VALUE
|
||||||
|
);
|
||||||
|
if (cornerstoneFreeTextFinding) {
|
||||||
|
measurement.labels.push({
|
||||||
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||||
|
value: cornerstoneFreeTextFinding.ConceptCodeSequence.CodeMeaning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NUMContentItems.forEach(item => {
|
NUMContentItems.forEach(item => {
|
||||||
const {
|
const {
|
||||||
ConceptNameCodeSequence,
|
ConceptNameCodeSequence,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||||
|
import { pixelToCanvas } from 'cornerstone-core';
|
||||||
|
|
||||||
import TOOL_NAMES from '../constants/toolNames';
|
import TOOL_NAMES from '../constants/toolNames';
|
||||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
@ -10,6 +11,7 @@ const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
|||||||
const drawCircle = importInternal('drawing/drawCircle');
|
const drawCircle = importInternal('drawing/drawCircle');
|
||||||
const drawEllipse = importInternal('drawing/drawEllipse');
|
const drawEllipse = importInternal('drawing/drawEllipse');
|
||||||
const drawHandles = importInternal('drawing/drawHandles');
|
const drawHandles = importInternal('drawing/drawHandles');
|
||||||
|
const drawArrow = importInternal('drawing/drawArrow');
|
||||||
const getNewContext = importInternal('drawing/getNewContext');
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
const BaseTool = importInternal('base/BaseTool');
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
|
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
|
||||||
@ -60,6 +62,12 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
|||||||
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
|
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// First: Render annotations with textboxes moved to closest side
|
||||||
|
// TODO: Render all annotations and get their locations.
|
||||||
|
// TODO: Find suitable places for textboxes that aren't covered.
|
||||||
|
|
||||||
|
let shouldRepositionTextBoxes = false;
|
||||||
|
|
||||||
for (let i = 0; i < filteredToolData.length; i++) {
|
for (let i = 0; i < filteredToolData.length; i++) {
|
||||||
const data = filteredToolData[i];
|
const data = filteredToolData[i];
|
||||||
const { renderableData, labels } = data;
|
const { renderableData, labels } = data;
|
||||||
@ -80,8 +88,10 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
|||||||
|
|
||||||
switch (GraphicType) {
|
switch (GraphicType) {
|
||||||
case SCOORD_TYPES.POINT:
|
case SCOORD_TYPES.POINT:
|
||||||
|
this.renderPoint(renderableDataForGraphicType, eventData, options);
|
||||||
|
break;
|
||||||
case SCOORD_TYPES.MULTIPOINT:
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
this.renderPointOrMultipoint(
|
this.renderMultipoint(
|
||||||
renderableDataForGraphicType,
|
renderableDataForGraphicType,
|
||||||
eventData,
|
eventData,
|
||||||
options
|
options
|
||||||
@ -110,36 +120,53 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
|||||||
const { element } = eventData;
|
const { element } = eventData;
|
||||||
const context = getNewContext(eventData.canvasContext.canvas);
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
debugger;
|
if (!data.handles || !data.handles.textBox) {
|
||||||
|
const textBox = {
|
||||||
|
active: false,
|
||||||
|
hasMoved: true,
|
||||||
|
movesIndependently: false,
|
||||||
|
drawnIndependently: true,
|
||||||
|
allowedOutsideImage: true,
|
||||||
|
hasBoundingBox: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const anchorPoints = _getTextBoxAnchorPointsForRenderableData(
|
||||||
|
renderableData,
|
||||||
|
eventData
|
||||||
|
);
|
||||||
|
textBox.anchorPoints = anchorPoints;
|
||||||
|
|
||||||
|
const bottomRight = {
|
||||||
|
x: Math.max(...anchorPoints.map(point => point.x)),
|
||||||
|
y: Math.max(...anchorPoints.map(point => point.y)),
|
||||||
|
};
|
||||||
|
|
||||||
|
textBox.x = bottomRight.x;
|
||||||
|
textBox.y = bottomRight.y;
|
||||||
|
|
||||||
|
data.handles = {};
|
||||||
|
data.handles.textBox = textBox;
|
||||||
|
|
||||||
|
shouldRepositionTextBoxes = true;
|
||||||
|
}
|
||||||
|
|
||||||
const text = _getTextBoxLinesFromLabels(labels);
|
const text = _getTextBoxLinesFromLabels(labels);
|
||||||
|
|
||||||
debugger;
|
|
||||||
|
|
||||||
function textBoxAnchorPoints() {
|
function textBoxAnchorPoints() {
|
||||||
return [{ x: 1000, y: 1000 }];
|
return data.handles.textBox.anchorPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(context, context => {
|
draw(context, context => {
|
||||||
drawLinkedTextBox(
|
drawLinkedTextBox(
|
||||||
context,
|
context,
|
||||||
element,
|
element,
|
||||||
{
|
data.handles.textBox,
|
||||||
active: false,
|
|
||||||
hasMoved: false,
|
|
||||||
movesIndependently: false,
|
|
||||||
drawnIndependently: true,
|
|
||||||
allowedOutsideImage: true,
|
|
||||||
hasBoundingBox: true,
|
|
||||||
x: 256,
|
|
||||||
y: 256,
|
|
||||||
},
|
|
||||||
text,
|
text,
|
||||||
null,
|
data.handles,
|
||||||
textBoxAnchorPoints,
|
textBoxAnchorPoints,
|
||||||
color,
|
color,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
10,
|
0,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -157,7 +184,7 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPointOrMultipoint(renderableData, eventData, options) {
|
renderMultipoint(renderableData, eventData, options) {
|
||||||
const context = getNewContext(eventData.canvasContext.canvas);
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
renderableData.forEach(points => {
|
renderableData.forEach(points => {
|
||||||
@ -167,6 +194,46 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPoint(renderableData, eventData, options) {
|
||||||
|
// Render single point as an arrow.
|
||||||
|
debugger;
|
||||||
|
const { element, image } = eventData;
|
||||||
|
const { rows, columns } = image;
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
const { color, lineWidth } = options;
|
||||||
|
|
||||||
|
// Find a suitable length for the image size.
|
||||||
|
|
||||||
|
const xOffset = columns / 10;
|
||||||
|
const yOffset = rows / 10;
|
||||||
|
|
||||||
|
debugger;
|
||||||
|
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
const point = points[0]; // The SCOORD type is POINT so the array length is 1.
|
||||||
|
draw(context, context => {
|
||||||
|
// Draw the arrow
|
||||||
|
const handleStartCanvas = pixelToCanvas(element, point);
|
||||||
|
const handleEndCanvas = pixelToCanvas(element, {
|
||||||
|
x: point.x + xOffset,
|
||||||
|
y: point.y + yOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
debugger;
|
||||||
|
|
||||||
|
drawArrow(
|
||||||
|
context,
|
||||||
|
handleEndCanvas,
|
||||||
|
handleStartCanvas,
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderCircle(renderableData, eventData, options) {
|
renderCircle(renderableData, eventData, options) {
|
||||||
const { element } = eventData;
|
const { element } = eventData;
|
||||||
|
|
||||||
@ -220,6 +287,7 @@ const SHORT_HAND_MAP = {
|
|||||||
'Long Axis': 'L ',
|
'Long Axis': 'L ',
|
||||||
AREA: 'Area ',
|
AREA: 'Area ',
|
||||||
Length: '',
|
Length: '',
|
||||||
|
CORNERSTONEFREETEXT: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function _labelToShorthand(label) {
|
function _labelToShorthand(label) {
|
||||||
@ -231,3 +299,67 @@ function _labelToShorthand(label) {
|
|||||||
|
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) {
|
||||||
|
let anchorPoints = [];
|
||||||
|
|
||||||
|
Object.keys(renderableData).forEach(GraphicType => {
|
||||||
|
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
renderableDataForGraphicType.forEach(points => {
|
||||||
|
anchorPoints = [...anchorPoints, ...points];
|
||||||
|
|
||||||
|
// Add other arrow point based on image size.
|
||||||
|
const { image } = eventData;
|
||||||
|
const { rows, columns } = image;
|
||||||
|
|
||||||
|
const xOffset = columns / 10;
|
||||||
|
const yOffset = rows / 10;
|
||||||
|
const point = points[0];
|
||||||
|
|
||||||
|
anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderableDataForGraphicType.forEach(points => {
|
||||||
|
anchorPoints = [...anchorPoints, ...points];
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
renderableDataForGraphicType.forEach(circle => {
|
||||||
|
const { center, radius } = circle;
|
||||||
|
|
||||||
|
anchorPoints.push({ x: center.x + radius, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x - radius, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y + radius });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y - radius });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
renderableDataForGraphicType.forEach(ellipse => {
|
||||||
|
const { corner1, corner2 } = ellipse;
|
||||||
|
|
||||||
|
const halfWidth = Math.abs(corner1.x - corner2.x) / 2;
|
||||||
|
const halfHeight = Math.abs(corner1.y - corner2.y) / 2;
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: (corner1.x + corner2.x) / 2,
|
||||||
|
y: (corner1.y + corner2.y) / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
anchorPoints.push({ x: center.x + halfWidth, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x - halfWidth, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y + halfHeight });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y - halfHeight });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return anchorPoints;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,11 +14,6 @@ export default function addMeasurement(
|
|||||||
imageId,
|
imageId,
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
) {
|
) {
|
||||||
console.log('== ADD MEASUREMENT TO CST ==');
|
|
||||||
console.log(measurement);
|
|
||||||
console.log(imageId);
|
|
||||||
console.log('============================');
|
|
||||||
|
|
||||||
// TODO -> Render rotated ellipse .
|
// TODO -> Render rotated ellipse .
|
||||||
|
|
||||||
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@ -2679,6 +2679,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" ">= 8"
|
"@types/node" ">= 8"
|
||||||
|
|
||||||
|
"@ohif/extension-cornerstone@^2.4.0":
|
||||||
|
version "2.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.8.2.tgz#b8d95610b91eb43c05f84adcdd699183918a57ff"
|
||||||
|
integrity sha512-fIkPPQdDCkoVHCYTUFB4UhmdL9VJRyYawa6FxXiKXjj9OcUsrkXj+BORYgzcEkLWTLmrB+66qtIqpQ7FyXlzJQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
classnames "^2.2.6"
|
||||||
|
lodash.merge "^4.6.2"
|
||||||
|
lodash.throttle "^4.1.1"
|
||||||
|
react-cornerstone-viewport "2.3.9"
|
||||||
|
|
||||||
"@ohif/extension-lesion-tracker@^0.2.0":
|
"@ohif/extension-lesion-tracker@^0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f"
|
resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f"
|
||||||
@ -17889,6 +17900,16 @@ react-cornerstone-viewport@2.3.8:
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-resize-detector "^4.2.1"
|
react-resize-detector "^4.2.1"
|
||||||
|
|
||||||
|
react-cornerstone-viewport@2.3.9:
|
||||||
|
version "2.3.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.9.tgz#f9761da8e536f0a217137c6ca1a983f5882249f9"
|
||||||
|
integrity sha512-qrhq8CbX/jq6b93cQjV2qC/mhHOvFBpxzxcHlvHzEQZt/rmRMcaYxCqjpNaNbUmmBu61wNkxesUVsggkPTTcqg==
|
||||||
|
dependencies:
|
||||||
|
classnames "^2.2.6"
|
||||||
|
date-fns "^2.2.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-resize-detector "^4.2.1"
|
||||||
|
|
||||||
react-dates@21.2.1:
|
react-dates@21.2.1:
|
||||||
version "21.2.1"
|
version "21.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8"
|
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user