diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt index 6c429a2b..d141482e 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt @@ -220,26 +220,23 @@ class MapViewModel( var orbitalPass = defaultPass var aosTime = 0L.toTimerString() var isTimeAos = true - allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 1 } - ?.let { satPass -> - orbitalPass = satPass - if (!satPass.isDeepSpace) { - when { - date.time < satPass.aosTime -> { - // Pass hasn't started yet — count down to AOS - isTimeAos = true - aosTime = (satPass.aosTime - date.time).toTimerString() - } - - date.time < satPass.losTime -> { - // Pass is in progress — count down to LOS - isTimeAos = false - aosTime = (satPass.losTime - date.time).toTimerString() - } - // else: pass has ended (losTime <= date.time) — keep default "00:00:00" - } + // Select by time, not by OrbitalPass.progress: that field defaults to 0 + // and is only ever filled in by PassesViewModel's own local copy, never + // written back to the repository. Filtering on `progress < 1` therefore + // always matched the satellite's *first* pass, so once it ended the map + // countdown stuck at 00:00:00 and never advanced to the next pass. + currentOrNextPass(sat.data.catnum, date.time)?.let { satPass -> + orbitalPass = satPass + if (!satPass.isDeepSpace) { + if (date.time < satPass.aosTime) { + isTimeAos = true + aosTime = (satPass.aosTime - date.time).toTimerString() + } else if (date.time < satPass.losTime) { + isTimeAos = false + aosTime = (satPass.losTime - date.time).toTimerString() } } + } val azimuth = satPos.azimuth.toDegrees() val elevation = satPos.elevation.toDegrees() val osmPos = satPos.toMapGeoPos() @@ -266,6 +263,10 @@ class MapViewModel( return satData to orbitalPass } + /** Ongoing pass for [catnum] if any, otherwise its next upcoming pass. */ + private fun currentOrNextPass(catnum: Int, timeMillis: Long): OrbitalPass? = + selectCurrentOrNextPass(allPasses, catnum, timeMillis) + private suspend fun getSatTrack(orbitalObject: OrbitalObject, pos: GeoPos, date: Date) { val endDate = Date(date.time + (orbitalObject.data.orbitalPeriod * 2.4 * 60000L).toLong()) val track = satelliteRepo.getTrack(orbitalObject, pos, date.time, endDate.time) @@ -331,3 +332,20 @@ internal fun crossingLatitude(from: GeoPos, to: GeoPos, edgeLongitude: Double): val fraction = (edgeLongitude - from.longitude) / span return from.latitude + (to.latitude - from.latitude) * fraction } +/** + * Picks the pass to describe for [catnum] at [timeMillis]: the one in progress, + * or else the earliest one still to come. + * + * Deliberately ignores OrbitalPass.progress. That field is a UI-side value filled + * in by PassesViewModel on its own copy of the list; the repository always + * reports 0, so any `progress` predicate here silently matches everything. + */ +internal fun selectCurrentOrNextPass( + passes: List, + catnum: Int, + timeMillis: Long +): OrbitalPass? { + val forSatellite = passes.filter { it.catNum == catnum } + return forSatellite.firstOrNull { timeMillis >= it.aosTime && timeMillis < it.losTime } + ?: forSatellite.filter { it.aosTime > timeMillis }.minByOrNull { it.aosTime } +} diff --git a/feature/map/src/test/java/com/rtbishop/look4sat/feature/map/CurrentOrNextPassTest.kt b/feature/map/src/test/java/com/rtbishop/look4sat/feature/map/CurrentOrNextPassTest.kt new file mode 100644 index 00000000..792a4cf2 --- /dev/null +++ b/feature/map/src/test/java/com/rtbishop/look4sat/feature/map/CurrentOrNextPassTest.kt @@ -0,0 +1,84 @@ +package com.rtbishop.look4sat.feature.map + +import com.rtbishop.look4sat.core.domain.predict.NearEarthObject +import com.rtbishop.look4sat.core.domain.predict.OrbitalData +import com.rtbishop.look4sat.core.domain.predict.OrbitalPass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * The map info panel must describe the pass that is happening now, or the next + * one if none is. + * + * Regression: selection used to filter on `OrbitalPass.progress < 1`. That field + * defaults to 0 and is only ever updated on PassesViewModel's private copy, so + * the predicate matched every pass and the map always described the satellite's + * first pass - leaving the countdown frozen at 00:00:00 once it had ended. + */ +class CurrentOrNextPassTest { + + private val hour = 3_600_000L + private val iss = 25544 + private val noaa = 33591 + + private fun orbitalData(catnum: Int) = OrbitalData( + name = "sat-$catnum", + epoch = 21320.51955234, + meanmo = 15.48582035, + eccn = 0.0004694, + incl = 51.6447, + raan = 309.4881, + argper = 203.6966, + meanan = 299.8876, + catnum = catnum, + bstar = 0.31985e-4 + ) + + private fun pass(catnum: Int, aos: Long, los: Long) = OrbitalPass( + aosTime = aos, + losTime = los, + orbitalObject = NearEarthObject(orbitalData(catnum)) + ) + + private val issFirst = pass(iss, 10 * hour, 10 * hour + 600_000) + private val issSecond = pass(iss, 12 * hour, 12 * hour + 600_000) + private val issThird = pass(iss, 14 * hour, 14 * hour + 600_000) + private val other = pass(noaa, 11 * hour, 11 * hour + 600_000) + private val passes = listOf(issFirst, other, issSecond, issThird) + + @Test + fun `before the first pass returns the first pass`() { + assertEquals(issFirst, selectCurrentOrNextPass(passes, iss, (9.5 * hour).toLong())) + } + + @Test + fun `during a pass returns that pass`() { + assertEquals(issFirst, selectCurrentOrNextPass(passes, iss, 10 * hour + 300_000)) + assertEquals(issSecond, selectCurrentOrNextPass(passes, iss, 12 * hour + 300_000)) + } + + @Test + fun `after a pass ends returns the following pass`() { + // The case the progress predicate got wrong: it kept returning issFirst. + assertEquals(issSecond, selectCurrentOrNextPass(passes, iss, (10.5 * hour).toLong())) + assertEquals(issThird, selectCurrentOrNextPass(passes, iss, 13 * hour)) + } + + @Test + fun `after the last pass returns null`() { + assertNull(selectCurrentOrNextPass(passes, iss, 15 * hour)) + } + + @Test + fun `other satellites are ignored`() { + assertEquals(other, selectCurrentOrNextPass(passes, noaa, 11 * hour + 60_000)) + assertNull(selectCurrentOrNextPass(passes, noaa, 12 * hour)) + } + + @Test + fun `exact aos belongs to the pass and exact los does not`() { + assertEquals(issFirst, selectCurrentOrNextPass(passes, iss, 10 * hour)) + assertEquals(issSecond, selectCurrentOrNextPass(passes, iss, 10 * hour + 600_000)) + } +}