feat: Added support for US Counties in OpsLog / Extra feature with DXHunter
This commit is contained in:
@@ -54,6 +54,7 @@ import (
|
||||
"hamlog/internal/settings"
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/steppir"
|
||||
"hamlog/internal/uls"
|
||||
"hamlog/internal/ultrabeam"
|
||||
"hamlog/internal/winkeyer"
|
||||
|
||||
@@ -437,6 +438,7 @@ type App struct {
|
||||
clusterEventCh chan clusterEvent
|
||||
clusterDropped int64 // spots/lines dropped when the queue was full (atomic)
|
||||
pota *pota.Cache
|
||||
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
|
||||
awardRefs *awardref.Repo
|
||||
qslTemplates *qslcard.Repo
|
||||
operating *operating.Repo
|
||||
@@ -761,6 +763,14 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
a.settings.SetProfile(active.ID)
|
||||
// US county resolver — its own local SQLite (data/uls.db), populated on demand
|
||||
// by DownloadULSCounties. Opening (creating an empty store) is cheap and never
|
||||
// fatal: county resolution simply stays inert until the operator downloads it.
|
||||
if store, err := uls.Open(filepath.Join(a.dataDir, "uls.db")); err != nil {
|
||||
applog.Printf("uls: open failed (county resolution disabled): %v", err)
|
||||
} else {
|
||||
a.uls = store
|
||||
}
|
||||
a.awardRefs = awardref.NewRepo(conn)
|
||||
a.qslTemplates = qslcard.NewRepo(conn)
|
||||
a.migrateAwardDefs() // upgrade legacy award definitions (enable + new fields)
|
||||
@@ -1815,6 +1825,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
}()
|
||||
a.applyStationDefaults(&q, true)
|
||||
a.applyDXCCNumber(&q)
|
||||
a.applyULSCounty(&q) // fill blank US county/grid from the offline ULS store
|
||||
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
|
||||
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
|
||||
a.applyQSLDefaults(&q)
|
||||
@@ -4244,7 +4255,7 @@ func freeAwardCode(code string, taken map[string]int) string {
|
||||
// builtinRefsVersion is bumped whenever the built-in reference data changes
|
||||
// (e.g. the West Malaysia 155→299 fix) so existing installs re-seed the
|
||||
// derived lists. Bump this after correcting BuiltinRefs / the DXCC name table.
|
||||
const builtinRefsVersion = "2"
|
||||
const builtinRefsVersion = "3"
|
||||
|
||||
// seedBuiltinReferences populates the reference lists of built-in awards.
|
||||
// - First run (no version stored), or already up to date: seed only awards that
|
||||
@@ -7793,6 +7804,155 @@ func (a *App) GetSlotStats() qso.SlotStats {
|
||||
return st
|
||||
}
|
||||
|
||||
// --- US Counties (ULS) -----------------------------------------------------
|
||||
|
||||
// applyULSCounty fills a US QSO's blank county (and blank grid) from the offline
|
||||
// ULS store. Non-US QSOs, an un-downloaded store, or already-filled fields make
|
||||
// it a no-op — a value the operator or QRZ supplied is never overwritten, since
|
||||
// the ZIP-derived county is only ~98% and the logged one is usually better. The
|
||||
// award normalises whatever shape cnty holds, so stamping "ST,County" is safe.
|
||||
func (a *App) applyULSCounty(q *qso.QSO) {
|
||||
if a.uls == nil || q.DXCC == nil {
|
||||
return
|
||||
}
|
||||
switch *q.DXCC {
|
||||
case 291, 110, 6: // United States, Hawaii, Alaska
|
||||
default:
|
||||
return
|
||||
}
|
||||
haveCounty := strings.TrimSpace(q.County) != ""
|
||||
haveGrid := strings.TrimSpace(q.Grid) != ""
|
||||
if haveCounty && haveGrid {
|
||||
return
|
||||
}
|
||||
loc, ok := a.uls.Resolve(q.Callsign)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !haveCounty {
|
||||
q.County = loc.CNTY()
|
||||
if strings.TrimSpace(q.State) == "" {
|
||||
q.State = loc.State
|
||||
}
|
||||
}
|
||||
if !haveGrid && loc.Grid != "" {
|
||||
q.Grid = loc.Grid
|
||||
}
|
||||
}
|
||||
|
||||
// ULSStatusResult reports whether the county database is loaded, and how fresh.
|
||||
type ULSStatusResult struct {
|
||||
Count int `json:"count"`
|
||||
UpdatedAt string `json:"updated_at"` // RFC3339, empty if never downloaded
|
||||
}
|
||||
|
||||
// ULSStatus returns the state of the offline US county database.
|
||||
func (a *App) ULSStatus() ULSStatusResult {
|
||||
if a.uls == nil {
|
||||
return ULSStatusResult{}
|
||||
}
|
||||
var updated string
|
||||
if t := a.uls.UpdatedAt(); !t.IsZero() {
|
||||
updated = t.Format(time.RFC3339)
|
||||
}
|
||||
return ULSStatusResult{Count: a.uls.Count(), UpdatedAt: updated}
|
||||
}
|
||||
|
||||
// DownloadULSCounties downloads and (re)builds the offline US county database in
|
||||
// the background, emitting "uls:progress" ({stage,pct}) and "uls:done" ({ok,
|
||||
// error,count}) so the Settings panel can show progress and reuse one flow.
|
||||
func (a *App) DownloadULSCounties() error {
|
||||
if a.uls == nil {
|
||||
return fmt.Errorf("county database unavailable")
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
applog.Printf("PANIC in DownloadULSCounties: %v\n%s", r, debug.Stack())
|
||||
wruntime.EventsEmit(a.ctx, "uls:done", map[string]any{"ok": false, "error": fmt.Sprintf("%v", r)})
|
||||
}
|
||||
}()
|
||||
err := a.uls.Import(a.ctx, a.dataDir, func(stage string, pct int) {
|
||||
wruntime.EventsEmit(a.ctx, "uls:progress", map[string]any{"stage": stage, "pct": pct})
|
||||
})
|
||||
res := map[string]any{"ok": err == nil, "count": a.uls.Count()}
|
||||
if err != nil {
|
||||
applog.Printf("uls: import failed: %v", err)
|
||||
res["error"] = err.Error()
|
||||
} else {
|
||||
applog.Printf("uls: import complete — %d callsigns", a.uls.Count())
|
||||
}
|
||||
wruntime.EventsEmit(a.ctx, "uls:done", res)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackfillUSCountiesResult summarises a bulk county/grid backfill over the log.
|
||||
type BackfillUSCountiesResult struct {
|
||||
Scanned int `json:"scanned"` // US QSOs examined
|
||||
County int `json:"county"` // QSOs that gained a county
|
||||
Grid int `json:"grid"` // QSOs that gained a grid
|
||||
}
|
||||
|
||||
// BackfillUSCounties resolves county (and grid) for existing US QSOs that lack
|
||||
// them, using the offline ULS store. It only FILLS blanks — an existing county
|
||||
// is left as the operator logged it. Returns how many rows were updated.
|
||||
func (a *App) BackfillUSCounties() (BackfillUSCountiesResult, error) {
|
||||
var res BackfillUSCountiesResult
|
||||
if a.uls == nil || a.uls.Count() == 0 {
|
||||
return res, fmt.Errorf("county database not downloaded")
|
||||
}
|
||||
if a.qso == nil {
|
||||
return res, fmt.Errorf("db not initialized")
|
||||
}
|
||||
rows, err := a.qso.List(a.ctx, qso.ListFilter{Limit: 1_000_000})
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
for i := range rows {
|
||||
q := &rows[i]
|
||||
if q.DXCC == nil {
|
||||
continue
|
||||
}
|
||||
switch *q.DXCC {
|
||||
case 291, 110, 6:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
res.Scanned++
|
||||
needCounty := strings.TrimSpace(q.County) == ""
|
||||
needGrid := strings.TrimSpace(q.Grid) == ""
|
||||
if !needCounty && !needGrid {
|
||||
continue
|
||||
}
|
||||
loc, ok := a.uls.Resolve(q.Callsign)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
changed := false
|
||||
if needCounty && loc.CNTY() != "" {
|
||||
q.County = loc.CNTY()
|
||||
if strings.TrimSpace(q.State) == "" {
|
||||
q.State = loc.State
|
||||
}
|
||||
res.County++
|
||||
changed = true
|
||||
}
|
||||
if needGrid && loc.Grid != "" {
|
||||
q.Grid = loc.Grid
|
||||
res.Grid++
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
if err := a.qso.Update(a.ctx, *q); err != nil {
|
||||
applog.Printf("uls backfill: update qso %d failed: %v", q.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.invalidateAwardStats() // county changes affect the US Counties matrix
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// DownloadConfirmations pulls confirmed QSOs from a service and updates the
|
||||
// matching local QSOs' received status. LoTW only for now (the canonical
|
||||
// confirmation system); runs in the background emitting the same
|
||||
|
||||
Reference in New Issue
Block a user