feat: docked watch-list panel, and auto-call learns the orthogonal markers
The watch list was a tab, and an operator working FT8 lives on the decodes one: a station they had asked to be told about turned up on a screen they were not looking at. The same answer is now docked in the widget strip, above the tabs, reduced to what is worth acting on — on the air and still needed, one row per band and mode, with the cluster's own NEW DXCC / NEW BAND / NEW SLOT badge and a click that tunes. Off by default. The "active and needed" answer costs a debounced query per visible slot, so it is written once (lib/watchlistSpots) and the tab uses it too. Auto-call: - It answers a new prefix, county, state, square or park. Those markers are orthogonal to the entity, they ranked as nothing-needed, and the engine sat through a never-worked WPX prefix calling CQ. New rung at the foot of the ladder, gated by the chase switches the badges use — which meant making those switches portable, since the backend cannot read localStorage. - It calls THROUGH a pileup. Giving up the moment the DX answered somebody else is precisely how a queue is not worked; the call and miss counters already bound the effort, and a station in mid-exchange is still never chosen as a new target. The PSK Reporter panel now follows the station auto-call is waiting for: the analysis takes a history query and a period or two to fill, so starting it when the DX comes free is starting it too late. Callbook lookup: a compound callsign with a page of its OWN keeps that page's location. QRZ files HP/WE9G under exactly that form, with the Panama square the station is operating from, and the rule that drops a home address from a portable call was throwing it away. The record's own country tells an operation's page from a home page. Changelog: entries may open with [NEW], drawn as a pill in the What's new dialog — a release is mostly fixes and the two or three genuinely new things should not have to be found by reading all of it.
This commit is contained in:
@@ -395,6 +395,47 @@ func titleCase(s string) string {
|
||||
// lives, and keeps the ones that say WHO they are — name, address, QSL route.
|
||||
// A portable operator's cards still go to the home address, so that address is
|
||||
// not wrong; their county is.
|
||||
// sameEntityName reports whether two country names denote the same entity.
|
||||
//
|
||||
// The two come from different vocabularies — the callbook writes "Germany"
|
||||
// where cty.dat writes "Fed. Rep. of Germany", "United States" where the ADIF
|
||||
// list says "United States of America" — so they are compared on their
|
||||
// significant words with the boilerplate of officialdom removed, and one
|
||||
// containing the other counts as a match.
|
||||
//
|
||||
// Deliberately generous. Getting it wrong in the strict direction discards a
|
||||
// correct grid, which is the fault this exists to fix; getting it wrong in the
|
||||
// generous direction keeps a location from a neighbouring entity, which is the
|
||||
// state of every callbook record that has no page for the portable call anyway.
|
||||
func sameEntityName(a, b string) bool {
|
||||
na, nb := entityKey(a), entityKey(b)
|
||||
if na == "" || nb == "" {
|
||||
return false
|
||||
}
|
||||
return na == nb || strings.Contains(na, nb) || strings.Contains(nb, na)
|
||||
}
|
||||
|
||||
var entityNoise = map[string]bool{
|
||||
"THE": true, "OF": true, "FED": true, "REP": true, "REPUBLIC": true,
|
||||
"FEDERAL": true, "FEDERATION": true, "DEM": true, "DEMOCRATIC": true,
|
||||
"STATE": true, "KINGDOM": true, "AMERICA": true,
|
||||
}
|
||||
|
||||
// entityKey reduces a country name to its significant letters.
|
||||
func entityKey(s string) string {
|
||||
s = strings.ToUpper(strings.TrimSpace(s))
|
||||
var b strings.Builder
|
||||
for _, tok := range strings.FieldsFunc(s, func(r rune) bool {
|
||||
return r == ' ' || r == '.' || r == ',' || r == '-'
|
||||
}) {
|
||||
if entityNoise[tok] {
|
||||
continue
|
||||
}
|
||||
b.WriteString(tok)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func clearHomeLocation(r *Result) {
|
||||
r.Country, r.Continent = "", ""
|
||||
r.CQZ, r.ITUZ, r.DXCC = 0, 0, 0
|
||||
@@ -430,11 +471,23 @@ func fillFromDXCC(r *Result, dxcc DXCCResolver) bool {
|
||||
//
|
||||
// Same-entity portables (F4BPO/P, W2RE/2) are untouched: the entities match,
|
||||
// and there the home details ARE where the operator is.
|
||||
// UNLESS THE RECORD IS ABOUT THE OPERATION ITSELF.
|
||||
//
|
||||
// Some compound calls have a callbook page of their own, filed under the
|
||||
// slashed form and describing where the station actually is: QRZ's HP/WE9G
|
||||
// carries Altos del Maria, Panama, square EJ98xq. Clearing that threw away
|
||||
// the one field the lookup existed to find, and the QSO was logged with no
|
||||
// grid at all while the page plainly showed one.
|
||||
//
|
||||
// The record's OWN country is what tells the two apart: a page for the
|
||||
// operation names the entity being operated from, a home page names home.
|
||||
if dxccNum != 0 && strings.ContainsRune(r.Callsign, '/') && !saysNothingAboutLocation(r.Callsign) {
|
||||
if home := homeCall(r.Callsign); home != "" && home != r.Callsign {
|
||||
if homeNum, _, _, _, _, _, _, homeOK := dxcc.Resolve(home); homeOK && homeNum != 0 && homeNum != dxccNum {
|
||||
clearHomeLocation(r)
|
||||
filled = true
|
||||
if !sameEntityName(r.Country, country) {
|
||||
clearHomeLocation(r)
|
||||
filled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +91,60 @@ func TestSameEntityPortableKeepsItsLocation(t *testing.T) {
|
||||
t.Errorf("lat/lon = %v/%v — the precise home position was replaced by the entity centroid", r.Lat, r.Lon)
|
||||
}
|
||||
}
|
||||
|
||||
// The other half again, and the one an operator reported: a compound call with
|
||||
// a callbook page OF ITS OWN. QRZ files HP/WE9G under that exact form, with the
|
||||
// Panama address and square the station is actually operating from — and the
|
||||
// QSO was being logged with no grid at all while the page plainly showed one.
|
||||
func TestACompoundCallWithItsOwnPageKeepsThatPagesLocation(t *testing.T) {
|
||||
dxcc := testDXCC()
|
||||
dxcc["HP/WE9G"] = struct {
|
||||
num int
|
||||
country string
|
||||
cont string
|
||||
cqz, ituz int
|
||||
lat, lon float64
|
||||
}{num: 88, country: "Panama", cont: "NA", cqz: 7, ituz: 11, lat: 8.5, lon: -80.0}
|
||||
dxcc["WE9G"] = struct {
|
||||
num int
|
||||
country string
|
||||
cont string
|
||||
cqz, ituz int
|
||||
lat, lon float64
|
||||
}{num: 291, country: "United States", cont: "NA", cqz: 5, ituz: 8, lat: 39.8, lon: -98.5}
|
||||
|
||||
r := Result{
|
||||
Callsign: "HP/WE9G",
|
||||
Name: "Richard B",
|
||||
Country: "Panama", // the RECORD's own country: this page is the operation
|
||||
Grid: "EJ98xq",
|
||||
Lat: 8.686667, Lon: -80.043333,
|
||||
}
|
||||
fillFromDXCC(&r, dxcc)
|
||||
|
||||
if r.Grid != "EJ98xq" {
|
||||
t.Errorf("grid = %q, want EJ98xq — the page describes the operation, not a home address", r.Grid)
|
||||
}
|
||||
if r.Lat != 8.686667 || r.Lon != -80.043333 {
|
||||
t.Errorf("lat/lon = %v/%v — the station's own position was replaced by the entity centroid", r.Lat, r.Lon)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameEntityNameAcrossVocabularies(t *testing.T) {
|
||||
same := [][2]string{
|
||||
{"Germany", "Fed. Rep. of Germany"},
|
||||
{"United States", "United States of America"},
|
||||
{"Panama", "Panama"},
|
||||
{"Kosovo", "Republic of Kosovo"},
|
||||
}
|
||||
for _, p := range same {
|
||||
if !sameEntityName(p[0], p[1]) {
|
||||
t.Errorf("%q and %q read as different entities", p[0], p[1])
|
||||
}
|
||||
}
|
||||
for _, p := range [][2]string{{"Costa Rica", "United States"}, {"France", "Belgium"}, {"", "Panama"}} {
|
||||
if sameEntityName(p[0], p[1]) {
|
||||
t.Errorf("%q and %q read as the same entity", p[0], p[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user