Skip to content

feat(map): declutter overlapping contact markers#572

Merged
torlando-tech merged 8 commits intotorlando-tech:mainfrom
MatthieuTexier:feature/map-marker-declutter
Mar 10, 2026
Merged

feat(map): declutter overlapping contact markers#572
torlando-tech merged 8 commits intotorlando-tech:mainfrom
MatthieuTexier:feature/map-marker-declutter

Conversation

@MatthieuTexier
Copy link
Copy Markdown
Contributor

@MatthieuTexier MatthieuTexier commented Feb 27, 2026

Summary

When multiple contacts share nearby GPS positions, their map markers overlap and become unreadable. This PR adds a custom decluttering algorithm that detects overlapping markers and spreads them radially, with thin lines connecting each displaced marker back to its true GPS position.

What it does

  • Detects overlapping markers in screen space using a Union-Find algorithm (threshold: 60px)
  • Spreads grouped markers radially around their centroid, sorted by natural angle to prevent line crossings
  • Scales the radius adaptively based on group size so markers don't overlap even in large clusters
  • Shows GPS pins (small dots) at true positions with connecting lines to displaced markers
  • Recalculates dynamically on zoom/pan via onCameraIdleListener
  • Symmetric bitmap padding ensures iconAnchor("center") positions the circle correctly

Architecture

  • MarkerDeclutter.kt — pure algorithm with ScreenToLatLng interface for testability (no MapLibre dependency)
  • MapScreen.kt — integration: projects markers, creates GeoJSON sources/layers
  • MarkerBitmapFactory.kt — top padding for vertical symmetry

Tests

  • 15 unit tests in MarkerDeclutterTest.kt: empty input, single marker, isolated pairs, overlapping pairs, equidistant offsets, multiple clusters, mixed isolated/clustered, large groups, radius scaling, custom thresholds, hash preservation, transitive grouping
  • 1 bitmap symmetry test in MarkerBitmapFactoryTest.kt

Screenshots

Screenshot_20260227-162758_Columba Screenshot_20260227-163302_Columba Screenshot_20260227-163338_Columba Screenshot_20260227-163726_Columba

Update: marker declutter is now optional (Map settings)

Following feedback, marker decluttering is now user-configurable in Settings.

What changed

  • Renamed the settings card title from "Map Sources" to "Map"
  • Split the card into 2 clear sections:
    • Sources (HTTP / RMSP)
    • Settings (marker behavior)
  • Added a new toggle: "Marker declutter"
    • When enabled (default): overlapping markers are spread for readability
    • When disabled: markers stay at their original GPS positions (no decluttering)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR implements a sophisticated marker decluttering system for the map screen. When multiple contacts share nearby GPS positions, their markers overlap and become unreadable. The solution uses a Union-Find algorithm to detect overlapping markers (within 60px threshold) and spreads them radially around their centroid with adaptive radius scaling based on cluster size.

Key implementation details:

  • MarkerDeclutter.kt provides a pure, testable algorithm with no MapLibre dependency
  • MapScreen integration recalculates positions dynamically on camera idle (zoom/pan)
  • GeoJSON sources/layers for pins (small dots at true GPS) and connecting lines to offset markers
  • Symmetric bitmap padding ensures iconAnchor("center") positions circles correctly
  • User-configurable toggle in Map settings (default: enabled)

Test coverage: 15 unit tests for core algorithm + bitmap symmetry test + deduplication test + UI tests

