Skip to content

Commit c90f514

Browse files
committed
Enable selection of bulk annotations
1 parent a74474a commit c90f514

File tree

1 file changed

+180
-34
lines changed

1 file changed

+180
-34
lines changed

src/viewer.js

Lines changed: 180 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -676,14 +676,17 @@ function _getColorPaletteStyleForPointLayer ({
676676
const expression = [
677677
'palette',
678678
indexExpression,
679-
colormap.map(c => rgb2hex(c))
679+
colormap
680680
]
681681

682682
return { color: expression }
683683
}
684684

685685
const _affine = Symbol('affine')
686686
const _affineInverse = Symbol('affineInverse')
687+
const _annotationManager = Symbol('annotationManager')
688+
const _annotationGroups = Symbol('annotationGroups')
689+
const _areIccProfilesFetched = Symbol('areIccProfilesFetched')
687690
const _controls = Symbol('controls')
688691
const _drawingLayer = Symbol('drawingLayer')
689692
const _drawingSource = Symbol('drawingSource')
@@ -693,17 +696,15 @@ const _interactions = Symbol('interactions')
693696
const _map = Symbol('map')
694697
const _mappings = Symbol('mappings')
695698
const _metadata = Symbol('metadata')
696-
const _areIccProfilesFetched = Symbol('areIccProfilesFetched')
699+
const _opticalPaths = Symbol('opticalPaths')
697700
const _options = Symbol('options')
701+
const _overlays = Symbol('overlays')
702+
const _overviewMap = Symbol('overviewMap')
703+
const _projection = Symbol('projection')
698704
const _pyramid = Symbol('pyramid')
699705
const _segments = Symbol('segments')
700-
const _opticalPaths = Symbol('opticalPaths')
701706
const _rotation = Symbol('rotation')
702-
const _projection = Symbol('projection')
703707
const _tileGrid = Symbol('tileGrid')
704-
const _annotationManager = Symbol('annotationManager')
705-
const _annotationGroups = Symbol('annotationGroups')
706-
const _overviewMap = Symbol('overviewMap')
707708
const _updateOverviewMapSize = Symbol('updateOverviewMapSize')
708709

709710
/**
@@ -731,6 +732,10 @@ class VolumeImageViewer {
731732
* turned on (e.g., display of tile boundaries)
732733
* @param {number} [options.tilesCacheSize=1000] - Number of tiles that should
733734
* be cached to avoid repeated retrieval for the DICOMweb server
735+
* @param {number[]} [options.primaryColor=[0, 126, 163]] - Primary color of
736+
* the application
737+
* @param {number[]} [options.highlightColor=[140, 184, 198]] - Color that
738+
* should be used to highlight things that get selected by the user
734739
*/
735740
constructor (options) {
736741
this[_options] = options
@@ -756,6 +761,13 @@ class VolumeImageViewer {
756761
}
757762
this[_options].controls = new Set(this[_options].controls)
758763

764+
if (this[_options].primaryColor == null) {
765+
this[_options].primaryColor = [0, 126, 163]
766+
}
767+
if (this[_options].highlightColor == null) {
768+
this[_options].highlightColor = [140, 184, 198]
769+
}
770+
759771
// Collection of Openlayers "TileLayer" instances
760772
this[_segments] = {}
761773
this[_mappings] = {}
@@ -1473,8 +1485,8 @@ class VolumeImageViewer {
14731485
opticalPath.overviewLayer.setStyle(style)
14741486
} else {
14751487
const styleVariables = {
1476-
windowCenter: windowCenter,
1477-
windowWidth: windowWidth,
1488+
windowCenter,
1489+
windowWidth,
14781490
red: opticalPath.style.color[0],
14791491
green: opticalPath.style.color[1],
14801492
blue: opticalPath.style.color[2]
@@ -2950,7 +2962,108 @@ class VolumeImageViewer {
29502962

29512963
const defaultAnnotationGroupStyle = {
29522964
opacity: 1.0,
2953-
color: [2, 126, 163]
2965+
color: this[_options].primaryColor
2966+
}
2967+
2968+
// We need to bind those variables to constants for the loader function
2969+
const client = this[_options].client
2970+
const pyramid = this[_pyramid].metadata
2971+
const affineInverse = this[_affineInverse]
2972+
const container = this[_map].getTargetElement()
2973+
const _getROIFromFeature = (feature) => {
2974+
const roi = this._getROIFromFeature(
2975+
feature,
2976+
this[_pyramid].metadata,
2977+
this[_affine]
2978+
)
2979+
const annotationGroupUID = feature.get('annotationGroupUID')
2980+
const annotationGroupMetadata = metadata.AnnotationGroupSequence.find(
2981+
item => item.AnnotationGroupUID === annotationGroupUID
2982+
)
2983+
2984+
const findingCategory = (
2985+
annotationGroupMetadata
2986+
.AnnotationPropertyCategoryCodeSequence[0]
2987+
)
2988+
roi.addEvaluation(
2989+
new dcmjs.sr.valueTypes.CodeContentItem({
2990+
name: new dcmjs.sr.coding.CodedConcept({
2991+
value: '276214006',
2992+
meaning: 'Finding category',
2993+
schemeDesignator: 'SCT'
2994+
}),
2995+
value: new dcmjs.sr.coding.CodedConcept({
2996+
value: findingCategory.CodeValue,
2997+
meaning: findingCategory.CodeMeaning,
2998+
schemeDesignator: findingCategory.CodingSchemeDesignator
2999+
}),
3000+
relationshipType: dcmjs.sr.valueTypes.RelationshipTypes.HAS_CONCEPT_MOD
3001+
})
3002+
)
3003+
const findingType = (
3004+
annotationGroupMetadata
3005+
.AnnotationPropertyTypeCodeSequence[0]
3006+
)
3007+
roi.addEvaluation(
3008+
new dcmjs.sr.valueTypes.CodeContentItem({
3009+
name: new dcmjs.sr.coding.CodedConcept({
3010+
value: '121071',
3011+
meaning: 'Finding',
3012+
schemeDesignator: 'DCM'
3013+
}),
3014+
value: new dcmjs.sr.coding.CodedConcept({
3015+
value: findingType.CodeValue,
3016+
meaning: findingType.CodeMeaning,
3017+
schemeDesignator: findingType.CodingSchemeDesignator
3018+
}),
3019+
relationshipType: dcmjs.sr.valueTypes.RelationshipTypes.HAS_CONCEPT_MOD
3020+
})
3021+
)
3022+
3023+
annotationGroupMetadata.MeasurementsSequence.forEach(
3024+
(measurementItem, measurementIndex) => {
3025+
const key = `measurementValue${measurementIndex.toString()}`
3026+
const value = feature.get(key)
3027+
const name = measurementItem.ConceptNameCodeSequence[0]
3028+
const unit = measurementItem.MeasurementUnitsCodeSequence[0]
3029+
3030+
const measurement = new dcmjs.sr.valueTypes.NumContentItem({
3031+
value: Number(value),
3032+
name: new dcmjs.sr.coding.CodedConcept({
3033+
value: name.CodeValue,
3034+
meaning: name.CodeMeaning,
3035+
schemeDesignator: name.CodingSchemeDesignator
3036+
}),
3037+
unit: new dcmjs.sr.coding.CodedConcept({
3038+
value: unit.CodeValue,
3039+
meaning: unit.CodeMeaning,
3040+
schemeDesignator: unit.CodingSchemeDesignator
3041+
}),
3042+
relationshipType: dcmjs.sr.valueTypes.RelationshipTypes.CONTAINS
3043+
})
3044+
if (measurementItem.ReferencedImageSequence != null) {
3045+
const ref = measurementItem.ReferencedImageSequence[0]
3046+
const image = new dcmjs.sr.valueTypes.ImageContentItem({
3047+
name: new dcmjs.sr.coding.CodedConcept({
3048+
value: '121112',
3049+
meaning: 'Source of Measurement',
3050+
schemeDesignator: 'DCM'
3051+
}),
3052+
referencedSOPClassUID: ref.ReferencedSOPClassUID,
3053+
referencedSOPInstanceUID: ref.ReferencedSOPInstanceUID
3054+
})
3055+
if (ref.ReferencedOpticalPathIdentifier != null) {
3056+
image.ReferencedSOPSequence[0].ReferencedOpticalPathIdentifier = (
3057+
ref.ReferencedOpticalPathIdentifier
3058+
)
3059+
}
3060+
measurement.ContentSequence = [image]
3061+
}
3062+
roi.addMeasurement(measurement)
3063+
}
3064+
)
3065+
3066+
return roi
29543067
}
29553068

29563069
metadata.AnnotationGroupSequence.forEach((item, index) => {
@@ -2971,7 +3084,7 @@ class VolumeImageViewer {
29713084
}),
29723085
style: { ...defaultAnnotationGroupStyle },
29733086
defaultStyle: defaultAnnotationGroupStyle,
2974-
metadata: metadata
3087+
metadata
29753088
}
29763089

29773090
if (item.GraphicType === 'POLYLINE') {
@@ -2986,11 +3099,6 @@ class VolumeImageViewer {
29863099
return
29873100
}
29883101

2989-
// We need to bind those variables to constants for the loader function
2990-
const client = this[_options].client
2991-
const pyramid = this[_pyramid].metadata
2992-
const affineInverse = this[_affineInverse]
2993-
29943102
/**
29953103
* In the loader function "this" is bound to the vector source.
29963104
*/
@@ -3053,13 +3161,16 @@ class VolumeImageViewer {
30533161
const feature = new Feature({
30543162
geometry: new PointGeometry(coordinates)
30553163
})
3056-
const properties = {}
3164+
3165+
feature.set('annotationGroupUID', annotationGroupUID, true)
30573166
measurements.forEach((measurementItem, measurementIndex) => {
3167+
const key = `measurementValue${measurementIndex.toString()}`
30583168
const value = measurementItem.values[i]
3059-
properties[measurementIndex] = value
3169+
// Needed for the WebGL renderer
3170+
feature.set(key, value, true)
30603171
})
3061-
feature.setProperties(properties, true)
3062-
feature.setId(i + 1)
3172+
const uid = _generateUID(`${annotationGroupUID}-${i}`)
3173+
feature.setId(uid)
30633174
features.push(feature)
30643175
}
30653176

@@ -3086,7 +3197,8 @@ class VolumeImageViewer {
30863197
(a, b) => Math.max(a, b),
30873198
-Infinity
30883199
)
3089-
properties[measurementIndex] = { min, max }
3200+
const key = `measurementValue${measurementIndex.toString()}`
3201+
properties[key] = { min, max }
30903202
})
30913203
this.setProperties(properties, true)
30923204
success(features)
@@ -3139,13 +3251,40 @@ class VolumeImageViewer {
31393251
}
31403252
annotationGroup.layer = new PointsLayer({
31413253
source,
3142-
style
3254+
style,
3255+
disableHitDetection: false
31433256
})
31443257
annotationGroup.layer.setVisible(false)
31453258

31463259
this[_map].addLayer(annotationGroup.layer)
31473260
this[_annotationGroups][annotationGroupUID] = annotationGroup
31483261
})
3262+
3263+
let selectedAnnotation = null
3264+
this[_map].on('singleclick', (e) => {
3265+
if (selectedAnnotation !== null) {
3266+
selectedAnnotation.set('selected', 0)
3267+
selectedAnnotation = null
3268+
}
3269+
3270+
this[_map].forEachFeatureAtPixel(
3271+
e.pixel,
3272+
(feature) => {
3273+
feature.set('selected', 1)
3274+
selectedAnnotation = feature
3275+
publish(
3276+
container,
3277+
EVENT.ROI_SELECTED,
3278+
_getROIFromFeature(feature)
3279+
)
3280+
return true
3281+
},
3282+
{
3283+
hitTolerance: 1,
3284+
layerFilter: (layer) => (layer instanceof PointsLayer)
3285+
}
3286+
)
3287+
})
31493288
}
31503289

31513290
/**
@@ -3286,11 +3425,8 @@ class VolumeImageViewer {
32863425
)
32873426
}
32883427
const properties = source.getProperties()
3289-
if (properties[measurementIndex]) {
3290-
const colormap = createColormap({
3291-
name: ColormapNames.VIRIDIS,
3292-
bins: 50
3293-
})
3428+
const key = `measurementValue${measurementIndex.toString()}`
3429+
if (properties[key]) {
32943430
const style = {
32953431
symbol: {
32963432
symbolType: 'circle',
@@ -3306,19 +3442,23 @@ class VolumeImageViewer {
33063442
opacity: annotationGroup.style.opacity
33073443
}
33083444
}
3445+
const colormap = createColormap({
3446+
name: ColormapNames.VIRIDIS,
3447+
bins: 50
3448+
})
33093449
Object.assign(
3310-
style,
3450+
style.symbol,
33113451
_getColorPaletteStyleForPointLayer({
3312-
key: measurementIndex,
3313-
minValue: properties[measurementIndex].min,
3314-
maxValue: properties[measurementIndex].max,
3452+
key,
3453+
minValue: properties[key].min,
3454+
maxValue: properties[key].max,
33153455
colormap
33163456
})
33173457
)
33183458
const newLayer = new PointsLayer({
33193459
source,
33203460
style,
3321-
disableHitDetection: true,
3461+
disableHitDetection: false,
33223462
visible: false
33233463
})
33243464
this[_map].addLayer(newLayer)
@@ -3339,14 +3479,20 @@ class VolumeImageViewer {
33393479
this[_pyramid].metadata.length,
33403480
15
33413481
],
3342-
color: rgb2hex(annotationGroup.style.color),
3482+
color: [
3483+
'match',
3484+
['get', 'selected'],
3485+
1,
3486+
rgb2hex(this[_options].highlightColor),
3487+
rgb2hex(annotationGroup.style.color)
3488+
],
33433489
opacity: annotationGroup.style.opacity
33443490
}
33453491
}
33463492
const newLayer = new PointsLayer({
33473493
source,
33483494
style,
3349-
disableHitDetection: true,
3495+
disableHitDetection: false,
33503496
visible: false
33513497
})
33523498
this[_map].addLayer(newLayer)

0 commit comments

Comments
 (0)