@@ -826,6 +826,10 @@ class VolumeImageViewer {
826826 * @param {number[] } [options.highlightColor=[140, 184, 198]] - Color that
827827 * should be used to highlight things that get selected by the user
828828 * @param {object } [options.annotationOptions] - Annotation options
829+ * @param {number } [options.annotationOptions.clusteringPixelSizeThreshold] -
830+ * Pixel size threshold in millimeters. When the current pixel size is smaller
831+ * than or equal to this threshold, clustering is disabled (high resolution mode).
832+ * Defaults to undefined, which falls back to zoom-based detection.
829833 * @param {errorInterceptor } [options.errorInterceptor] - Callback for
830834 * intercepting errors
831835 * @param {number[] } [options.mapViewResolutions] Map's view list of
@@ -901,6 +905,10 @@ class VolumeImageViewer {
901905 this [ _annotationOptions ] = this [ _options ] . annotationOptions
902906 }
903907
908+ if ( this [ _annotationOptions ] . clusteringPixelSizeThreshold === undefined ) {
909+ this [ _annotationOptions ] . clusteringPixelSizeThreshold = 0.001
910+ }
911+
904912 if ( this [ _options ] . errorInterceptor == null ) {
905913 this [ _options ] . errorInterceptor = error => error
906914 }
@@ -3823,18 +3831,50 @@ class VolumeImageViewer {
38233831 const affine = this [ _affine ]
38243832 const map = this [ _map ]
38253833 const view = map . getView ( )
3826- const maxZoom = view . getMaxZoom ( )
38273834 const isHighResolution = ( ) => {
3828- const isZoomUnlimited = this [ _mapViewResolutions ] === undefined
3829- const highestResolution = this [ _tileGrid ] . getResolutions ( ) [ 0 ]
3830- const updatedMaxZoom = isZoomUnlimited ? highestResolution : ( this [ _annotationOptions ] . maxZoom || maxZoom )
3831- const zoom = isZoomUnlimited ? ( view . getZoom ( ) * this [ _tileGrid ] . getResolutions ( ) . length ) : view . getZoom ( )
3832- console . debug ( 'Zoom:' , zoom )
3833- console . debug ( 'Max Zoom:' , updatedMaxZoom )
3834- console . debug ( 'Original Max Zoom:' , maxZoom )
3835- console . debug ( 'Highest Resolution:' , highestResolution )
3836- console . debug ( 'Resolutions:' , this [ _tileGrid ] . getResolutions ( ) . length )
3837- return zoom >= updatedMaxZoom
3835+ const clusteringPixelSizeThreshold = this [ _annotationOptions ] ?. clusteringPixelSizeThreshold
3836+ if ( clusteringPixelSizeThreshold !== undefined ) {
3837+ const currentResolution = view . getResolution ( )
3838+ const resolutions = this [ _tileGrid ] . getResolutions ( )
3839+
3840+ /** Find the closest pyramid level based on current resolution */
3841+ let closestLevelIndex = 0
3842+ let minDiff = Math . abs ( resolutions [ 0 ] - currentResolution )
3843+ for ( let i = 1 ; i < resolutions . length ; i ++ ) {
3844+ const diff = Math . abs ( resolutions [ i ] - currentResolution )
3845+ if ( diff < minDiff ) {
3846+ minDiff = diff
3847+ closestLevelIndex = i
3848+ }
3849+ }
3850+
3851+ /** Get pixel spacing for the current pyramid level */
3852+ const currentPixelSpacing = this [ _pyramid ] . pixelSpacings [ closestLevelIndex ]
3853+ /** Use the smaller of the two pixel spacing values (typically they're similar) */
3854+ const currentPixelSize = Math . min ( currentPixelSpacing [ 0 ] , currentPixelSpacing [ 1 ] )
3855+
3856+ console . debug ( 'Current Resolution:' , currentResolution )
3857+ console . debug ( 'Closest Level Index:' , closestLevelIndex )
3858+ console . debug ( 'Current Pixel Size (mm):' , currentPixelSize )
3859+ console . debug ( 'Clustering Threshold (mm):' , clusteringPixelSizeThreshold )
3860+
3861+ /** Return true (high resolution) when pixel size is <= threshold (smaller pixels = higher resolution) */
3862+ return currentPixelSize <= clusteringPixelSizeThreshold
3863+ }
3864+
3865+ /**
3866+ * When clusteringPixelSizeThreshold is undefined, it means clustering is disabled.
3867+ * In this case, always use high-res layer (return true).
3868+ *
3869+ * Note: This handles both cases:
3870+ * 1. Clustering was explicitly disabled (threshold set to undefined)
3871+ * 2. Clustering was never configured (threshold never set)
3872+ *
3873+ * In both cases, we want to use high-res layer, so return true.
3874+ * The zoom-based fallback in setAnnotationOptions is for backward compatibility
3875+ * but here we simplify to always use high-res when threshold is undefined.
3876+ */
3877+ return true
38383878 }
38393879
38403880 /**
@@ -4533,6 +4573,95 @@ class VolumeImageViewer {
45334573 return annotationGroup . activeLayer ( ) . getVisible ( )
45344574 }
45354575
4576+ /**
4577+ * Update annotation options.
4578+ *
4579+ * @param {Object } options - Annotation options
4580+ * @param {number } [options.clusteringPixelSizeThreshold] - Pixel size threshold
4581+ * in millimeters. When the current pixel size is smaller than or equal to this
4582+ * threshold, clustering is disabled (high resolution mode). Set to undefined
4583+ * to use zoom-based detection.
4584+ * @returns {void }
4585+ */
4586+ setAnnotationOptions ( options = { } ) {
4587+ if ( 'clusteringPixelSizeThreshold' in options ) {
4588+ if ( options . clusteringPixelSizeThreshold !== undefined ) {
4589+ this [ _annotationOptions ] . clusteringPixelSizeThreshold = options . clusteringPixelSizeThreshold
4590+ } else {
4591+ if ( this [ _annotationOptions ] . clusteringPixelSizeThreshold !== undefined ) {
4592+ delete this [ _annotationOptions ] . clusteringPixelSizeThreshold
4593+ }
4594+ }
4595+
4596+ const view = this [ _map ] . getView ( )
4597+ const isHighResolution = ( ) => {
4598+ const clusteringPixelSizeThreshold = this [ _annotationOptions ] ?. clusteringPixelSizeThreshold
4599+ if ( clusteringPixelSizeThreshold !== undefined ) {
4600+ const currentResolution = view . getResolution ( )
4601+ const resolutions = this [ _tileGrid ] . getResolutions ( )
4602+
4603+ let closestLevelIndex = 0
4604+ let minDiff = Math . abs ( resolutions [ 0 ] - currentResolution )
4605+ for ( let i = 1 ; i < resolutions . length ; i ++ ) {
4606+ const diff = Math . abs ( resolutions [ i ] - currentResolution )
4607+ if ( diff < minDiff ) {
4608+ minDiff = diff
4609+ closestLevelIndex = i
4610+ }
4611+ }
4612+
4613+ const currentPixelSpacing = this [ _pyramid ] . pixelSpacings [ closestLevelIndex ]
4614+ const currentPixelSize = Math . min ( currentPixelSpacing [ 0 ] , currentPixelSpacing [ 1 ] )
4615+ return currentPixelSize <= clusteringPixelSizeThreshold
4616+ }
4617+
4618+ /**
4619+ * Fallback to zoom-based detection
4620+ */
4621+ const isZoomUnlimited = this [ _mapViewResolutions ] === undefined
4622+ const highestResolution = this [ _tileGrid ] . getResolutions ( ) [ 0 ]
4623+ const updatedMaxZoom = isZoomUnlimited ? highestResolution : ( this [ _annotationOptions ] . maxZoom || view . getMaxZoom ( ) )
4624+ const zoom = isZoomUnlimited ? ( view . getZoom ( ) * this [ _tileGrid ] . getResolutions ( ) . length ) : view . getZoom ( )
4625+ return zoom >= updatedMaxZoom
4626+ }
4627+
4628+ /**
4629+ * Update visibility for all annotation groups
4630+ * Only update if the annotation group is currently visible to avoid triggering unnecessary loads
4631+ */
4632+ Object . values ( this [ _annotationGroups ] ) . forEach ( ( annotationGroup ) => {
4633+ if ( annotationGroup . layers && annotationGroup . layers . length >= 2 ) {
4634+ /** Check if annotation group is currently visible (at least one layer is visible) */
4635+ const isCurrentlyVisible = annotationGroup . layers . some ( layer => layer . getVisible ( ) === true )
4636+
4637+ /**
4638+ * Only update visibility if the annotation group is already visible
4639+ * If it's not visible, just update the config and let moveend handler take care of it
4640+ */
4641+ if ( isCurrentlyVisible ) {
4642+ /** When clustering is disabled (undefined), always use high-res layer */
4643+ const clusteringPixelSizeThreshold = this [ _annotationOptions ] ?. clusteringPixelSizeThreshold
4644+ let shouldShowHighRes
4645+ if ( clusteringPixelSizeThreshold === undefined ) {
4646+ shouldShowHighRes = true
4647+ } else {
4648+ shouldShowHighRes = isHighResolution ( )
4649+ }
4650+
4651+ /** Only update visibility if it's actually changing to avoid triggering unnecessary loads */
4652+ const currentlyHighResVisible = annotationGroup . layers [ 0 ] . getVisible ( )
4653+ if ( currentlyHighResVisible !== shouldShowHighRes ) {
4654+ annotationGroup . layers [ 0 ] . setVisible ( shouldShowHighRes )
4655+ annotationGroup . layers [ 1 ] . setVisible ( ! shouldShowHighRes )
4656+ }
4657+ }
4658+ // If annotation group is not visible, don't touch layers - just let the config update
4659+ // The moveend handler will apply the correct layer when the group becomes visible
4660+ }
4661+ } )
4662+ }
4663+ }
4664+
45364665 /**
45374666 * Set style of an annotation group.
45384667 *
0 commit comments