diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt index 98504e38..768cad88 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/AmSatRepository.kt @@ -125,6 +125,11 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor utc.timeInMillis = (todayMidnightSec - d * 86400L) * 1000 "${monthAbbr[utc.get(Calendar.MONTH)]} ${utc.get(Calendar.DAY_OF_MONTH)}" } + // Oldest report across the whole response, marking how far back the data reaches. + // Taken globally rather than per satellite: a quiet satellite has no reports of its + // own, but the slots it shares with the rest of the response were still covered. + val dataFromSec = reports.minOfOrNull { it.reportedTimeUtcSec } ?: todayMidnightSec + return names.map { name -> val satReports = byName[name].orEmpty() val days = (0 until 3).map { dayIdx -> @@ -135,7 +140,9 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor val slotEnd = slotStart + 7200L val inSlot = satReports.filter { it.reportedTimeUtcSec in slotStart until slotEnd } if (inSlot.isEmpty()) { - SatSlot(statusColor = NO_REPORT_GRAY, count = 0) + // A slot entirely before the data starts is unknown, not silent. + val colour = if (slotEnd <= dataFromSec) NO_DATA_GRAY else NO_REPORT_GRAY + SatSlot(statusColor = colour, count = 0) } else { val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!! SatSlot( @@ -184,5 +191,15 @@ class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepositor private const val NOT_HEARD_PINK = 0xFFDC267F private const val CONFLICT_DEEP_ORANGE = 0xFFFE6100 private const val NO_REPORT_GRAY = 0xFFC0C0C0 + + /** + * Slots older than the data we actually received. + * + * The API caps at 500 records however many hours are requested. Measured live: a + * 72-hour request returned 500 reports spanning only 49 hours, leaving the oldest + * 9.5 hours of the third day with no data at all. Painting those the same grey as + * "nobody reported" claimed knowledge we do not have, so they get a lighter shade. + */ + private const val NO_DATA_GRAY = 0xFFE8E8E8 } } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt index be354baf..6971f864 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/AmSatSlotBuildTest.kt @@ -256,4 +256,77 @@ class AmSatSlotBuildTest { fun `an empty catalog yields no rows rather than a malformed grid`() { assertTrue(build(emptyList(), emptyList()).isEmpty()) } + + /** + * Slots older than the data we received must not claim nobody was listening. + * + * The API caps at 500 records however many hours are asked for. Measured live, a + * 72-hour request returned 500 reports covering only 49 hours, so the oldest 9.5 hours + * of the third day had no data at all - 352 of 3168 cells were painting "nobody heard + * it" over "we never looked". + */ + @Test + fun `slots before the data starts are marked no-data, not no-report`() { + // The only report is midday yesterday, so nothing older than that was covered. + val oldestReport = utc(2026, 8, 21, 12) + val status = build( + listOf("AO-91"), + listOf(report("AO-91", "heard", oldestReport, id = "only")) + ).single() + + val noReport = 0xFFC0C0C0 + val noData = 0xFFE8E8E8 + + // The day before yesterday is entirely before the data begins. + assertTrue( + "every slot older than the data must read as no-data", + status.days[2].slots.all { it.statusColor == noData } + ) + + // Yesterday straddles it: bands after midday are covered, bands before are not. + val yesterday = status.days[1] + assertEquals("the report's own band", 1, yesterday.slots[5].count) + assertTrue( + "bands after the oldest report are covered, so silence there is real", + yesterday.slots.take(6).all { it.statusColor != noData } + ) + assertTrue( + "the earliest band of yesterday is before any data", + yesterday.slots[11].statusColor == noData + ) + + // Today is entirely after the data starts, so its silence is genuine. + assertTrue( + "today's empty slots mean nobody reported", + status.days[0].slots.all { it.statusColor == noReport } + ) + } + + @Test + fun `coverage is judged from all reports, not one satellite's`() { + // A satellite nobody reported must not show as no-data for the whole grid: the + // slots were covered, that satellite simply was not heard. + val statuses = build( + listOf("LOUD", "QUIET"), + listOf(report("LOUD", "heard", utc(2026, 8, 20, 1), id = "early")) + ) + val quiet = statuses.first { it.name == "QUIET" } + val noData = 0xFFE8E8E8 + + assertTrue( + "coverage reaches back to the earliest report of any satellite", + quiet.days.all { day -> day.slots.none { it.statusColor == noData } } + ) + } + + @Test + fun `an empty response marks nothing as covered`() { + // With no reports at all there is no evidence about any slot. + val status = build(listOf("AO-91"), emptyList()).single() + val noData = 0xFFE8E8E8 + assertTrue( + "yesterday and earlier cannot be claimed as silent", + status.days.drop(1).all { day -> day.slots.all { it.statusColor == noData } } + ) + } } diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml index 63b77b52..d99318d5 100644 --- a/core/presentation/src/main/res/values-es/strings.xml +++ b/core/presentation/src/main/res/values-es/strings.xml @@ -171,4 +171,6 @@ Desplazar tonos CW fuera de rango DeepCW solo analiza 400-1200 Hz. Si está activo, un tono fuera de ese rango se traslada al rango antes de decodificar; un tono que ya está dentro no se modifica. + Sin informes + Sin datos diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index 935f221f..33f25cec 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -300,4 +300,6 @@ Aplikasi ini hadir tanpa jaminan Geser nada CW di luar rentang DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah. + Tidak ada laporan + Tidak ada data diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index ee57779a..d790855e 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -300,4 +300,6 @@ Aplikasi ini hadir tanpa jaminan Geser nada CW di luar rentang DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah. + Tidak ada laporan + Tidak ada data diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml index 6bc0f3dc..0a166b52 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -171,4 +171,6 @@ Сдвигать CW-тоны вне диапазона DeepCW анализирует только 400-1200 Гц. Если включено, тон вне этого диапазона переносится внутрь перед декодированием; тон внутри диапазона не изменяется. + Нет отчётов + Нет данных diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml index 66d0ad1a..05167d8d 100644 --- a/core/presentation/src/main/res/values-si/strings.xml +++ b/core/presentation/src/main/res/values-si/strings.xml @@ -171,4 +171,6 @@ පරාසයෙන් පිටත CW ස්වර මාරු කරන්න DeepCW විශ්ලේෂණය කරන්නේ 400-1200 Hz පමණි. සක්‍රීය විට, එම පරාසයෙන් පිටත ස්වරයක් විකේතනයට පෙර පරාසය තුළට ගෙන එයි; දැනටමත් පරාසය තුළ ඇති ස්වරයක් වෙනස් නොකරයි. + වාර්තා නැත + දත්ත නැත diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index a07e2ae4..a9a23b18 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -312,4 +312,6 @@ Aralık dışı CW tonlarını kaydır DeepCW yalnızca 400-1200 Hz analiz eder. Açıkken bu aralığın dışındaki bir ton çözülmeden önce aralığa taşınır; aralıkta olan ton değiştirilmez. + Rapor yok + Veri yok diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml index 2e5810fe..67605704 100644 --- a/core/presentation/src/main/res/values-uk/strings.xml +++ b/core/presentation/src/main/res/values-uk/strings.xml @@ -171,4 +171,6 @@ Зсувати CW-тони поза діапазоном DeepCW аналізує лише 400-1200 Гц. Якщо увімкнено, тон поза цим діапазоном переноситься в нього перед декодуванням; тон, що вже в діапазоні, не змінюється. + Немає звітів + Немає даних diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 366a0ad1..cc5733df 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -302,4 +302,6 @@ 雷达罗盘偏移 (仰角) 搬移超出范围的 CW 音调 DeepCW 仅分析 400-1200 Hz。开启后,超出该范围的音调会先搬移到范围内再解码;已在范围内的音调不作处理。 + 无人上报 + 无数据 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 84a374a2..72de5412 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -335,4 +335,6 @@ Radar compass offset (elev) Shift out-of-range CW tones DeepCW only analyses 400-1200 Hz. When on, a tone outside that range is moved into it before decoding; a tone already inside is untouched. + No report + No data diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt index 45ff4318..e958490f 100644 --- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt @@ -167,11 +167,16 @@ private fun StatusHeader(fetchedAtUtcMs: Long, isRefreshing: Boolean, onRefresh: /** Legend: FlowRow of colored chips — wraps to two lines on narrow screens, stays one line when wide. */ @Composable private fun LegendRow() { + // The two greys are listed because most of a typical grid is grey: measured live, 81% + // of cells were "nobody reported" and 11% were outside the data we received. Without + // the distinction a mostly-grey row reads as a dead satellite. val legend = listOf( stringResource(id = R.string.amsat_active) to Color(0xFF648FFF), stringResource(id = R.string.amsat_tlm) to Color(0xFFFFB000), stringResource(id = R.string.amsat_not_heard) to Color(0xFFDC267F), - stringResource(id = R.string.amsat_conflict) to Color(0xFFFE6100) + stringResource(id = R.string.amsat_conflict) to Color(0xFFFE6100), + stringResource(id = R.string.amsat_no_report_legend) to Color(0xFFC0C0C0), + stringResource(id = R.string.amsat_no_data_legend) to Color(0xFFE8E8E8) ) FlowRow( modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp),