feat(cluster): grids from the UDP decodes, plus spotter continent and LoTW
Backend groundwork; the columns and filters that consume it come next. GRIDS. A CQ is the one WSJT-X message that carries a locator, and wsjtSender was throwing that token away. It is now returned, validated as a real field+square, and remembered per callsign. This is the ONLY grid source available: a DX-cluster line carries the spotter's grid at best and never the DX's, and a per-callsign QRZ lookup under an RBN firehose is not a trade worth making. So grids are known for the stations this receiver decoded - which is exactly the FT8/FT4 watering hole an operator is looking at while grid chasing. RR73 is why the grid is validated rather than pattern-matched. R is inside A-R and 73 inside 00-99, so a sign-off satisfies the Maidenhead shape exactly and would have planted a grid that does not exist into the index, silently. NEW GRID keys on "GRID|MODE" with the mode put through the same normMode as everything else, so the "group digital modes" option decides whether a grid worked on FT8 is still new on FT4 - one rule, no branch. Grids are truncated to four characters: a log holds a mix of JN36 and JN36QU, and without that the same square is new forever, once per subsquare. On cost, which was the condition: one more DISTINCT scan when the status snapshot is rebuilt, then map lookups per spot. The same shape as the county and POTA sets it sits beside, and the snapshot exists precisely so a spot batch never touches the logbook. Spotter continent and the LoTW flag come from tables already in memory - the DXCC prefix table and ARRL's user list - so they cost a lookup each. The spotter continent answers a different question from the DX's: whether anyone near you is hearing this at all.
This commit is contained in:
@@ -3002,3 +3002,43 @@ func IsFilterable(column string) bool { return filterableColumns[column] }
|
||||
// column of their own and live in extras_json.
|
||||
func IsBulkEditableExtra(field string) bool { _, ok := bulkEditableExtras[field]; return ok }
|
||||
func IsFilterableExtra(field string) bool { _, ok := filterableExtras[field]; return ok }
|
||||
|
||||
// WorkedGridKeys returns the set of "GRID|MODE" keys already in the log, where
|
||||
// GRID is the 4-character field+square and MODE has been through normMode.
|
||||
//
|
||||
// The mode is part of the key, and normMode is what makes the DX-cluster
|
||||
// "group digital modes" option apply to grids for free: with grouping on, FT8
|
||||
// and FT4 both normalise to the same token, so a grid worked on FT8 is not new
|
||||
// on FT4; with it off they stay separate keys and it is.
|
||||
//
|
||||
// Truncated to four characters on the way in. A log holds a mix of JN36 and
|
||||
// JN36QU depending on where each QSO came from, and grid chasing is a
|
||||
// field+square game — without the truncation the same square counts as new
|
||||
// forever, once per subsquare.
|
||||
func (r *Repo) WorkedGridKeys(ctx context.Context, normMode func(string) string) (map[string]struct{}, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT DISTINCT COALESCE(gridsquare,''), COALESCE(mode,'') FROM qso
|
||||
WHERE gridsquare IS NOT NULL AND gridsquare != ''`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[string]struct{}, 4096)
|
||||
for rows.Next() {
|
||||
var grid, mode string
|
||||
if err := rows.Scan(&grid, &mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grid = strings.ToUpper(strings.TrimSpace(grid))
|
||||
if len(grid) < 4 {
|
||||
continue
|
||||
}
|
||||
grid = grid[:4]
|
||||
mode = strings.ToUpper(strings.TrimSpace(mode))
|
||||
if normMode != nil && mode != "" {
|
||||
mode = normMode(mode)
|
||||
}
|
||||
out[grid+"|"+mode] = struct{}{}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user