Skip to content

[google_maps_flutter_android] Reindex clustered markers in spatial index on position change#11749

Draft
EverCamilo wants to merge 3 commits into
flutter:mainfrom
EverCamilo:fix/cluster-position-reindex
Draft

[google_maps_flutter_android] Reindex clustered markers in spatial index on position change#11749
EverCamilo wants to merge 3 commits into
flutter:mainfrom
EverCamilo:fix/cluster-position-reindex

Conversation

@EverCamilo
Copy link
Copy Markdown

@EverCamilo EverCamilo commented May 21, 2026

Description

When a clustered marker's position is updated in-place via MarkersController.changeMarkers(), the ClusterManager's NonHierarchicalDistanceBasedAlgorithm retains stale QuadTree entries at the old coordinates. This causes ghost clusters to appear at positions where no marker currently exists.

Root cause

MarkersController.changeMarkers() has two code paths when updating a marker:

  1. Cluster ID changed — correctly calls removeItem + addItem on the ClusterManager, refreshing the spatial index.
  2. Same cluster ID (in-place update) — calls interpretMarkerOptions() to update the MarkerBuilder properties (including position), but does not reindex the item in the ClusterManager. The QuadTree inside NonHierarchicalDistanceBasedAlgorithm stores a QuadItem wrapper that captures the position at insertion time. Without reindexing, the spatial index retains the stale position.

Fix

  • Capture the marker's LatLng position before interpretMarkerOptions() updates the builder.
  • After the update, compare old vs. new position.
  • If changed and the marker belongs to a ClusterManager, call removeItem + addItem to refresh the QuadTree.
  • Batch variant (reindexItems) added for the public changeMarkers() path — calls cluster() only once per ClusterManager for efficiency.

Files changed

  • ClusterManagersController.java: added reindexItem() and reindexItems() methods.
  • MarkersController.java: added position-change detection and reindex call in both the changeMarkers() (batch) and changeMarker() (single) paths.

Reproduction

  1. Create a GoogleMap with a ClusterManager and several markers.
  2. Programmatically update the position of a clustered marker (keeping the same MarkerId and ClusterManagerId).
  3. Observe that ghost clusters appear at the old positions and persist until the ClusterManager is fully rebuilt.

Issues

No associated issue yet — I will file one in flutter/flutter describing the ghost-cluster behavior and link it here before requesting review.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

…dex on position change

When a clustered marker position is updated in-place via MarkersController.changeMarkers(), the ClusterManager NonHierarchicalDistanceBasedAlgorithm retains stale QuadTree entries at the old coordinates. This causes ghost clusters to appear at positions where no marker currently exists.

Fix: capture the marker position before interpretMarkerOptions(), compare after, and if changed call removeItem + addItem on the ClusterManager to refresh the QuadTree. Batch variant added for the public changeMarkers() path for efficiency.
Co-authored-by: Cursor <cursoragent@cursor.com>
@flutter-dashboard
Copy link
Copy Markdown

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@google-cla
Copy link
Copy Markdown

google-cla Bot commented May 21, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces reindexing logic to handle in-place position updates for clustered markers by adding reindexItem and reindexItems methods to ClusterManagersController. However, the review feedback indicates a logic error where marker positions are updated before the removal from the spatial index is triggered. This prevents the ClusterManager from correctly locating and removing markers from their old coordinates in the QuadTree, which would result in the failure to clear stale data.

public void reindexItem(@NonNull String clusterManagerId, @NonNull MarkerBuilder item) {
ClusterManager<MarkerBuilder> clusterManager = clusterManagerIdToManager.get(clusterManagerId);
if (clusterManager != null) {
clusterManager.removeItem(item);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The reindexItem method (and reindexItems below) will fail to remove the item from the spatial index if its position has already been updated. The ClusterManager.removeItem method relies on the item's current getPosition() to locate it in the underlying QuadTree. If the position was mutated before calling this method, the removal will look at the new coordinates, while the item is still indexed at the old coordinates, leading to the "ghost clusters" this PR intends to fix.

Comment on lines +151 to +155
if (clusterManagerId != null && !markerBuilder.getPosition().equals(oldPosition)) {
markersToReindexByCluster
.computeIfAbsent(clusterManagerId, k -> new ArrayList<>())
.add(markerBuilder);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In changeMarkers, interpretMarkerOptions is called at line 140, which updates the markerBuilder's position. The subsequent call to reindexItems (line 168) will attempt to remove the markers using their new positions. This will fail to clear the old entries from the ClusterManager's spatial index. To fix this, markers whose positions are changing should be removed from the ClusterManager before their state is mutated, or the reindexing logic must be provided with the old coordinates to perform the removal.

Comment on lines +405 to +407
if (clusterManagerId != null && !markerBuilder.getPosition().equals(oldPosition)) {
clusterManagersController.reindexItem(clusterManagerId, markerBuilder);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to the batch update path, in changeMarker, the markerBuilder is updated at line 397 before reindexItem is called. This prevents the ClusterManager from correctly identifying and removing the marker from its old position in the spatial index because removeItem will be searching at the new coordinates.

EverCamilo and others added 2 commits May 20, 2026 23:09
The QuadTree locates entries by getPosition(). The previous reindexItem approach called removeItem after the position was already mutated, so the spatial lookup searched at the new coordinates and missed the stale entry at the old position.

Fix: call removeItemSilently (without recluster) BEFORE interpretMarkerOptions mutates the builder, then batch re-add after mutation. This ensures the QuadTree removal finds the item at its original coordinates.
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous commit removed and re-added ALL clustered markers unconditionally, causing excessive cluster() calls that could interfere with tap detection. Now we only reindex markers whose position actually changed.

To correctly remove the stale QuadTree entry (which requires the old position), the builder is temporarily set back to the old coordinates for removal, then restored to the new coordinates for re-addition.

Co-authored-by: Cursor <cursoragent@cursor.com>
@stuartmorgan-g
Copy link
Copy Markdown
Collaborator

Thanks for the contribution!

Please do not replace our PR checklist with an AI-created alternative; the things on our checklist are all there for a reason. This PR is missing required elements described in the checklist, which need to be addressed before it moves forward with review.

I am marking the PR as a Draft. Please add and complete the correct checklist, updating the PR as appropriate, and when that’s complete please feel free to mark the PR as ready for review.

@stuartmorgan-g stuartmorgan-g marked this pull request as draft May 26, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants