diff --git a/extensions/dicom-html/package.json b/extensions/dicom-html/package.json index eca2b16a3..a56b3fd5f 100644 --- a/extensions/dicom-html/package.json +++ b/extensions/dicom-html/package.json @@ -28,7 +28,7 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "prop-types": "^15.6.2", "react": "^16.11.0", "react-dom": "^16.11.0" diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index 323ea25d8..30cc11a22 100644 --- a/extensions/dicom-rt/package.json +++ b/extensions/dicom-rt/package.json @@ -31,7 +31,7 @@ "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-tools": "4.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "prop-types": "^15.6.2", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index 9306becf7..b09a4d289 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -31,7 +31,7 @@ "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", "cornerstone-tools": "4.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "prop-types": "^15.6.2", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index 731b7d39c..99e378bd9 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -35,7 +35,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index 265209160..e0d79765b 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -26,12 +26,16 @@ const CodeNameCodeSequenceValues = { ImageLibraryGroup: '126200', TrackingUniqueIdentifier: '112040', TrackingIdentifier: '112039', + Finding: '121071', }; const RELATIONSHIP_TYPE = { INFERRED_FROM: 'INFERRED FROM', }; +const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4'; +const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; + /** * Basic SOPClassHandler: * - For all Image types that are stackable, create @@ -68,8 +72,9 @@ function _getDisplaySetsFromSeries( const { ConceptNameCodeSequence, ContentSequence } = instance; if ( + !ConceptNameCodeSequence || ConceptNameCodeSequence.CodeValue !== - CodeNameCodeSequenceValues.ImagingMeasurementReport + CodeNameCodeSequenceValues.ImagingMeasurementReport ) { console.warn( 'Only support Imaging Measurement Report SRs (TID1500) for now' @@ -416,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { CodeNameCodeSequenceValues.TrackingIdentifier ); + const Findings = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.Finding + ); + const measurement = { loaded: false, labels: [], @@ -424,6 +435,23 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { 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 => { const { ConceptNameCodeSequence, @@ -495,7 +523,16 @@ function _getLabelFromMeasuredValueSequence( const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; - return { label: CodeMeaning, value: `${NumericValue} ${CodeValue}` }; // E.g. Long Axis: 31.0 mm + debugger; + + const formatedNumericValue = NumericValue + ? Number(NumericValue).toFixed(2) + : ''; + + return { + label: CodeMeaning, + value: `${formatedNumericValue} ${CodeValue}`, + }; // E.g. Long Axis: 31.0 mm } function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index f0dc64b4e..4efca058e 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -1,4 +1,5 @@ import { importInternal, getToolState, toolColors } from 'cornerstone-tools'; +import { pixelToCanvas } from 'cornerstone-core'; import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; @@ -10,8 +11,10 @@ const drawJoinedLines = importInternal('drawing/drawJoinedLines'); const drawCircle = importInternal('drawing/drawCircle'); const drawEllipse = importInternal('drawing/drawEllipse'); const drawHandles = importInternal('drawing/drawHandles'); +const drawArrow = importInternal('drawing/drawArrow'); const getNewContext = importInternal('drawing/getNewContext'); const BaseTool = importInternal('base/BaseTool'); +const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox'); /** * @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay). @@ -59,18 +62,20 @@ export default class DICOMSRDisplayTool extends BaseTool { trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier) ); + let shouldRepositionTextBoxes = false; + for (let i = 0; i < filteredToolData.length; i++) { const data = filteredToolData[i]; - const { renderableData } = data; + const { renderableData, labels } = data; const color = data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier ? toolColors.getActiveColor() : toolColors.getToolColor(); - + const lineWidth = 2; const options = { color, - lineWidth: 2, + lineWidth, handleRadius: 6, }; @@ -79,8 +84,10 @@ export default class DICOMSRDisplayTool extends BaseTool { switch (GraphicType) { case SCOORD_TYPES.POINT: + this.renderPoint(renderableDataForGraphicType, eventData, options); + break; case SCOORD_TYPES.MULTIPOINT: - this.renderPointOrMultipoint( + this.renderMultipoint( renderableDataForGraphicType, eventData, options @@ -105,9 +112,84 @@ export default class DICOMSRDisplayTool extends BaseTool { break; } }); + + const { element } = eventData; + const context = getNewContext(eventData.canvasContext.canvas); + + 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); + + function textBoxAnchorPoints() { + return data.handles.textBox.anchorPoints; + } + + draw(context, context => { + drawLinkedTextBox( + context, + element, + data.handles.textBox, + text, + data.handles, + textBoxAnchorPoints, + color, + lineWidth, + 0, + true + ); + }); } + + // TOOD -> text boxes may overlap with other annotations at the moment. + // To be fixed after we get requirements. + // if (shouldRepositionTextBoxes) { + // this.repositionTextBox(filteredToolData, eventData); + // } } + // repositionTextBox(toolData, eventData) { + // const toolBoundingBoxes = []; + + // for (let i = 0; i < toolData.length; i++) { + // const toolDataI = toolData[i]; + + // const { textBox } = toolDataI.handles; + // const { anchorPoints } = textBox; + + // const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints); + // // Get the textbox bounding locations. + // // Get the tool extents. + // } + // } + renderPolyLine(renderableData, eventData, options) { const { element } = eventData; const context = getNewContext(eventData.canvasContext.canvas); @@ -119,7 +201,7 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } - renderPointOrMultipoint(renderableData, eventData, options) { + renderMultipoint(renderableData, eventData, options) { const context = getNewContext(eventData.canvasContext.canvas); renderableData.forEach(points => { @@ -129,6 +211,41 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } + renderPoint(renderableData, eventData, options) { + // Render single point as an arrow. + 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; + + 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, + }); + + drawArrow( + context, + handleEndCanvas, + handleStartCanvas, + color, + lineWidth, + false + ); + }); + }); + } + renderCircle(renderableData, eventData, options) { const { element } = eventData; @@ -161,3 +278,123 @@ export default class DICOMSRDisplayTool extends BaseTool { }); } } + +function _getTextBoxLinesFromLabels(labels) { + // TODO -> max 2 for now, need a generic solution for this! + + const labelLength = Math.min(labels.length, 2); + + const lines = []; + + for (let i = 0; i < labelLength; i++) { + const labelEntry = labels[i]; + lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`); + } + + return lines; +} + +const SHORT_HAND_MAP = { + 'Short Axis': 'S ', + 'Long Axis': 'L ', + AREA: 'Area ', + Length: '', + CORNERSTONEFREETEXT: '', +}; + +function _labelToShorthand(label) { + const shortHand = SHORT_HAND_MAP[label]; + + if (shortHand !== undefined) { + return shortHand; + } + + 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; +} + +function _getBoundingBoxFromAnchorPoints(anchorPoints) { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + anchorPoints.forEach(point => { + const { x, y } = point; + + if (x > maxX) { + maxX = x; + } else if (x < minX) { + minX = x; + } + + if (y > maxX) { + maxY = y; + } else if (y < minY) { + minY = y; + } + }); +} diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index f3d28c317..b7ea21440 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -14,11 +14,6 @@ export default function addMeasurement( imageId, displaySetInstanceUID ) { - console.log('== ADD MEASUREMENT TO CST =='); - console.log(measurement); - console.log(imageId); - console.log('============================'); - // TODO -> Render rotated ellipse . const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; @@ -26,6 +21,7 @@ export default function addMeasurement( const measurementData = { TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier, renderableData: {}, + labels: measurement.labels, }; measurement.coords.forEach(coord => { diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 1ed3dbb5b..6b39b6eae 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -29,7 +29,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-tools": "4.16.1", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 71c59eaf3..a69e92835 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -65,6 +65,7 @@ const machineConfiguration = { cond: 'isNewSeries', }, ], + UNTRACK_SERIES: [ { target: 'tracking', diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 3e33aa678..d5e309444 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -33,7 +33,7 @@ "@ohif/ui": "^2.0.0", "cornerstone-core": "^2.3.0", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicom-parser": "^1.8.3", "i18next": "^17.0.3", "i18next-browser-languagedetector": "^3.0.1", diff --git a/platform/core/package.json b/platform/core/package.json index 3a16b5b95..239876138 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -38,7 +38,7 @@ "dependencies": { "@babel/runtime": "7.7.6", "ajv": "^6.10.0", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicomweb-client": "^0.6.0", "immer": "6.0.2", "isomorphic-base64": "^1.0.2", diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 629e5e304..c79f11e3a 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -69,7 +69,7 @@ "cornerstone-math": "^0.1.8", "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", - "dcmjs": "0.14.0", + "dcmjs": "0.14.1", "dicom-parser": "^1.8.3", "dicomweb-client": "^0.4.4", "dotenv-webpack": "^1.7.0", diff --git a/yarn.lock b/yarn.lock index ee59610f2..19aeaf498 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,6 +2679,17 @@ dependencies: "@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": version "0.2.0" resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f" @@ -7353,10 +7364,10 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dcmjs@0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c" - integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w== +dcmjs@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.1.tgz#4a4f18d1e5332d38429e2d77340a6ad0052ec71e" + integrity sha512-GGGQt4zjX7Fp0rLO5rcIQgmxZdLsEwBSZ1T74iXeJG5W32P1yiodJfBW900tNZCKj3zWamO3f5jDC1Xkv81fdg== dependencies: "@babel/polyfill" "^7.8.3" "@babel/runtime" "^7.8.4" @@ -17889,6 +17900,16 @@ react-cornerstone-viewport@2.3.8: prop-types "^15.7.2" 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: version "21.2.1" resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8"