diff --git a/package-lock.json b/package-lock.json index d502d8d9..58c77a65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dicom-microscopy-viewer", - "version": "0.10.2", + "version": "0.21.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3656,7 +3656,7 @@ }, "semver": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-2.3.2.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-2.3.2.tgz", "integrity": "sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=", "dev": true }, diff --git a/package.json b/package.json index edf2d79c..875d246d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dicom-microscopy-viewer", - "version": "0.20.1", + "version": "0.21.0", "description": "Interactive web-based viewer for DICOM Microscopy Images", "main": "build/dicom-microscopy-viewer.js", "scripts": { diff --git a/src/roi.js b/src/roi.js index b31dba17..4ed7b513 100644 --- a/src/roi.js +++ b/src/roi.js @@ -4,6 +4,10 @@ const _uid = Symbol('uid'); const _scoord3d = Symbol('scoord3d'); const _properties = Symbol('properties'); +const _name = Symbol('name'); +const _value = Symbol('value'); + + /** A region of interest (ROI) * * @class @@ -16,7 +20,7 @@ class ROI { * @param {Object} options - Options for construction of ROI * @param {Scoord3D} options.scoord3d - Spatial 3D coordinates * @param {string} options.uid - Unique idenfifier - * @param {Object} options.properties - Qualititative evaluations + * @param {Object} options.properties - Properties (name-value pairs) */ constructor(options) { if (!('scoord3d' in options)) { @@ -34,8 +38,23 @@ class ROI { this[_uid] = options.uid; } this[_scoord3d] = options.scoord3d; - this[_properties] = options.properties; - // TODO: store SOPInstanceUID, SOPClassUID and FrameNumbers as reference + if ('properties' in options) { + if (!(typeof(options.properties) === 'object')) { + throw new Error('properties of ROI must be an object') + } + this[_properties] = options.properties; + if (this[_properties].evaluations === undefined) { + this[_properties]['evaluations'] = [] + } + if (this[_properties].measurements === undefined) { + this[_properties]['measurements'] = [] + } + } else { + this[_properties] = {}; + this[_properties]['evaluations'] = [] + this[_properties]['measurements'] = [] + } + console.log(this[_properties]) } /** Gets unique identifier of region of interest. @@ -62,6 +81,38 @@ class ROI { return this[_properties]; } + /** Gets measurements of region of interest. + * + * @returns {Object[]} Measurements + */ + get measurements() { + return this[_properties].measurements; + } + + /** Gets qualitative evaluations of region of interest. + * + * @returns {Object[]} QualitativeEvaluations + */ + get evaluations() { + return this[_properties].evaluations; + } + + /** Adds a measurement. + * + * @params {Object} item - NUM content item representing a measurement + */ + addMeasurement(item) { + this[_properties]['measurements'].push(item); + } + + /** Adds a qualitative evaluation. + * + * @params {Object} item - CODE content item representing a qualitative evaluation + */ + addEvaluation(item) { + this[_properties]['evaluations'].push(item); + } + } -export { ROI }; +export { Property, ROI }; diff --git a/src/viewer.js b/src/viewer.js index 7a3b4a8d..28339679 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1106,6 +1106,42 @@ class VolumeImageViewer { return _getROIFromFeature(feature, this[_pyramidMetadata]); } + /** Adds a measurement to a region of interest. + * + * @param {string} uid - Unique identifier of the region of interest + * @param {Object} item - NUM content item representing a measurement + */ + addROIMeasurement(uid, item) { + const meaning = item.ConceptNameCodeSequence[0].CodeMeaning + console.info(`add measurement "${meaning}" to ROI ${uid}`) + this[_features].forEach(feature => { + const id = feature.getId(); + if (id === uid) { + const properties = feature.getProperties(); + properties['measurements'].push(item); + feature.setProperties(properties, true); + } + }) + } + + /** Adds a qualitative evaluation to a region of interest. + * + * @param {string} uid - Unique identifier of the region of interest + * @param {Object} item - CODE content item representing a qualitative evaluation + */ + addROIEvaluation(uid, item) { + const meaning = item.ConceptNameCodeSequence[0].CodeMeaning + console.info(`add qualitative evaluation "${meaning}" to ROI ${uid}`) + this[_features].forEach(feature => { + const id = feature.getId(); + if (id === uid) { + const properties = feature.getProperties(); + properties['evaluations'].push(item); + feature.setProperties(properties, true); + } + }) + } + /** Pops the most recently annotated regions of interest. * * @returns {ROI} Regions of interest. diff --git a/test/api.spec.js b/test/api.spec.js index ecf38641..5b50e028 100644 --- a/test/api.spec.js +++ b/test/api.spec.js @@ -23,7 +23,6 @@ describe('dicomMicroscopyViewer.viewer.VolumeImageViewer', ()=> { }); }) - const properties = {}; const ellipse = new dicomMicroscopyViewer.scoord3d.Ellipse({ coordinates: [ [8.0, 9.2, 0], @@ -102,82 +101,360 @@ describe('dicomMicroscopyViewer.viewer.VolumeImageViewer', ()=> { }) it('should return [] if there is no drawing', () => { - assert.deepEqual(viewer.getAllROIs(), []); + assert.deepEqual(viewer.getAllROIs(), []); + }) + + it('should add property to ROI upon construction', () => { + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point, + properties : { foo: 'bar' } + }); + assert.deepEqual( + roi.properties, + { + foo: 'bar', + measurements: [], + evaluations: [], + } + ); + assert.deepEqual(roi.measurements, []) + assert.deepEqual(roi.evaluations, []) + }) + + it('should add evaluation to ROI upon construction', () => { + const evaluation = { + ConceptNameCodeSequence: [{ + CodeValue: '121071', + CodeMeaning: 'Finding', + CodingSchemeDesignator: 'DCM', + }], + ConceptCodeSequence: [{ + CodeValue: '108369006', + CodingSchemeDesignator: 'SCT', + CodeMeaning: 'Tumor' + }], + ValueType: 'CODE', + } + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point, + properties : { + 'evaluations': [evaluation] + } + }); + assert.deepEqual( + roi.properties, + { + 'measurements': [], + 'evaluations': [evaluation] + } + ); + assert.deepEqual(roi.measurements, []) + assert.deepEqual(roi.evaluations, [evaluation]) + }) + + it('should add evaluation to ROI after construction', () => { + const evaluation = { + ConceptNameCodeSequence: [{ + CodeValue: '121071', + CodeMeaning: 'Finding', + CodingSchemeDesignator: 'DCM', + }], + ConceptCodeSequence: [{ + CodeValue: '108369006', + CodingSchemeDesignator: 'SCT', + CodeMeaning: 'Tumor' + }], + ValueType: 'CODE', + } + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point + }); + assert.deepEqual( + roi.properties, + { + 'measurements': [], + 'evaluations': [] + } + ); + assert.deepEqual(roi.measurements, []) + assert.deepEqual(roi.evaluations, []) + roi.addEvaluation(evaluation) + assert.deepEqual( + roi.properties, + { + 'measurements': [], + 'evaluations': [evaluation] + } + ); + assert.deepEqual(roi.measurements, []) + assert.deepEqual(roi.evaluations, [evaluation]) + }) + + it('should add measurement to ROI upon construction', () => { + const measurement = { + ConceptNameCodeSequence: [{ + CodeValue: '410668003', + CodeMeaning: 'Length', + CodingSchemeDesignator: 'DCM', + }], + MeasuredValueSequence: [{ + MeasurementUnitsCodeSequence: [{ + CodeValue: 'mm', + CodeMeaning: 'millimeter', + CodingSchemeDesignator: 'UCUM', + }], + NumericValue: 5 + }], + ValueType: 'CODE', + } + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point, + properties : { measurements: [measurement] } + }); + assert.deepEqual( + roi.properties, + { + 'measurements': [measurement], + 'evaluations': [], + } + ); + assert.deepEqual(roi.measurements, [measurement]) + assert.deepEqual(roi.evaluations, []) + }) + + it('should add measurement to ROI after construction', () => { + const measurement = { + ConceptNameCodeSequence: [{ + CodeValue: '410668003', + CodeMeaning: 'Length', + CodingSchemeDesignator: 'DCM', + }], + MeasuredValueSequence: [{ + MeasurementUnitsCodeSequence: [{ + CodeValue: 'mm', + CodeMeaning: 'millimeter', + CodingSchemeDesignator: 'UCUM', + }], + NumericValue: 5 + }], + ValueType: 'CODE', + } + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point + }); + assert.deepEqual( + roi.properties, + { + 'measurements': [], + 'evaluations': [], + } + ); + assert.deepEqual(roi.measurements, []) + assert.deepEqual(roi.evaluations, []) + roi.addMeasurement(measurement) + assert.deepEqual( + roi.properties, + { + 'measurements': [measurement], + 'evaluations': [], + } + ); + assert.deepEqual(roi.measurements, [measurement]) + assert.deepEqual(roi.evaluations, []) }) it('should create a Point ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : point, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, point.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + point.graphicData + ); + assert.deepEqual( + viewer.getROI(roi.uid).properties, + { + 'measurements': [], + 'evaluations': [], + } + ); + assert.deepEqual( + viewer.getROI(roi.uid).measurements, + [] + ); + assert.deepEqual( + viewer.getROI(roi.uid).evaluations, + [] + ); }) it('should create a Box ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : box, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, box.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : box, + properties : { 'foo': 'bar' } + }); + viewer.addROI(roi); + const measurement = { + ConceptNameCodeSequence: [{ + CodeValue: '410668003', + CodeMeaning: 'Length', + CodingSchemeDesignator: 'DCM', + }], + MeasuredValueSequence: [{ + MeasurementUnitsCodeSequence: [{ + CodeValue: 'mm', + CodeMeaning: 'millimeter', + CodingSchemeDesignator: 'UCUM', + }], + NumericValue: 5 + }], + ValueType: 'CODE', + } + viewer.addROIMeasurement(roi.uid, measurement) + const evaluation = { + ConceptNameCodeSequence: [{ + CodeValue: '121071', + CodeMeaning: 'Finding', + CodingSchemeDesignator: 'DCM', + }], + ConceptCodeSequence: [{ + CodeValue: '108369006', + CodingSchemeDesignator: 'SCT', + CodeMeaning: 'Tumor' + }], + ValueType: 'CODE', + } + viewer.addROIEvaluation(roi.uid, evaluation) + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + box.graphicData + ); + assert.deepEqual( + viewer.getROI(roi.uid).properties, + { + 'foo': 'bar', + 'measurements': [measurement], + 'evaluations': [evaluation], + } + ); + assert.deepEqual( + viewer.getROI(roi.uid).measurements, + [measurement] + ); + assert.deepEqual( + viewer.getROI(roi.uid).evaluations, + [evaluation] + ); }) it('should create a Polygon ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : polygon, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, polygon.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : polygon + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + polygon.graphicData + ); }) it('should create a Freehand Polygon ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : freehandPolygon, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, freehandPolygon.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : freehandPolygon + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + freehandPolygon.graphicData + ); }) it('should create a Line ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : line, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, line.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : line + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + line.graphicData + ); }) it('should create a FreehandLine ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : freeHandLine, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData, freeHandLine.graphicData); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : freeHandLine + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData, + freeHandLine.graphicData + ); }) it('should return all ROIs created up to now', () => { - const rois = viewer.getAllROIs(); - assert.equal(rois.length, 6) + const rois = viewer.getAllROIs(); + assert.equal(rois.length, 6) }) it('should be able to remove the point ROI', () => { - let rois = viewer.getAllROIs(); - assert.equal(rois.length, 6); - assert.deepEqual(viewer.getROI(rois[0].uid).scoord3d.graphicData, point.graphicData); - viewer.removeROI(rois[0].uid); - rois = viewer.getAllROIs(); - assert.equal(rois.length, 5); - assert.deepEqual(viewer.getROI(rois[0].uid).scoord3d.graphicData, box.graphicData); + let rois = viewer.getAllROIs(); + assert.equal(rois.length, 6); + assert.deepEqual( + viewer.getROI(rois[0].uid).scoord3d.graphicData, + point.graphicData + ); + viewer.removeROI(rois[0].uid); + rois = viewer.getAllROIs(); + assert.equal(rois.length, 5); + assert.deepEqual( + viewer.getROI(rois[0].uid).scoord3d.graphicData, + box.graphicData + ); }) it('should create an Ellipse ROI and return it back successfuly', () => { - const roi = new dicomMicroscopyViewer.roi.ROI({scoord3d : ellipse, properties}); - viewer.addROI(roi); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData[0], ellipse.graphicData[0]); - assert.deepEqual(viewer.getROI(roi.uid).scoord3d.graphicData[1], ellipse.graphicData[1]); + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : ellipse + }); + viewer.addROI(roi); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData[0], + ellipse.graphicData[0] + ); + assert.deepEqual( + viewer.getROI(roi.uid).scoord3d.graphicData[1], + ellipse.graphicData[1] + ); }) it('should remove all ROIs', () => { - viewer.removeAllROIs(); - assert.deepEqual(viewer.getAllROIs(), []); + viewer.removeAllROIs(); + assert.deepEqual(viewer.getAllROIs(), []); }) it('should throw an error if uid of ROI is undefined', () => { - assert.throws( function() { - const roid = new dicomMicroscopyViewer.roi.ROI({scoord3d : point, uid: undefined, properties}) - }, Error ) + assert.throws( function() { + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point, + uid : undefined, + properties : {} + }); + }, + Error + ) }) it('should throw an error if uid of ROI is null', () => { - assert.throws( function() { - const roid = new dicomMicroscopyViewer.roi.ROI({scoord3d : point, uid: null, properties}) - }, Error ) + assert.throws( function() { + const roi = new dicomMicroscopyViewer.roi.ROI({ + scoord3d : point, + uid : null, + properties : {} + }); + }, + Error + ) }) });