feat: the offline FCC database answers during entry, not only at save
ULS was wired into AddQSO alone, so an operator who downloaded it and has no QRZ.com/HamQTH account saw nothing while typing a US call: cty.dat gives the country, the zones, and a 4-character grid that is the ENTITY centroid — a thousand kilometres from the station. The county and the real square were stamped after logging, too late to point an antenna with. The enrichment runs last in the lookup wrapper and only fills blanks, so a provider always wins. ULS carries no name and no address, so it completes a QRZ record and can never replace one. The grid needed a rule of its own. refineGrid keeps what is there unless the new value extends it — correct for a QRZ square, wrong against an entity centroid, which is simply a different square. A per-callsign FCC square therefore beats any 4-character grid, while a 6-character one already present is left untouched. Lat/lon are recomputed only when we actually moved the grid. Portable calls are skipped: W1AW/4 is not what the FCC licensed.
This commit is contained in:
@@ -6370,9 +6370,75 @@ func (a *App) lookupCallsign(callsign string, force bool) (lookup.Result, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.enrichFromULS(&r, callsign)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// enrichFromULS fills a US station's state, county and grid from the offline FCC
|
||||
// database while the operator is still typing.
|
||||
//
|
||||
// The ULS data was only ever applied at SAVE time (applyULSCounty), so an
|
||||
// operator with the database downloaded and no QRZ.com/HamQTH account saw
|
||||
// nothing but cty.dat during entry — country, zones, and a 4-character grid that
|
||||
// is the ENTITY centroid, thousands of km off. The county and a 6-character grid
|
||||
// were stamped silently after logging, too late to steer an antenna by.
|
||||
//
|
||||
// It runs LAST and only fills blanks, so an online provider always wins: ULS
|
||||
// holds no name and no address, so it can complete a QRZ record but never
|
||||
// replace one. The grid goes through refineGrid, which accepts the ULS square
|
||||
// only when it EXTENDS what is already there (JN → JN05JG) and never when it
|
||||
// would contradict it.
|
||||
func (a *App) enrichFromULS(r *lookup.Result, callsign string) {
|
||||
if a.uls == nil {
|
||||
return
|
||||
}
|
||||
switch r.DXCC {
|
||||
case 291, 110, 6: // United States, Hawaii, Alaska
|
||||
default:
|
||||
return
|
||||
}
|
||||
call := strings.ToUpper(strings.TrimSpace(callsign))
|
||||
if r.Callsign != "" {
|
||||
call = strings.ToUpper(strings.TrimSpace(r.Callsign))
|
||||
}
|
||||
if call == "" || strings.Contains(call, "/") {
|
||||
// A portable call is not what the FCC licensed — W1AW/4 is not a row.
|
||||
return
|
||||
}
|
||||
loc, ok := a.uls.Resolve(call)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(r.State) == "" {
|
||||
r.State = loc.State
|
||||
}
|
||||
if strings.TrimSpace(r.County) == "" {
|
||||
r.County = loc.CNTY()
|
||||
}
|
||||
// Grid. refineGrid keeps the ULS square when it extends what is there. It
|
||||
// does NOT cover the case this feature exists for: cty.dat's 4-character
|
||||
// square is the ENTITY centroid, so for a US call it is usually a different
|
||||
// square altogether — refineGrid would keep it, and the operator would go on
|
||||
// pointing an antenna at the middle of the country. A per-callsign ULS square
|
||||
// beats any 4-character one, whatever its letters; a 6-character grid already
|
||||
// present (QRZ) is left alone.
|
||||
newGrid := ""
|
||||
if g := refineGrid(r.Grid, loc.Grid); g != "" && g != r.Grid {
|
||||
newGrid = g
|
||||
} else if len(strings.TrimSpace(r.Grid)) <= 4 && len(strings.TrimSpace(loc.Grid)) >= 6 {
|
||||
newGrid = loc.Grid
|
||||
}
|
||||
if newGrid != "" {
|
||||
r.Grid = newGrid
|
||||
// Lat/lon follow, or distance and azimuth would still be computed from the
|
||||
// centroid the grid no longer says. Only when WE changed the grid: a
|
||||
// provider's own coordinates are more precise than a square's centre.
|
||||
if lat, lon, ok := gridToLatLon(newGrid); ok {
|
||||
r.Lat, r.Lon = lat, lon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenExternalURL opens a URL in the user's default browser. Wails ships
|
||||
// runtime.BrowserOpenURL for exactly this — used by the QRZ.com icon
|
||||
// next to the callsign field, the future Clublog/HamQTH shortcuts, etc.
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"version": "0.21.9",
|
||||
"date": "2026-07-28",
|
||||
"en": [
|
||||
"The offline FCC (ULS) database now answers while you type a US callsign, filling state, county and a 6-character grid. It was only applied when the QSO was saved, so without a QRZ.com or HamQTH account you saw nothing but the country and a coarse grid. Online lookups still win — ULS only fills what is blank.",
|
||||
"CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes."
|
||||
],
|
||||
"fr": [
|
||||
"La base FCC (ULS) hors ligne répond désormais pendant la saisie d'un indicatif américain, en remplissant l'état, le comté et un locator 6 caractères. Elle n'était appliquée qu'à l'enregistrement du QSO : sans compte QRZ.com ou HamQTH, vous ne voyiez que le pays et un locator approximatif. Les recherches en ligne restent prioritaires — l'ULS ne comble que ce qui est vide.",
|
||||
"Les zones CQ et ITU corrigées à la main dans votre profil restent corrigées. Elles proviennent de cty.dat, qui donne les zones de l'entité entière — dans un pays qui en couvre plusieurs, la valeur automatique est fausse et revenait à chaque redémarrage. Elles ne se remplissent désormais que si le champ est vide, et se recalculent quand l'indicatif ou le locator change."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hamlog/internal/uls"
|
||||
)
|
||||
|
||||
// The ULS grid rules, which are NOT the same as refineGrid's.
|
||||
//
|
||||
// cty.dat answers a US call with the ENTITY centroid — a 4-character square in
|
||||
// the middle of the country. refineGrid keeps it (the ULS square is not an
|
||||
// extension of it, it is a different square), which is right for a QRZ grid and
|
||||
// wrong here: a per-callsign FCC square beats any entity centroid. But a
|
||||
// 6-character grid already present came from a provider and must survive.
|
||||
func TestULSGridPreference(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
have string // grid already on the result
|
||||
uls string // grid from the FCC database
|
||||
want string
|
||||
}{
|
||||
{"nothing known → take ULS", "", "EM12AB", "EM12AB"},
|
||||
{"cty.dat entity centroid → ULS wins", "EN90", "EM12AB", "EM12AB"},
|
||||
{"4-char square, same square → ULS extends it", "EM12", "EM12AB", "EM12AB"},
|
||||
{"provider gave 6 chars → untouched", "FN31PR", "EM12AB", "FN31PR"},
|
||||
{"ULS has only 4 chars, provider has 6 → untouched", "FN31PR", "EM12", "FN31PR"},
|
||||
{"ULS empty → untouched", "EN90", "", "EN90"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := c.have
|
||||
if g := refineGrid(got, c.uls); g != "" && g != got {
|
||||
got = g
|
||||
} else if len(got) <= 4 && len(c.uls) >= 6 {
|
||||
got = c.uls
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("%s: have=%q uls=%q → %q, want %q", c.name, c.have, c.uls, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CNTY is what gets written to the QSO's county field, so its shape is part of
|
||||
// the contract: ADIF wants "State,County" and nothing when either half is
|
||||
// missing — a lone county name would be an invalid CNTY on export.
|
||||
func TestULSCountyFormat(t *testing.T) {
|
||||
cases := []struct {
|
||||
loc uls.Location
|
||||
want string
|
||||
}{
|
||||
{uls.Location{State: "MA", County: "Middlesex"}, "MA,Middlesex"},
|
||||
{uls.Location{State: "", County: "Middlesex"}, ""},
|
||||
{uls.Location{State: "MA", County: ""}, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.loc.CNTY(); got != c.want {
|
||||
t.Errorf("Location%+v.CNTY() = %q, want %q", c.loc, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user