Architecture highlights:

  • Clean separation: algorithm is MapLibre-agnostic via ScreenToLatLng interface
  • Proper reactive flow: Settings → DataStore → ViewModel → UI
  • Efficient updates: only recalculates on camera idle, not every frame

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • Well-architected implementation with comprehensive testing (15+ unit tests), clean separation of concerns, proper reactive flows, and no security concerns. The O(n²) lookup performance issue was already noted in a previous review. Code follows established patterns and includes proper documentation.
  • No files require special attention

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/ui/util/MarkerDeclutter.kt Well-designed pure algorithm with Union-Find clustering, adaptive radius scaling, and comprehensive test coverage (15 tests)
app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt Solid integration with proper camera idle recalculation and GeoJSON layer management; O(n²) lookup already noted in previous review
app/src/main/java/com/lxmf/messenger/ui/util/MarkerBitmapFactory.kt Added symmetric vertical padding to center circles for iconAnchor positioning; verified by unit test
app/src/main/java/com/lxmf/messenger/viewmodel/MapViewModel.kt Clean flow integration collecting mapMarkerDeclutterEnabledFlow from settings repository
app/src/main/java/com/lxmf/messenger/ui/screens/settings/cards/MapSourcesCard.kt Restructured card with Sources/Settings sections; marker declutter toggle added with clear description
app/src/test/java/com/lxmf/messenger/ui/util/MarkerDeclutterTest.kt Excellent test coverage: empty inputs, isolated/overlapping pairs, multiple clusters, transitive grouping, radius scaling, custom thresholds

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User toggles declutter in Settings] --> B[SettingsViewModel.setMapMarkerDeclutterEnabled]
    B --> C[SettingsRepository saves to DataStore]
    C --> D[mapMarkerDeclutterEnabledFlow emits]
    D --> E[MapViewModel collects flow]
    E --> F[MapState.mapMarkerDeclutterEnabled updates]
    F --> G[MapScreen LaunchedEffect triggers]
    
    H[Contact markers update] --> G
    I[Camera idle pan/zoom] --> J[onCameraIdleListener]
    J --> K[updateDeclutterSources]
    G --> K
    
    K --> L{declutterEnabled?}
    L -->|Yes| M[Project markers to screen space]
    M --> N[MarkerDeclutter.calculateDeclutteredPositions]
    N --> O[Union-Find groups overlapping markers]
    O --> P[Spread groups radially with adaptive radius]
    P --> Q[Convert back to lat/lng]
    Q --> R[Update marker GeoJSON with display positions]
    R --> S[Create pin/line GeoJSON for offset markers]
    
    L -->|No| T[Use original GPS positions]
    T --> R
    S --> U[Render map with decluttered markers]
Loading

Last reviewed commit: 49e26d4

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@serialrf433
Copy link
Copy Markdown
Contributor

I really dislike this. Please make a option to not have it turned on when you do not like it.
When there are many markers on the map, i zoom into the map further and put them apart by doing so. The only reason to have such a thing is when i reached the max zoom-in and there are still two markers that overlap each other because they have both the same coordinates.
When i zoom out on the map, i can see where the most markers on the map are by having this visual blob build of many markers.

@MatthieuTexier
Copy link
Copy Markdown
Contributor Author

Where would you set the option ?

@serialrf433
Copy link
Copy Markdown
Contributor

Settings -> Map Sources

In combination with a separate PR renaming 'Map Sources' to 'Map'

@MatthieuTexier
Copy link
Copy Markdown
Contributor Author

done

Detect overlapping markers using screen-space proximity (Union-Find
grouping) and spread them radially around their centroid with
connecting lines to the original GPS positions.

- Extract declutter algorithm to MarkerDeclutter.kt with testable API
- Add ScreenToLatLng interface to abstract MapLibre projection
- Add pin (CircleLayer) and line (LineLayer) sources for GPS points
- Sort markers by angle from centroid to prevent line crossing
- Scale radius adaptively based on group size
- Recalculate on camera idle (zoom/pan) for dynamic updates
- Pad marker bitmaps symmetrically for iconAnchor(center) alignment
- Add 15 unit tests for declutter algorithm (MarkerDeclutterTest)
- Add bitmap symmetry test (MarkerBitmapFactoryTest)
@MatthieuTexier MatthieuTexier force-pushed the feature/map-marker-declutter branch from 06feffc to 9355163 Compare February 27, 2026 21:41
@MatthieuTexier MatthieuTexier marked this pull request as draft February 28, 2026 19:05
@MatthieuTexier MatthieuTexier marked this pull request as ready for review February 28, 2026 20:51
@torlando-tech torlando-tech added this to the v0.10.0 milestone Mar 7, 2026
@torlando-tech torlando-tech merged commit 44a1eed into torlando-tech:main Mar 10, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants