fix(amsat): distinguish slots we have no data for from slots nobody reported
Grey meant two different things. The API caps at 500 records however many hours are requested: measured against the live endpoint, a 72-hour request returned 500 reports spanning only 49 hours, so the oldest 9.5 hours of the third day had no data at all. Those cells were painted the same grey as "nobody reported", which claimed knowledge we did not have - 352 of 3168 cells on a real page, a third of the third day's column. Slots entirely older than the earliest report in the response now use a lighter grey. Coverage is judged from all reports 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, so it must read as "not heard" rather than "unknown". The two greys are now in the legend, which previously listed only the four active states. That matters more than it sounds: on the live page 81% of cells are "nobody reported" and 11% are outside our data, so a user looking at a mostly-grey row had no way to tell a dead satellite from a gap in what we fetched. The legend chips use a solid dot, so the two greys stay distinguishable despite the 25% alpha background. Strings added to all nine locales. Three tests cover it: a day entirely before the data starts, a day straddling the boundary, and an empty response marking nothing as covered.
This commit is contained in:
1 parent
79215e7623
commit
3612e662e7
12 files changed
+115
-2
No files matched your search
+18
-1
@@ -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
|
||||
}
|
||||
}
|
||||
+73
@@ -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 } }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -171,4 +171,6 @@
|
||||
|
||||
<string name="prefs_other_switch_cw_tone_shift">Desplazar tonos CW fuera de rango</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">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.</string>
|
||||
<string name="amsat_no_report_legend">Sin informes</string>
|
||||
<string name="amsat_no_data_legend">Sin datos</string>
|
||||
</resources>
|
||||
@@ -300,4 +300,6 @@
|
||||
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
||||
<string name="prefs_other_switch_cw_tone_shift">Geser nada CW di luar rentang</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">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.</string>
|
||||
<string name="amsat_no_report_legend">Tidak ada laporan</string>
|
||||
<string name="amsat_no_data_legend">Tidak ada data</string>
|
||||
</resources>
|
||||
@@ -300,4 +300,6 @@
|
||||
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
||||
<string name="prefs_other_switch_cw_tone_shift">Geser nada CW di luar rentang</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">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.</string>
|
||||
<string name="amsat_no_report_legend">Tidak ada laporan</string>
|
||||
<string name="amsat_no_data_legend">Tidak ada data</string>
|
||||
</resources>
|
||||
@@ -171,4 +171,6 @@
|
||||
|
||||
<string name="prefs_other_switch_cw_tone_shift">Сдвигать CW-тоны вне диапазона</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW анализирует только 400-1200 Гц. Если включено, тон вне этого диапазона переносится внутрь перед декодированием; тон внутри диапазона не изменяется.</string>
|
||||
<string name="amsat_no_report_legend">Нет отчётов</string>
|
||||
<string name="amsat_no_data_legend">Нет данных</string>
|
||||
</resources>
|
||||
@@ -171,4 +171,6 @@
|
||||
|
||||
<string name="prefs_other_switch_cw_tone_shift">පරාසයෙන් පිටත CW ස්වර මාරු කරන්න</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW විශ්ලේෂණය කරන්නේ 400-1200 Hz පමණි. සක්රීය විට, එම පරාසයෙන් පිටත ස්වරයක් විකේතනයට පෙර පරාසය තුළට ගෙන එයි; දැනටමත් පරාසය තුළ ඇති ස්වරයක් වෙනස් නොකරයි.</string>
|
||||
<string name="amsat_no_report_legend">වාර්තා නැත</string>
|
||||
<string name="amsat_no_data_legend">දත්ත නැත</string>
|
||||
</resources>
|
||||
@@ -312,4 +312,6 @@
|
||||
|
||||
<string name="prefs_other_switch_cw_tone_shift">Aralık dışı CW tonlarını kaydır</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">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.</string>
|
||||
<string name="amsat_no_report_legend">Rapor yok</string>
|
||||
<string name="amsat_no_data_legend">Veri yok</string>
|
||||
</resources>
|
||||
@@ -171,4 +171,6 @@
|
||||
|
||||
<string name="prefs_other_switch_cw_tone_shift">Зсувати CW-тони поза діапазоном</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW аналізує лише 400-1200 Гц. Якщо увімкнено, тон поза цим діапазоном переноситься в нього перед декодуванням; тон, що вже в діапазоні, не змінюється.</string>
|
||||
<string name="amsat_no_report_legend">Немає звітів</string>
|
||||
<string name="amsat_no_data_legend">Немає даних</string>
|
||||
</resources>
|
||||
@@ -302,4 +302,6 @@
|
||||
<string name="prefs_other_compass_offset_elev">雷达罗盘偏移 (仰角)</string>
|
||||
<string name="prefs_other_switch_cw_tone_shift">搬移超出范围的 CW 音调</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW 仅分析 400-1200 Hz。开启后,超出该范围的音调会先搬移到范围内再解码;已在范围内的音调不作处理。</string>
|
||||
<string name="amsat_no_report_legend">无人上报</string>
|
||||
<string name="amsat_no_data_legend">无数据</string>
|
||||
</resources>
|
||||
@@ -335,4 +335,6 @@
|
||||
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string>
|
||||
<string name="prefs_other_switch_cw_tone_shift">Shift out-of-range CW tones</string>
|
||||
<string name="prefs_other_cw_tone_shift_help">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.</string>
|
||||
<string name="amsat_no_report_legend">No report</string>
|
||||
<string name="amsat_no_data_legend">No data</string>
|
||||
</resources>
|
||||
+6
-1
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user