diff --git a/src/adapters/Cornerstone/Angle.js b/src/adapters/Cornerstone/Angle.js new file mode 100644 index 00000000..f1c3c100 --- /dev/null +++ b/src/adapters/Cornerstone/Angle.js @@ -0,0 +1,96 @@ +import MeasurementReport from "./MeasurementReport.js"; +import TID300CobbAngle from "../../utilities/TID300/CobbAngle.js"; +import CORNERSTONE_4_TAG from "./cornerstone4Tag"; + +const ANGLE = "Angle"; + +class Angle { + constructor() {} + + /** + * Generate TID300 measurement data for a plane angle measurement - use a CobbAngle, but label it as Angle + * @param MeasurementGroup + * @returns + */ + static getMeasurementData(MeasurementGroup) { + const { + defaultState, + NUMGroup, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + + const state = { + ...defaultState, + rAngle: NUMGroup.MeasuredValueSequence.NumericValue, + toolType: Angle.toolType, + handles: { + start: {}, + middle: {}, + end: {}, + textBox: { + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + } + } + }; + + [ + state.handles.start.x, + state.handles.start.y, + state.handles.middle.x, + state.handles.middle.y, + state.handles.middle.x, + state.handles.middle.y, + state.handles.end.x, + state.handles.end.y + ] = SCOORDGroup.GraphicData; + + return state; + } + + static getTID300RepresentationArguments(tool) { + const { handles, finding, findingSites } = tool; + const point1 = handles.start; + const point2 = handles.middle; + const point3 = handles.middle; + const point4 = handles.end; + const rAngle = tool.rAngle; + + const trackingIdentifierTextValue = "cornerstoneTools@^4.0.0:Angle"; + + return { + point1, + point2, + point3, + point4, + rAngle, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] + }; + } +} + +Angle.toolType = ANGLE; +Angle.utilityToolType = ANGLE; +Angle.TID300Representation = TID300CobbAngle; +Angle.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone4Tag !== CORNERSTONE_4_TAG) { + return false; + } + + return toolType === ANGLE; +}; + +MeasurementReport.registerTool(Angle); + +export default Angle; diff --git a/src/adapters/Cornerstone/ArrowAnnotate.js b/src/adapters/Cornerstone/ArrowAnnotate.js index bccb7084..9dd78a5a 100644 --- a/src/adapters/Cornerstone/ArrowAnnotate.js +++ b/src/adapters/Cornerstone/ArrowAnnotate.js @@ -1,48 +1,26 @@ import MeasurementReport from "./MeasurementReport.js"; import TID300Point from "../../utilities/TID300/Point.js"; import CORNERSTONE_4_TAG from "./cornerstone4Tag"; -import { toArray } from "../helpers.js"; const ARROW_ANNOTATE = "ArrowAnnotate"; -const FINDING = "121071"; -const FINDING_SITE = "G-C0E3"; const CORNERSTONEFREETEXT = "CORNERSTONEFREETEXT"; class ArrowAnnotate { constructor() {} - // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData(MeasurementGroup) { - const { ContentSequence } = MeasurementGroup; - - const NUMGroup = toArray(ContentSequence).find( - group => group.ValueType === "NUM" - ); - - const SCOORDGroup = toArray(NUMGroup.ContentSequence).find( - group => group.ValueType === "SCOORD" - ); - - const findingGroup = toArray(ContentSequence).find( - group => group.ConceptNameCodeSequence.CodeValue === FINDING - ); - - const findingSiteGroups = toArray(ContentSequence).filter( - group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE - ); + const { + defaultState, + SCOORDGroup, + findingGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); const text = findingGroup.ConceptCodeSequence.CodeMeaning; const { GraphicData } = SCOORDGroup; - const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; - const { - ReferencedSOPInstanceUID, - ReferencedFrameNumber - } = ReferencedSOPSequence; const state = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 0, + ...defaultState, toolType: ArrowAnnotate.toolType, active: false, handles: { @@ -52,11 +30,17 @@ class ArrowAnnotate { highlight: true, active: false }, - // TODO: How do we choose where the end goes? - // Just put it pointing from the bottom right for now? + // Use a generic offset if the stored data doesn't have the endpoint, otherwise + // use the actual endpoint. end: { - x: GraphicData[0] + 20, - y: GraphicData[1] + 20, + x: + GraphicData.length == 4 + ? GraphicData[2] + : GraphicData[0] + 20, + y: + GraphicData.length == 4 + ? GraphicData[3] + : GraphicData[1] + 20, highlight: true, active: false }, @@ -70,20 +54,14 @@ class ArrowAnnotate { }, invalidated: true, text, - visible: true, - finding: findingGroup - ? findingGroup.ConceptCodeSequence - : undefined, - findingSites: findingSiteGroups.map(fsg => { - return { ...fsg.ConceptCodeSequence }; - }) + visible: true }; return state; } static getTID300RepresentationArguments(tool) { - const points = [tool.handles.start]; + const points = [tool.handles.start, tool.handles.end]; let { finding, findingSites } = tool; diff --git a/src/adapters/Cornerstone/CobbAngle.js b/src/adapters/Cornerstone/CobbAngle.js new file mode 100644 index 00000000..237142b6 --- /dev/null +++ b/src/adapters/Cornerstone/CobbAngle.js @@ -0,0 +1,99 @@ +import MeasurementReport from "./MeasurementReport.js"; +import TID300CobbAngle from "../../utilities/TID300/CobbAngle.js"; +import CORNERSTONE_4_TAG from "./cornerstone4Tag"; + +const COBB_ANGLE = "CobbAngle"; + +class CobbAngle { + constructor() {} + + // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. + static getMeasurementData(MeasurementGroup) { + const { + defaultState, + NUMGroup, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + + const state = { + ...defaultState, + rAngle: NUMGroup.MeasuredValueSequence.NumericValue, + toolType: CobbAngle.toolType, + handles: { + start: {}, + end: {}, + start2: { + highlight: true, + drawnIndependently: true + }, + end2: { + highlight: true, + drawnIndependently: true + }, + textBox: { + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + } + } + }; + + [ + state.handles.start.x, + state.handles.start.y, + state.handles.end.x, + state.handles.end.y, + state.handles.start2.x, + state.handles.start2.y, + state.handles.end2.x, + state.handles.end2.y + ] = SCOORDGroup.GraphicData; + + return state; + } + + static getTID300RepresentationArguments(tool) { + const { handles, finding, findingSites } = tool; + const point1 = handles.start; + const point2 = handles.end; + const point3 = handles.start2; + const point4 = handles.end2; + const rAngle = tool.rAngle; + + const trackingIdentifierTextValue = "cornerstoneTools@^4.0.0:CobbAngle"; + + return { + point1, + point2, + point3, + point4, + rAngle, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] + }; + } +} + +CobbAngle.toolType = COBB_ANGLE; +CobbAngle.utilityToolType = COBB_ANGLE; +CobbAngle.TID300Representation = TID300CobbAngle; +CobbAngle.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone4Tag !== CORNERSTONE_4_TAG) { + return false; + } + + return toolType === COBB_ANGLE; +}; + +MeasurementReport.registerTool(CobbAngle); + +export default CobbAngle; diff --git a/src/adapters/Cornerstone/EllipticalRoi.js b/src/adapters/Cornerstone/EllipticalRoi.js index c0e3c6b9..c6280422 100644 --- a/src/adapters/Cornerstone/EllipticalRoi.js +++ b/src/adapters/Cornerstone/EllipticalRoi.js @@ -12,23 +12,11 @@ class EllipticalRoi { // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData(MeasurementGroup) { - const { ContentSequence } = MeasurementGroup; - - const findingGroup = toArray(ContentSequence).find( - group => group.ConceptNameCodeSequence.CodeValue === FINDING - ); - - const findingSiteGroups = toArray(ContentSequence).filter( - group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE - ); - - const NUMGroup = toArray(ContentSequence).find( - group => group.ValueType === "NUM" - ); - - const SCOORDGroup = toArray(NUMGroup.ContentSequence).find( - group => group.ValueType === "SCOORD" - ); + const { + defaultState, + NUMGroup, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); const { GraphicData } = SCOORDGroup; @@ -66,14 +54,8 @@ class EllipticalRoi { x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength, y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength }; - const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; - const { - ReferencedSOPInstanceUID, - ReferencedFrameNumber - } = ReferencedSOPSequence; const state = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 0, + ...defaultState, toolType: EllipticalRoi.toolType, active: false, cachedStats: { @@ -102,13 +84,7 @@ class EllipticalRoi { } }, invalidated: true, - visible: true, - finding: findingGroup - ? findingGroup.ConceptCodeSequence - : undefined, - findingSites: findingSiteGroups.map(fsg => { - return { ...fsg.ConceptCodeSequence }; - }) + visible: true }; return state; diff --git a/src/adapters/Cornerstone/Freehand.js b/src/adapters/Cornerstone/Freehand.js deleted file mode 100644 index 4c695662..00000000 --- a/src/adapters/Cornerstone/Freehand.js +++ /dev/null @@ -1,49 +0,0 @@ -import MeasurementReport from "./MeasurementReport"; -import TID300Polyline from "../../utilities/TID300/Polyline"; -import CORNERSTONE_4_TAG from "./cornerstone4Tag"; - -class Freehand { - constructor() {} - - static measurementContentToLengthState(groupItemContent) { - const content = groupItemContent.ContentSequence; - const { ReferencedSOPSequence } = content.ContentSequence; - const { - ReferencedSOPInstanceUID, - ReferencedFrameNumber - } = ReferencedSOPSequence; - const state = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 0, - toolType: Freehand.toolType - }; - - // TODO: To be implemented! - // Needs to add points, lengths - - return state; - } - - // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. - static getMeasurementData(measurementContent) { - return measurementContent.map(Freehand.measurementContentToLengthState); - } - - static getTID300RepresentationArguments(/*tool*/) { - // TO BE IMPLEMENTED - return { - /*points, lengths*/ - }; - } -} - -Freehand.toolType = "Freehand"; -Freehand.utilityToolType = "Freehand"; -Freehand.TID300Representation = TID300Polyline; -Freehand.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - return false; // TODO -}; - -MeasurementReport.registerTool(Freehand); - -export default Freehand; diff --git a/src/adapters/Cornerstone/FreehandRoi.js b/src/adapters/Cornerstone/FreehandRoi.js new file mode 100644 index 00000000..6c4f8ddf --- /dev/null +++ b/src/adapters/Cornerstone/FreehandRoi.js @@ -0,0 +1,80 @@ +import MeasurementReport from "./MeasurementReport"; +import TID300Polyline from "../../utilities/TID300/Polyline"; +import CORNERSTONE_4_TAG from "./cornerstone4Tag"; + +class FreehandRoi { + constructor() {} + + static getMeasurementData(MeasurementGroup) { + const { + defaultState, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + + const state = { + ...defaultState, + toolType: FreehandRoi.toolType, + handles: { + points: [], + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + } + }, + color: undefined, + invalidated: true + }; + const { GraphicData } = SCOORDGroup; + for (let i = 0; i < GraphicData.length; i += 2) { + state.handles.points.push({ + x: GraphicData[i], + y: GraphicData[i + 1] + }); + } + + return state; + } + + static getTID300RepresentationArguments(/*tool*/) { + const { handles, finding, findingSites, cachedStats } = tool; + const { points } = handles; + const { area = 0, perimeter = 0 } = cachedStats; + + const trackingIdentifierTextValue = + "cornerstoneTools@^4.0.0:FreehandRoi"; + + return { + points, + area, + perimeter, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] + }; + } +} + +FreehandRoi.toolType = "FreehandRoi"; +FreehandRoi.utilityToolType = "FreehandRoi"; +FreehandRoi.TID300Representation = TID300Polyline; +FreehandRoi.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone4Tag !== CORNERSTONE_4_TAG) { + return false; + } + + return toolType === FreehandRoi.toolType; +}; + +MeasurementReport.registerTool(FreehandRoi); + +export default FreehandRoi; diff --git a/src/adapters/Cornerstone/Length.js b/src/adapters/Cornerstone/Length.js index 517e5602..7fe9b0c6 100644 --- a/src/adapters/Cornerstone/Length.js +++ b/src/adapters/Cornerstone/Length.js @@ -12,32 +12,14 @@ class Length { // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData(MeasurementGroup) { - const { ContentSequence } = MeasurementGroup; - - const findingGroup = toArray(ContentSequence).find( - group => group.ConceptNameCodeSequence.CodeValue === FINDING - ); - - const findingSiteGroups = toArray(ContentSequence).filter( - group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE - ); - - const NUMGroup = toArray(ContentSequence).find( - group => group.ValueType === "NUM" - ); - - const SCOORDGroup = toArray(NUMGroup.ContentSequence).find( - group => group.ValueType === "SCOORD" - ); - - const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; const { - ReferencedSOPInstanceUID, - ReferencedFrameNumber - } = ReferencedSOPSequence; - const lengthState = { - sopInstanceUid: ReferencedSOPInstanceUID, - frameIndex: ReferencedFrameNumber || 1, + defaultState, + NUMGroup, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + + const state = { + ...defaultState, length: NUMGroup.MeasuredValueSequence.NumericValue, toolType: Length.toolType, handles: { @@ -50,23 +32,17 @@ class Length { allowedOutsideImage: true, hasBoundingBox: true } - }, - finding: findingGroup - ? findingGroup.ConceptCodeSequence - : undefined, - findingSites: findingSiteGroups.map(fsg => { - return { ...fsg.ConceptCodeSequence }; - }) + } }; [ - lengthState.handles.start.x, - lengthState.handles.start.y, - lengthState.handles.end.x, - lengthState.handles.end.y + state.handles.start.x, + state.handles.start.y, + state.handles.end.x, + state.handles.end.y ] = SCOORDGroup.GraphicData; - return lengthState; + return state; } static getTID300RepresentationArguments(tool) { diff --git a/src/adapters/Cornerstone/MeasurementReport.js b/src/adapters/Cornerstone/MeasurementReport.js index d20afc60..6ef35ec5 100644 --- a/src/adapters/Cornerstone/MeasurementReport.js +++ b/src/adapters/Cornerstone/MeasurementReport.js @@ -6,6 +6,23 @@ import TID1501MeasurementGroup from "../../utilities/TID1500/TID1501MeasurementG import { toArray, codeMeaningEquals } from "../helpers.js"; +const FINDING = { CodingSchemeDesignator: "DCM", CodeValue: "121071" }; +const FINDING_SITE = { CodingSchemeDesignator: "SCT", CodeValue: "363698007" }; +const FINDING_SITE_OLD = { CodingSchemeDesignator: "SRT", CodeValue: "G-C0E3" }; + +const codeValueMatch = (group, code, oldCode) => { + const { ConceptNameCodeSequence } = group; + if (!ConceptNameCodeSequence) return; + const { CodingSchemeDesignator, CodeValue } = ConceptNameCodeSequence; + return ( + (CodingSchemeDesignator == code.CodingSchemeDesignator && + CodeValue == code.CodeValue) || + (oldCode && + CodingSchemeDesignator == oldCode.CodingSchemeDesignator && + CodeValue == oldCode.CodeValue) + ); +}; + function getTID300ContentItem( tool, toolType, @@ -50,6 +67,62 @@ function getMeasurementGroup(toolType, toolData, ReferencedSOPSequence) { export default class MeasurementReport { constructor() {} + static getSetupMeasurementData(MeasurementGroup) { + const { ContentSequence } = MeasurementGroup; + + const contentSequenceArr = toArray(ContentSequence); + const findingGroup = contentSequenceArr.find(group => + codeValueMatch(group, FINDING) + ); + const findingSiteGroups = + contentSequenceArr.filter(group => + codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD) + ) || []; + const NUMGroup = contentSequenceArr.find( + group => group.ValueType === "NUM" + ); + const SCOORDGroup = toArray(NUMGroup.ContentSequence).find( + group => group.ValueType === "SCOORD" + ); + const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; + const { + ReferencedSOPInstanceUID, + ReferencedFrameNumber + } = ReferencedSOPSequence; + + const defaultState = { + sopInstanceUid: ReferencedSOPInstanceUID, + frameIndex: ReferencedFrameNumber || 1, + complete: true, + finding: findingGroup + ? findingGroup.ConceptCodeSequence + : undefined, + findingSites: findingSiteGroups.map(fsg => { + return { ...fsg.ConceptCodeSequence }; + }) + }; + if (defaultState.finding) { + defaultState.description = defaultState.finding.CodeMeaning; + } + const findingSite = + defaultState.findingSites && defaultState.findingSites[0]; + if (findingSite) { + defaultState.location = + (findingSite[0] && findingSite[0].CodeMeaning) || + findingSite.CodeMeaning; + } + return { + defaultState, + findingGroup, + findingSiteGroups, + NUMGroup, + SCOORDGroup, + ReferencedSOPSequence, + ReferencedSOPInstanceUID, + ReferencedFrameNumber + }; + } + static generateReport(toolState, metadataProvider, options) { // ToolState for array of imageIDs to a Report // Assume Cornerstone metadata provider has access to Study / Series / Sop Instance UID diff --git a/src/adapters/Cornerstone/RectangleRoi.js b/src/adapters/Cornerstone/RectangleRoi.js new file mode 100644 index 00000000..361435ff --- /dev/null +++ b/src/adapters/Cornerstone/RectangleRoi.js @@ -0,0 +1,93 @@ +import MeasurementReport from "./MeasurementReport"; +import TID300Polyline from "../../utilities/TID300/Polyline"; +import CORNERSTONE_4_TAG from "./cornerstone4Tag"; + +class RectangleRoi { + constructor() {} + + static getMeasurementData(MeasurementGroup) { + const { + defaultState, + SCOORDGroup + } = MeasurementReport.getSetupMeasurementData(MeasurementGroup); + + const state = { + ...defaultState, + toolType: RectangleRoi.toolType, + handles: { + start: {}, + end: {}, + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + }, + initialRotation: 0 + }, + color: undefined, + invalidated: true + }; + const intermediate = {}; + + [ + state.handles.start.x, + state.handles.start.y, + intermediate.x, + intermediate.y, + state.handles.end.x, + state.handles.end.y + ] = SCOORDGroup.GraphicData; + + return state; + } + + static getTID300RepresentationArguments(tool) { + const { finding, findingSites, cachedStats, handles } = tool; + console.log("getTID300 Rectangle", tool, cachedStats, handles); + const { start, end } = handles; + const points = [ + start, + { x: start.x, y: end.y }, + end, + { x: end.x, y: start.y } + ]; + const { area, perimeter } = cachedStats; + + console.log("Point=", points, "cachedStats=", cachedStats); + const trackingIdentifierTextValue = + "cornerstoneTools@^4.0.0:RectangleRoi"; + + return { + points, + area, + perimeter, + trackingIdentifierTextValue, + finding, + findingSites: findingSites || [] + }; + } +} + +RectangleRoi.toolType = "RectangleRoi"; +RectangleRoi.utilityToolType = "RectangleRoi"; +RectangleRoi.TID300Representation = TID300Polyline; +RectangleRoi.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { + if (!TrackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); + + if (cornerstone4Tag !== CORNERSTONE_4_TAG) { + return false; + } + + return toolType === RectangleRoi.toolType; +}; + +MeasurementReport.registerTool(RectangleRoi); + +export default RectangleRoi; diff --git a/src/adapters/Cornerstone/index.js b/src/adapters/Cornerstone/index.js index 505289c6..b1d2bb35 100644 --- a/src/adapters/Cornerstone/index.js +++ b/src/adapters/Cornerstone/index.js @@ -1,19 +1,25 @@ import MeasurementReport from "./MeasurementReport.js"; import Length from "./Length.js"; -import Freehand from "./Freehand.js"; +import FreehandRoi from "./FreehandRoi.js"; import Bidirectional from "./Bidirectional.js"; import EllipticalRoi from "./EllipticalRoi.js"; import ArrowAnnotate from "./ArrowAnnotate.js"; import Segmentation from "./Segmentation.js"; +import CobbAngle from "./CobbAngle"; +import Angle from "./Angle"; +import RectangleRoi from "./RectangleRoi"; const Cornerstone = { Length, - Freehand, + FreehandRoi, Bidirectional, EllipticalRoi, ArrowAnnotate, MeasurementReport, - Segmentation + Segmentation, + CobbAngle, + Angle, + RectangleRoi }; export default Cornerstone; diff --git a/src/utilities/TID300/CobbAngle.js b/src/utilities/TID300/CobbAngle.js new file mode 100644 index 00000000..2b8afbf3 --- /dev/null +++ b/src/utilities/TID300/CobbAngle.js @@ -0,0 +1,56 @@ +import { DicomMetaDictionary } from "../../DicomMetaDictionary.js"; +import TID300Measurement from "./TID300Measurement.js"; + +export default class CobbAngle extends TID300Measurement { + contentItem() { + const { + point1, + point2, + point3, + point4, + rAngle, + ReferencedSOPSequence + } = this.props; + + return this.getMeasurement([ + { + RelationshipType: "CONTAINS", + ValueType: "NUM", + ConceptNameCodeSequence: { + CodeValue: "285285000", + CodingSchemeDesignator: "SCT", + CodeMeaning: "Cobb angle" + }, + MeasuredValueSequence: { + MeasurementUnitsCodeSequence: { + CodeValue: "deg", + CodingSchemeDesignator: "UCUM", + CodingSchemeVersion: "1.4", + CodeMeaning: "\u00B0" + }, + NumericValue: rAngle + }, + ContentSequence: { + RelationshipType: "INFERRED FROM", + ValueType: "SCOORD", + GraphicType: "POLYLINE", + GraphicData: [ + point1.x, + point1.y, + point2.x, + point2.y, + point3.x, + point3.y, + point4.x, + point4.y + ], + ContentSequence: { + RelationshipType: "SELECTED FROM", + ValueType: "IMAGE", + ReferencedSOPSequence + } + } + } + ]); + } +} diff --git a/src/utilities/TID300/Point.js b/src/utilities/TID300/Point.js index 2c7a55df..8e70e047 100644 --- a/src/utilities/TID300/Point.js +++ b/src/utilities/TID300/Point.js @@ -12,7 +12,12 @@ export default class Point extends TID300Measurement { const GraphicData = use3DSpatialCoordinates ? [points[0].x, points[0].y, points[0].z] : [points[0].x, points[0].y]; - + // Allow storing another point as part of an indicator showing a single point + if (points.length == 2) { + GraphicData.push(points[1].x); + GraphicData.push(points[1].y); + if (use3DSpatialCoordinates) GraphicData.push(points[1].z); + } return this.getMeasurement([ { RelationshipType: "CONTAINS", diff --git a/src/utilities/TID300/Polyline.js b/src/utilities/TID300/Polyline.js index b91ac2a0..a8caca91 100644 --- a/src/utilities/TID300/Polyline.js +++ b/src/utilities/TID300/Polyline.js @@ -12,10 +12,10 @@ function expandPoints(points) { const allPoints = []; points.forEach(point => { - allPoints.push(point[0]); - allPoints.push(point[1]); - if (point[2] !== undefined) { - allPoints.push(point[2]); + allPoints.push(point[0] || point.x); + allPoints.push(point[1] || point.y); + if (point[2] !== undefined || point.z !== undefined) { + allPoints.push(point[2] || point.z); } }); @@ -26,27 +26,23 @@ export default class Polyline extends TID300Measurement { contentItem() { const { points, + area, ReferencedSOPSequence, - use3DSpatialCoordinates = false + use3DSpatialCoordinates = false, + perimeter } = this.props; - // Combine all lengths to save the perimeter - // @ToDO The permiter has to be implemented - // const reducer = (accumulator, currentValue) => accumulator + currentValue; - // const perimeter = lengths.reduce(reducer); - const perimeter = {}; const GraphicData = expandPoints(points); // TODO: Add Mean and STDev value of (modality?) pixels - return this.getMeasurement([ { RelationshipType: "CONTAINS", ValueType: "NUM", ConceptNameCodeSequence: { - CodeValue: "G-A197", - CodingSchemeDesignator: "SRT", - CodeMeaning: "Perimeter" // TODO: Look this up from a Code Meaning dictionary + CodeValue: "131191004", + CodingSchemeDesignator: "SCT", + CodeMeaning: "Perimeter" }, MeasuredValueSequence: { MeasurementUnitsCodeSequence: { @@ -87,7 +83,7 @@ export default class Polyline extends TID300Measurement { CodingSchemeVersion: "1.4", CodeMeaning: "SquareMilliMeter" }, - NumericValue: perimeter + NumericValue: area }, ContentSequence: { RelationshipType: "INFERRED FROM", diff --git a/src/utilities/TID300/TID300Measurement.js b/src/utilities/TID300/TID300Measurement.js index ae4ef1ff..497220d1 100644 --- a/src/utilities/TID300/TID300Measurement.js +++ b/src/utilities/TID300/TID300Measurement.js @@ -82,8 +82,8 @@ export default class TID300Measurement { RelationshipType: "CONTAINS", ValueType: "CODE", ConceptNameCodeSequence: { - CodeValue: "G-C0E3", - CodingSchemeDesignator: "SRT", + CodeValue: "363698007", + CodingSchemeDesignator: "SCT", CodeMeaning: "Finding Site" }, ConceptCodeSequence: { diff --git a/src/utilities/TID300/index.js b/src/utilities/TID300/index.js index 7f965e87..44e8ac1d 100644 --- a/src/utilities/TID300/index.js +++ b/src/utilities/TID300/index.js @@ -1,5 +1,6 @@ import TID300Measurement from "./TID300Measurement.js"; import Length from "./Length.js"; +import CobbAngle from "./CobbAngle"; import Bidirectional from "./Bidirectional.js"; import Polyline from "./Polyline.js"; import Ellipse from "./Ellipse.js"; @@ -63,6 +64,7 @@ import Ellipse from "./Ellipse.js"; const TID300 = { TID300Measurement, Length, + CobbAngle, Bidirectional, Polyline, Ellipse