mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-04 11:37:22 +00:00
feat(vucc): per operated-grid VUCC counting
GridQso gains myGrid (ADIF MY_GRIDSQUARE) captured per QSO during LoTW parsing (handles alphabetical field order + EOR reset) and persisted as JSON key 'mg' (absent in pre-myGrid data -> null). Legacy syncs keep the global VUCC count; one resync unlocks per-grid counting.
This commit is contained in:
1 parent
6a6399b606
commit
25b982ac59
4 files changed
+37
-3
No files matched your search
+11
-1
@@ -169,6 +169,7 @@ class LoTWRepository : ILoTWRepository {
|
|||||||
var country: String? = null
|
var country: String? = null
|
||||||
var cqz: Int? = null
|
var cqz: Int? = null
|
||||||
var state: String? = null
|
var state: String? = null
|
||||||
|
var myGrid: String? = null
|
||||||
val gridsInRecord = mutableListOf<String>()
|
val gridsInRecord = mutableListOf<String>()
|
||||||
|
|
||||||
fun emitRecord() {
|
fun emitRecord() {
|
||||||
@@ -177,7 +178,8 @@ class LoTWRepository : ILoTWRepository {
|
|||||||
val qso = com.rtbishop.look4sat.core.domain.model.GridQso(
|
val qso = com.rtbishop.look4sat.core.domain.model.GridQso(
|
||||||
call = call, epochMs = epochMs, satName = satName,
|
call = call, epochMs = epochMs, satName = satName,
|
||||||
mode = mode, bandUp = bandUp, bandDown = bandDown,
|
mode = mode, bandUp = bandUp, bandDown = bandDown,
|
||||||
dxcc = dxcc, country = country, cqz = cqz, state = state
|
dxcc = dxcc, country = country, cqz = cqz, state = state,
|
||||||
|
myGrid = myGrid
|
||||||
)
|
)
|
||||||
for (grid in gridsInRecord) {
|
for (grid in gridsInRecord) {
|
||||||
result.getOrPut(grid) { mutableListOf() }.add(qso)
|
result.getOrPut(grid) { mutableListOf() }.add(qso)
|
||||||
@@ -188,6 +190,7 @@ class LoTWRepository : ILoTWRepository {
|
|||||||
propMode = null; call = ""; qsoDate = ""; timeOn = ""
|
propMode = null; call = ""; qsoDate = ""; timeOn = ""
|
||||||
satName = ""; mode = ""; bandUp = ""; bandDown = ""
|
satName = ""; mode = ""; bandUp = ""; bandDown = ""
|
||||||
dxcc = null; country = null; cqz = null; state = null
|
dxcc = null; country = null; cqz = null; state = null
|
||||||
|
myGrid = null
|
||||||
gridsInRecord.clear()
|
gridsInRecord.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +225,13 @@ class LoTWRepository : ILoTWRepository {
|
|||||||
cqz = adifValue(line).toIntOrNull()
|
cqz = adifValue(line).toIntOrNull()
|
||||||
line.startsWith("<STATE:") ->
|
line.startsWith("<STATE:") ->
|
||||||
state = adifValue(line).trim().ifBlank { null }?.let { normalizeState(it) }
|
state = adifValue(line).trim().ifBlank { null }?.let { normalizeState(it) }
|
||||||
|
line.startsWith("<MY_GRIDSQUARE:") -> {
|
||||||
|
// Own-station grid (must not be confused with GRIDSQUARE —
|
||||||
|
// the opposite station's grid). Recorded per QSO so awards
|
||||||
|
// can be counted per operated grid.
|
||||||
|
val value = adifValue(line)
|
||||||
|
if (value.length >= 4) myGrid = value.take(4).uppercase()
|
||||||
|
}
|
||||||
line.startsWith("<GRIDSQUARE:") || line.startsWith("<VUCC_GRIDS:") -> {
|
line.startsWith("<GRIDSQUARE:") || line.startsWith("<VUCC_GRIDS:") -> {
|
||||||
// VUCC_GRIDS holds a comma-separated list of grids
|
// VUCC_GRIDS holds a comma-separated list of grids
|
||||||
// ("EN52en,EN53fa"), up to four for contacts spanning
|
// ("EN52en,EN53fa"), up to four for contacts spanning
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ class SettingsRepo(
|
|||||||
dxcc = o.optInt("dx", 0).takeIf { it > 0 },
|
dxcc = o.optInt("dx", 0).takeIf { it > 0 },
|
||||||
country = o.optString("cty").ifBlank { null },
|
country = o.optString("cty").ifBlank { null },
|
||||||
cqz = o.optInt("cq", 0).takeIf { it > 0 },
|
cqz = o.optInt("cq", 0).takeIf { it > 0 },
|
||||||
state = o.optString("st").ifBlank { null }
|
state = o.optString("st").ifBlank { null },
|
||||||
|
// Own-grid field: absent in pre-myGrid data -> null.
|
||||||
|
myGrid = o.optString("mg").ifBlank { null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (list.isNotEmpty()) result[grid] = list
|
if (list.isNotEmpty()) result[grid] = list
|
||||||
@@ -232,6 +234,7 @@ class SettingsRepo(
|
|||||||
.put("cty", q.country ?: "")
|
.put("cty", q.country ?: "")
|
||||||
.put("cq", q.cqz ?: 0)
|
.put("cq", q.cqz ?: 0)
|
||||||
.put("st", q.state ?: "")
|
.put("st", q.state ?: "")
|
||||||
|
.put("mg", q.myGrid ?: "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
root.put(grid, array)
|
root.put(grid, array)
|
||||||
|
|||||||
+19
@@ -3,6 +3,7 @@ package com.rtbishop.look4sat.core.data.repository
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LoTWRepositoryTest {
|
class LoTWRepositoryTest {
|
||||||
@@ -150,6 +151,24 @@ class LoTWRepositoryTest {
|
|||||||
assertNull(repo.parseConfirmedGridQsos("<HTML>Username/password incorrect</HTML>"))
|
assertNull(repo.parseConfirmedGridQsos("<HTML>Username/password incorrect</HTML>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parseQsosCapturesOwnGridPerQso() {
|
||||||
|
// MY_GRIDSQUARE arrives BEFORE PROP_MODE (ADIF fields are emitted
|
||||||
|
// alphabetically); the per-record buffer must still attach it to the QSO.
|
||||||
|
val qso1 = "<CALL:5>A50QO\n<QSO_DATE:8>20260820\n<MY_GRIDSQUARE:4>OL62\n" +
|
||||||
|
"<PROP_MODE:3>SAT\n<SAT_NAME:5>FO-29\n<MODE:2>CW\n" +
|
||||||
|
"<BAND:3>70CM\n<BAND_RX:3>2M\n<GRIDSQUARE:4>NL47\n<EOR>\n"
|
||||||
|
val qso2 = "<CALL:6>BG7ZFK\n<QSO_DATE:8>20260819\n<MY_GRIDSQUARE:4>OL72\n" +
|
||||||
|
"<PROP_MODE:3>SAT\n<SAT_NAME:5>SO-50\n<MODE:3>FM\n" +
|
||||||
|
"<BAND:3>70CM\n<BAND_RX:3>2M\n<GRIDSQUARE:4>OK48\n<EOR>\n"
|
||||||
|
val result = repo.parseConfirmedGridQsos(report(qso1, qso2))!!
|
||||||
|
assertEquals("OL62", result["NL47"]!!.first().myGrid)
|
||||||
|
assertEquals("OL72", result["OK48"]!!.first().myGrid)
|
||||||
|
// Ground QSO (no PROP_MODE=SAT) must not leak its MY_GRIDSQUARE.
|
||||||
|
val ground = "<CALL:6>BA7OPF\n<QSO_DATE:8>20260821\n<MY_GRIDSQUARE:4>OL62\n<GRIDSQUARE:4>NL47\n<EOR>\n"
|
||||||
|
assertTrue(repo.parseConfirmedGridQsos(report(qso1, ground))!!.values.all { it.all { q -> q.myGrid == "OL62" } })
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region parseRoamedGrids (MY_GRIDSQUARE = grids the account operated from)
|
// region parseRoamedGrids (MY_GRIDSQUARE = grids the account operated from)
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ data class GridQso(
|
|||||||
val dxcc: Int? = null,
|
val dxcc: Int? = null,
|
||||||
val country: String? = null,
|
val country: String? = null,
|
||||||
val cqz: Int? = null,
|
val cqz: Int? = null,
|
||||||
val state: String? = null
|
val state: String? = null,
|
||||||
|
/** 4-char grid the station itself operated from (ADIF MY_GRIDSQUARE). */
|
||||||
|
val myGrid: String? = null
|
||||||
) {
|
) {
|
||||||
/** Short uplink/downlink band label ("U/V", "V/A"), or "" when unknown. */
|
/** Short uplink/downlink band label ("U/V", "V/A"), or "" when unknown. */
|
||||||
val bandLabel: String
|
val bandLabel: String
|
||||||
|
|||||||
Reference in new issue
Block a user