diff options
| author | Claude <noreply@anthropic.com> | 2026-04-10 04:01:42 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-04-10 04:01:42 +0000 |
| commit | cc4e283c24beb4e1cdbf9ba7aae5c772e2f8a29d (patch) | |
| tree | 43f806b1dd096be8b5f8d86516c202e300775a5b /android-app/app | |
| parent | 1c78d59567b700868a69d6d14012207506ee255d (diff) | |
Fix particle disappearance at antimeridian-crossing viewports; use white
When the viewport crosses ±180° longitude (e.g. Pacific Ocean view),
minOf/maxOf on raw longitudes produces lonWest/lonEast that are backwards.
Every particle then satisfies the out-of-bounds check and is respawned on
every frame → continue → never drawn.
Fix: compute lonSpan from screen-ordered corners (left edge / right edge)
with antimeridian wrap: lonSpan = lonEast >= lonWest ? span : span + 360.
Bounds check uses normalized particle longitude relative to lonWest mod 360.
Particle movement wraps longitude into [-180, 180] to stay consistent.
Also change particle color to white so it contrasts against the blue wind
raster overlay instead of blending into it.
https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
Diffstat (limited to 'android-app/app')
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt | 62 |
1 files changed, 43 insertions, 19 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt index 61c34ad..aba8027 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt @@ -22,6 +22,9 @@ import kotlin.random.Random * Speed is scaled to the visible viewport so the animation looks consistent at any * zoom level: a particle at reference wind (10 kt) crosses ~30% of the screen in * MAX_AGE seconds. + * + * Longitude arithmetic is done in a west-relative [0, lonSpan] coordinate space + * so that viewports crossing the antimeridian (±180°) work correctly. */ class ParticleWindView @JvmOverloads constructor( context: Context, @@ -74,14 +77,21 @@ class ParticleWindView @JvmOverloads constructor( lastFrameNs = now val r = m.projection.visibleRegion - val corners = listOfNotNull(r.nearLeft, r.nearRight, r.farLeft, r.farRight) - if (corners.size < 4) { postInvalidateOnAnimation(); return } - val latSouth = corners.minOf { it.latitude }.toFloat() - val latNorth = corners.maxOf { it.latitude }.toFloat() - val lonWest = corners.minOf { it.longitude }.toFloat() - val lonEast = corners.maxOf { it.longitude }.toFloat() + val nL = r.nearLeft ?: run { postInvalidateOnAnimation(); return } + val nR = r.nearRight ?: run { postInvalidateOnAnimation(); return } + val fL = r.farLeft ?: run { postInvalidateOnAnimation(); return } + val fR = r.farRight ?: run { postInvalidateOnAnimation(); return } + + val latSouth = minOf(nL.latitude, nR.latitude, fL.latitude, fR.latitude).toFloat() + val latNorth = maxOf(nL.latitude, nR.latitude, fL.latitude, fR.latitude).toFloat() val latRange = latNorth - latSouth - val lonRange = lonEast - lonWest + + // West edge = left screen side; east edge = right screen side. + // Using screen-ordered corners handles antimeridian crossing correctly. + val lonWest = minOf(nL.longitude, fL.longitude).toFloat() + val lonEast = maxOf(nR.longitude, fR.longitude).toFloat() + // Span wraps around antimeridian when lonEast < lonWest (e.g. Pacific viewport) + val lonSpan = if (lonEast >= lonWest) lonEast - lonWest else lonEast - lonWest + 360f // Speed scale: at 10kt a particle crosses 30% of viewport in MAX_AGE seconds. val speedScale = latRange * 0.03f / MAX_AGE @@ -102,15 +112,23 @@ class ParticleWindView @JvmOverloads constructor( for (i in 0 until N) { particleLat[i] += dlat particleLon[i] += dlon + // Wrap longitude into [-180, 180] after movement + if (particleLon[i] > 180f) particleLon[i] -= 360f + else if (particleLon[i] < -180f) particleLon[i] += 360f particleAge[i] += dt + // Normalize particle longitude relative to lonWest into [0, 360) + // so the antimeridian-spanning bounds check works correctly. + val normLon = ((particleLon[i] - lonWest + 360f) % 360f) val needsRespawn = particleAge[i] > MAX_AGE || particleLat[i] < latSouth || particleLat[i] > latNorth - || particleLon[i] < lonWest || particleLon[i] > lonEast + || normLon > lonSpan if (needsRespawn) { particleLat[i] = latSouth + Random.nextFloat() * latRange - particleLon[i] = lonWest + Random.nextFloat() * lonRange + var newLon = lonWest + Random.nextFloat() * lonSpan + if (newLon > 180f) newLon -= 360f + particleLon[i] = newLon particleAge[i] = Random.nextFloat() * MAX_AGE continue } @@ -119,8 +137,8 @@ class ParticleWindView @JvmOverloads constructor( LatLng(particleLat[i].toDouble(), particleLon[i].toDouble()) ) - val alpha = ((1f - particleAge[i] / MAX_AGE) * 200).toInt().coerceIn(30, 200) - paint.color = Color.argb(alpha, 120, 200, 255) + val alpha = ((1f - particleAge[i] / MAX_AGE) * 220).toInt().coerceIn(40, 220) + paint.color = Color.argb(alpha, 255, 255, 255) canvas.drawLine(pt.x - tailDx, pt.y - tailDy, pt.x, pt.y, paint) } @@ -140,17 +158,23 @@ class ParticleWindView @JvmOverloads constructor( private fun scatter(m: MapLibreMap) { val r = m.projection.visibleRegion - val corners = listOfNotNull(r.nearLeft, r.nearRight, r.farLeft, r.farRight) - if (corners.size < 4) return - val latSouth = corners.minOf { it.latitude }.toFloat() - val latNorth = corners.maxOf { it.latitude }.toFloat() - val lonWest = corners.minOf { it.longitude }.toFloat() - val lonEast = corners.maxOf { it.longitude }.toFloat() + val nL = r.nearLeft ?: return + val nR = r.nearRight ?: return + val fL = r.farLeft ?: return + val fR = r.farRight ?: return + + val latSouth = minOf(nL.latitude, nR.latitude, fL.latitude, fR.latitude).toFloat() + val latNorth = maxOf(nL.latitude, nR.latitude, fL.latitude, fR.latitude).toFloat() val latRange = latNorth - latSouth - val lonRange = lonEast - lonWest + val lonWest = minOf(nL.longitude, fL.longitude).toFloat() + val lonEast = maxOf(nR.longitude, fR.longitude).toFloat() + val lonSpan = if (lonEast >= lonWest) lonEast - lonWest else lonEast - lonWest + 360f + for (i in 0 until N) { particleLat[i] = latSouth + Random.nextFloat() * latRange - particleLon[i] = lonWest + Random.nextFloat() * lonRange + var newLon = lonWest + Random.nextFloat() * lonSpan + if (newLon > 180f) newLon -= 360f + particleLon[i] = newLon particleAge[i] = Random.nextFloat() * MAX_AGE // stagger so no mass respawn } scattered = true |
