Skip to content

fix(markers): render locally-dropped markers on the 2D engine (#77)#91

Merged
jfuginay merged 1 commit into
mainfrom
fix/77-markers-on-2d
Jun 11, 2026
Merged

fix(markers): render locally-dropped markers on the 2D engine (#77)#91
jfuginay merged 1 commit into
mainfrom
fix/77-markers-on-2d

Conversation

@jfuginay

Copy link
Copy Markdown
Contributor

Root cause

File: app/src/main/kotlin/soy/engindearing/omnitak/mobile/ui/components/TacticalMap.kt, line 383 (removed)

The contact push inside TacticalMap was wired as:

DisposableEffect(mapView, contacts) {
    mapView.getMapAsync { map ->
        if (map.style != null) {           // ← silent drop during style load
            ContactLayer.update(map, context, contacts)
            contactSymbolLayer.update(map, context, contacts)
        }
    }
    onDispose { }
}

During the initial style-load window (100–2 000 ms), map.style != null is false. The update is silently dropped. The style-ready callback at line ~156 was meant to recover via currentContacts (rememberUpdatedState), but it fires only once. Any marker ingested after that first push and before the next Compose-triggered contacts key change was irretrievably lost — showing up on the Cesium 3D Globe (whose AndroidView.update { pushEntities() } retries on every recomposition) but never on 2D MapLibre.

What changed

TacticalMap.kt — removed DisposableEffect(mapView, contacts) and changed the terminal AndroidView from factory-only to factory + update:

AndroidView(
    factory = { mapView },
    update = {
        mapView.getMapAsync { map ->
            map.getStyle { _ ->                // queuing variant — never drops
                ContactLayer.update(map, context, currentContacts)
                contactSymbolLayer.update(map, context, currentContacts)
            }
        }
    },
    modifier = modifier,
)

Three deliberate changes vs the removed effect:

  1. AndroidView.update { } fires on every recomposition that receives a new contacts list — identical cadence to the Cesium engine. The update retries on the next frame instead of being lost.
  2. map.getStyle { } (queuing variant) instead of if (map.style != null): when the style is still loading the callback queues and fires when ready, closing the style-load race entirely.
  3. currentContacts (rememberUpdatedState) inside the callback instead of the DisposableEffect-time snapshot — the async callback always sees the latest list.

ContactLayerFeatureTest.kt (new) — pure JVM test for the data-layer preconditions: every locally-dropped marker type (point-dropper, coordinate-entry, FEMA palette) produces finite lat/lon and a valid CSS hex color string, covering argbToHex edge cases and the NaN-guard contract that ContactLayer.update depends on.

Test evidence

./gradlew assembleDebug       → BUILD SUCCESSFUL
./gradlew testDebugUnitTest   → BUILD SUCCESSFUL (78 tasks, all green)

New test class: ContactLayerFeatureTest (14 tests).

On-device verification rides with the next closed-track build.

Root cause (TacticalMap.kt, line 383 — now removed): the contact push
was gated behind `map.style != null` inside an async `getMapAsync`
callback launched from a `DisposableEffect(mapView, contacts)`. During
the initial style-load window (100–2 000 ms) that guard evaluates false
and the update is silently dropped. The one-shot style-ready callback at
line ~156 was meant to recover via `currentContacts`, but it only fires
once — any marker ingested after that first push and before the next
Compose-driven `contacts` key change was irretrievably lost.

Cesium has never had this bug: `AndroidView(update = { pushEntities() })`
re-pushes on every recomposition using `rememberUpdatedState`, so the
update retries automatically on the next frame.

Fix (TacticalMap.kt, line 557):

  1. Move the contact push into `AndroidView.update { }` so it fires on
     every recomposition that delivers a changed contacts list — identical
     cadence to the Cesium path. The removed `DisposableEffect(mapView,
     contacts)` block is no longer needed.

  2. Replace `if (map.style != null)` with `map.getStyle { }` (the
     queuing variant): when the style is still loading the callback is
     queued and fires as soon as the style is ready, so a marker dropped
     during the style-load window is never silently discarded.

  3. Read `currentContacts` (rememberUpdatedState) inside the callback
     instead of the DisposableEffect-time snapshot — the async getStyle
     callback always sees the latest list, closing the snapshot-staleness
     gap that existed under concurrent rapid ingest.

No other files changed. On-device verification rides with the next
closed-track build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jfuginay jfuginay merged commit 3ba1c41 into main Jun 11, 2026
1 check passed
jfuginay added a commit that referenced this pull request Jun 11, 2026
- versionName 0.35.3 -> 0.35.4, versionCode 91 -> 92
- adds #88 (compass clears top bar, #81), #90 (drawings survive style
  reloads, #80), #91 (locally-dropped markers on 2D, #77) on top of
  0.35.3's #75/#78 — all five from the r/ATAK tester thread, same day
- supersedes the never-uploaded 0.35.3 vc91 AAB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant