award
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
// Package awardref stores and updates award reference lists (POTA parks, SOTA
|
||||
// summits, WWFF references, …). These provide award totals, reference names,
|
||||
// and per-DXCC filtering. Lists are downloaded from each program's public file.
|
||||
package awardref
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// allCols is the column list shared by read queries so they stay in sync.
|
||||
const allCols = `ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias`
|
||||
|
||||
func encodeDXCCList(l []int) string {
|
||||
if len(l) == 0 {
|
||||
return ""
|
||||
}
|
||||
b, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func decodeDXCCList(s string) []int {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
var l []int
|
||||
if json.Unmarshal([]byte(s), &l) != nil {
|
||||
return nil
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// scanRef reads one row selected with allCols into a Ref.
|
||||
func scanRef(rows *sql.Rows) (Ref, error) {
|
||||
var r Ref
|
||||
var dxccList string
|
||||
var valid int
|
||||
if err := rows.Scan(&r.Code, &r.Name, &r.DXCC, &r.Group, &r.SubGrp,
|
||||
&dxccList, &r.Pattern, &valid, &r.ValidFrom, &r.ValidTo,
|
||||
&r.Score, &r.Bonus, &r.GridSquare, &r.Alias); err != nil {
|
||||
return r, err
|
||||
}
|
||||
r.DXCCList = decodeDXCCList(dxccList)
|
||||
r.Valid = valid != 0
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Ref is one award reference. The first five fields are the original schema;
|
||||
// the rest mirror Log4OM's per-reference editor (group/subgroup, multi-DXCC,
|
||||
// per-reference regex, validity window, score/bonus, grid, alias).
|
||||
type Ref struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"` // description
|
||||
DXCC int `json:"dxcc"` // primary entity (kept for compatibility / fast filter)
|
||||
Group string `json:"group"`
|
||||
SubGrp string `json:"subgrp"`
|
||||
|
||||
DXCCList []int `json:"dxcc_list,omitempty"` // all entities this ref is valid for
|
||||
Pattern string `json:"pattern,omitempty"` // per-reference Go regexp
|
||||
Valid bool `json:"valid"` // reference enabled
|
||||
ValidFrom string `json:"valid_from,omitempty"`
|
||||
ValidTo string `json:"valid_to,omitempty"`
|
||||
Score int `json:"score,omitempty"`
|
||||
Bonus int `json:"bonus,omitempty"`
|
||||
GridSquare string `json:"gridsquare,omitempty"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// Repo accesses the award_references table.
|
||||
type Repo struct{ db *sql.DB }
|
||||
|
||||
// NewRepo builds a reference repo on the given connection.
|
||||
func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }
|
||||
|
||||
// ReplaceAll atomically replaces every reference for one award.
|
||||
func (r *Repo) ReplaceAll(ctx context.Context, awardCode string, refs []Ref) (int, error) {
|
||||
code := strings.ToUpper(strings.TrimSpace(awardCode))
|
||||
if code == "" {
|
||||
return 0, fmt.Errorf("empty award code")
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM award_references WHERE award_code = ?`, code); err != nil {
|
||||
return 0, fmt.Errorf("clear refs: %w", err)
|
||||
}
|
||||
stmt, err := tx.PrepareContext(ctx,
|
||||
`INSERT OR REPLACE INTO award_references
|
||||
(award_code, ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
n := 0
|
||||
for _, ref := range refs {
|
||||
rc := strings.ToUpper(strings.TrimSpace(ref.Code))
|
||||
if rc == "" {
|
||||
continue
|
||||
}
|
||||
// A bulk-replaced list is the authoritative enabled set: store every
|
||||
// row as valid. Per-reference disabling is done through Upsert.
|
||||
if _, err := stmt.ExecContext(ctx, code, rc, strings.TrimSpace(ref.Name), ref.DXCC, ref.Group, ref.SubGrp,
|
||||
encodeDXCCList(ref.DXCCList), ref.Pattern, 1, ref.ValidFrom, ref.ValidTo,
|
||||
ref.Score, ref.Bonus, ref.GridSquare, ref.Alias); err != nil {
|
||||
return 0, fmt.Errorf("insert ref %s: %w", rc, err)
|
||||
}
|
||||
n++
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Count returns how many references an award has stored.
|
||||
func (r *Repo) Count(ctx context.Context, awardCode string) (int, error) {
|
||||
var n int
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*) FROM award_references WHERE award_code = ?`,
|
||||
strings.ToUpper(strings.TrimSpace(awardCode))).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Counts returns reference counts for every award.
|
||||
func (r *Repo) Counts(ctx context.Context) (map[string]int, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT award_code, COUNT(*) FROM award_references GROUP BY award_code`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]int{}
|
||||
for rows.Next() {
|
||||
var code string
|
||||
var n int
|
||||
if err := rows.Scan(&code, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[code] = n
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// NamesFor returns ref_code → name for the given codes of one award (batched so
|
||||
// we never load a 250k-row map just to label a few worked references).
|
||||
func (r *Repo) NamesFor(ctx context.Context, awardCode string, codes []string) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
if len(codes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
code := strings.ToUpper(strings.TrimSpace(awardCode))
|
||||
// Chunk to stay under SQLite's parameter limit.
|
||||
const chunk = 400
|
||||
for start := 0; start < len(codes); start += chunk {
|
||||
end := start + chunk
|
||||
if end > len(codes) {
|
||||
end = len(codes)
|
||||
}
|
||||
batch := codes[start:end]
|
||||
ph := strings.TrimSuffix(strings.Repeat("?,", len(batch)), ",")
|
||||
args := make([]any, 0, len(batch)+1)
|
||||
args = append(args, code)
|
||||
for _, c := range batch {
|
||||
args = append(args, strings.ToUpper(strings.TrimSpace(c)))
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT ref_code, name FROM award_references WHERE award_code = ? AND ref_code IN (`+ph+`)`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var rc, name string
|
||||
if err := rows.Scan(&rc, &name); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
out[rc] = name
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Search returns up to `limit` references of an award matching a code/name
|
||||
// query, optionally restricted to a DXCC entity. Drives the per-QSO picker.
|
||||
func (r *Repo) Search(ctx context.Context, awardCode, query string, dxcc, limit int) ([]Ref, error) {
|
||||
code := strings.ToUpper(strings.TrimSpace(awardCode))
|
||||
q := strings.TrimSpace(query)
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
sqlStr := `SELECT ` + allCols + ` FROM award_references WHERE award_code = ?`
|
||||
args := []any{code}
|
||||
if dxcc > 0 {
|
||||
// Match the primary dxcc OR the multi-DXCC list (JSON contains the id).
|
||||
sqlStr += ` AND (dxcc = ? OR dxcc_list LIKE ?)`
|
||||
args = append(args, dxcc, fmt.Sprintf("%%%d%%", dxcc))
|
||||
}
|
||||
if q != "" {
|
||||
sqlStr += ` AND (ref_code LIKE ? OR name LIKE ?)`
|
||||
args = append(args, "%"+strings.ToUpper(q)+"%", "%"+q+"%")
|
||||
}
|
||||
sqlStr += ` ORDER BY ref_code LIMIT ?`
|
||||
args = append(args, limit)
|
||||
rows, err := r.db.QueryContext(ctx, sqlStr, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Ref
|
||||
for rows.Next() {
|
||||
ref, err := scanRef(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, ref)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// List returns every reference of an award, ordered by code. Used by the
|
||||
// reference editor and (via the engine) to show unworked references.
|
||||
func (r *Repo) List(ctx context.Context, awardCode string) ([]Ref, error) {
|
||||
code := strings.ToUpper(strings.TrimSpace(awardCode))
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT `+allCols+` FROM award_references WHERE award_code = ? ORDER BY ref_code`, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Ref
|
||||
for rows.Next() {
|
||||
ref, err := scanRef(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, ref)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Upsert inserts or updates a single reference of an award.
|
||||
func (r *Repo) Upsert(ctx context.Context, awardCode string, ref Ref) error {
|
||||
code := strings.ToUpper(strings.TrimSpace(awardCode))
|
||||
rc := strings.ToUpper(strings.TrimSpace(ref.Code))
|
||||
if code == "" || rc == "" {
|
||||
return fmt.Errorf("empty award or reference code")
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT OR REPLACE INTO award_references
|
||||
(award_code, ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
code, rc, strings.TrimSpace(ref.Name), ref.DXCC, ref.Group, ref.SubGrp,
|
||||
encodeDXCCList(ref.DXCCList), ref.Pattern, b2i(ref.Valid), ref.ValidFrom, ref.ValidTo,
|
||||
ref.Score, ref.Bonus, ref.GridSquare, ref.Alias)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes one reference from an award.
|
||||
func (r *Repo) Delete(ctx context.Context, awardCode, refCode string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`DELETE FROM award_references WHERE award_code = ? AND ref_code = ?`,
|
||||
strings.ToUpper(strings.TrimSpace(awardCode)), strings.ToUpper(strings.TrimSpace(refCode)))
|
||||
return err
|
||||
}
|
||||
|
||||
func b2i(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package awardref
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"hamlog/internal/dxcc"
|
||||
)
|
||||
|
||||
// BuiltinRefs returns the seed reference list for a built-in award (DXCC
|
||||
// entities, CQ zones, continents, US states, French departments). ok=false for
|
||||
// awards whose list is downloaded online (POTA/SOTA/WWFF) or fully custom.
|
||||
//
|
||||
// The reference CODE must equal what award.Compute extracts from the QSO field
|
||||
// so worked references map onto the list:
|
||||
// - DXCC → entity number ("291")
|
||||
// - WAZ → CQ zone number ("1".."40")
|
||||
// - WAC → continent code ("EU", "NA", …)
|
||||
// - WAS → ADIF STATE code ("AL", …)
|
||||
// - DDFM → "D06" (the award pattern captures the leading D)
|
||||
func BuiltinRefs(code string) ([]Ref, bool) {
|
||||
switch code {
|
||||
case "DXCC":
|
||||
return dxccEntities(), true
|
||||
case "WAZ":
|
||||
return cqZones(), true
|
||||
case "WAC":
|
||||
return continents(), true
|
||||
case "WAS":
|
||||
return usStates().Refs, true
|
||||
case "DDFM":
|
||||
return frenchDepartments(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func dxccEntities() []Ref {
|
||||
ents := dxcc.AllEntities()
|
||||
out := make([]Ref, 0, len(ents))
|
||||
for _, e := range ents {
|
||||
out = append(out, ref(strconv.Itoa(e.Num), e.Name, e.Num))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cqZones() []Ref {
|
||||
out := make([]Ref, 0, 40)
|
||||
for z := 1; z <= 40; z++ {
|
||||
out = append(out, ref(strconv.Itoa(z), "CQ Zone "+strconv.Itoa(z), 0))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func continents() []Ref {
|
||||
pairs := [][2]string{
|
||||
{"AF", "Africa"}, {"AN", "Antarctica"}, {"AS", "Asia"},
|
||||
{"EU", "Europe"}, {"NA", "North America"}, {"OC", "Oceania"}, {"SA", "South America"},
|
||||
}
|
||||
out := make([]Ref, 0, len(pairs))
|
||||
for _, p := range pairs {
|
||||
out = append(out, ref(p[0], p[1], 0))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// frenchDepartments — the 96 metropolitan French departments (DXCC 227).
|
||||
func frenchDepartments() []Ref {
|
||||
const fr = 227
|
||||
deps := [][2]string{
|
||||
{"D01", "Ain"}, {"D02", "Aisne"}, {"D03", "Allier"}, {"D04", "Alpes-de-Haute-Provence"},
|
||||
{"D05", "Hautes-Alpes"}, {"D06", "Alpes-Maritimes"}, {"D07", "Ardèche"}, {"D08", "Ardennes"},
|
||||
{"D09", "Ariège"}, {"D10", "Aube"}, {"D11", "Aude"}, {"D12", "Aveyron"},
|
||||
{"D13", "Bouches-du-Rhône"}, {"D14", "Calvados"}, {"D15", "Cantal"}, {"D16", "Charente"},
|
||||
{"D17", "Charente-Maritime"}, {"D18", "Cher"}, {"D19", "Corrèze"}, {"D2A", "Corse-du-Sud"},
|
||||
{"D2B", "Haute-Corse"}, {"D21", "Côte-d'Or"}, {"D22", "Côtes-d'Armor"}, {"D23", "Creuse"},
|
||||
{"D24", "Dordogne"}, {"D25", "Doubs"}, {"D26", "Drôme"}, {"D27", "Eure"},
|
||||
{"D28", "Eure-et-Loir"}, {"D29", "Finistère"}, {"D30", "Gard"}, {"D31", "Haute-Garonne"},
|
||||
{"D32", "Gers"}, {"D33", "Gironde"}, {"D34", "Hérault"}, {"D35", "Ille-et-Vilaine"},
|
||||
{"D36", "Indre"}, {"D37", "Indre-et-Loire"}, {"D38", "Isère"}, {"D39", "Jura"},
|
||||
{"D40", "Landes"}, {"D41", "Loir-et-Cher"}, {"D42", "Loire"}, {"D43", "Haute-Loire"},
|
||||
{"D44", "Loire-Atlantique"}, {"D45", "Loiret"}, {"D46", "Lot"}, {"D47", "Lot-et-Garonne"},
|
||||
{"D48", "Lozère"}, {"D49", "Maine-et-Loire"}, {"D50", "Manche"}, {"D51", "Marne"},
|
||||
{"D52", "Haute-Marne"}, {"D53", "Mayenne"}, {"D54", "Meurthe-et-Moselle"}, {"D55", "Meuse"},
|
||||
{"D56", "Morbihan"}, {"D57", "Moselle"}, {"D58", "Nièvre"}, {"D59", "Nord"},
|
||||
{"D60", "Oise"}, {"D61", "Orne"}, {"D62", "Pas-de-Calais"}, {"D63", "Puy-de-Dôme"},
|
||||
{"D64", "Pyrénées-Atlantiques"}, {"D65", "Hautes-Pyrénées"}, {"D66", "Pyrénées-Orientales"}, {"D67", "Bas-Rhin"},
|
||||
{"D68", "Haut-Rhin"}, {"D69", "Rhône"}, {"D70", "Haute-Saône"}, {"D71", "Saône-et-Loire"},
|
||||
{"D72", "Sarthe"}, {"D73", "Savoie"}, {"D74", "Haute-Savoie"}, {"D75", "Paris"},
|
||||
{"D76", "Seine-Maritime"}, {"D77", "Seine-et-Marne"}, {"D78", "Yvelines"}, {"D79", "Deux-Sèvres"},
|
||||
{"D80", "Somme"}, {"D81", "Tarn"}, {"D82", "Tarn-et-Garonne"}, {"D83", "Var"},
|
||||
{"D84", "Vaucluse"}, {"D85", "Vendée"}, {"D86", "Vienne"}, {"D87", "Haute-Vienne"},
|
||||
{"D88", "Vosges"}, {"D89", "Yonne"}, {"D90", "Territoire de Belfort"}, {"D91", "Essonne"},
|
||||
{"D92", "Hauts-de-Seine"}, {"D93", "Seine-Saint-Denis"}, {"D94", "Val-de-Marne"}, {"D95", "Val-d'Oise"},
|
||||
}
|
||||
out := make([]Ref, 0, len(deps))
|
||||
for _, d := range deps {
|
||||
out = append(out, ref(d[0], d[1], fr))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package awardref
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Importer downloads and parses a program's reference list into []Ref.
|
||||
type Importer struct {
|
||||
AwardCode string
|
||||
URL string
|
||||
Fetch func(ctx context.Context, body io.Reader) ([]Ref, error)
|
||||
}
|
||||
|
||||
// Importers is the registry of built-in reference-list updaters, keyed by
|
||||
// award code. Awards not present here have no online list (manual only).
|
||||
var Importers = map[string]Importer{
|
||||
"POTA": {AwardCode: "POTA", URL: "https://pota.app/all_parks.csv", Fetch: parsePOTA},
|
||||
"SOTA": {AwardCode: "SOTA", URL: "https://www.sotadata.org.uk/summitslist.csv", Fetch: parseSOTA},
|
||||
"WWFF": {AwardCode: "WWFF", URL: "https://wwff.co/wwff-data/wwff_directory.csv", Fetch: parseWWFF},
|
||||
}
|
||||
|
||||
// CanUpdate reports whether an award has an online reference list.
|
||||
func CanUpdate(awardCode string) bool {
|
||||
_, ok := Importers[strings.ToUpper(strings.TrimSpace(awardCode))]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Download fetches and parses the reference list for an award (does not store).
|
||||
func Download(ctx context.Context, awardCode string) ([]Ref, error) {
|
||||
imp, ok := Importers[strings.ToUpper(strings.TrimSpace(awardCode))]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no online list for award %q", awardCode)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", imp.URL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "OpsLog")
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download %s: %w", imp.URL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("download %s: http %d", imp.URL, resp.StatusCode)
|
||||
}
|
||||
return imp.Fetch(ctx, resp.Body)
|
||||
}
|
||||
|
||||
// headerIndex maps lowercased header names to their column index.
|
||||
func headerIndex(header []string) map[string]int {
|
||||
m := make(map[string]int, len(header))
|
||||
for i, h := range header {
|
||||
m[strings.ToLower(strings.TrimSpace(h))] = i
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func get(rec []string, idx int) string {
|
||||
if idx < 0 || idx >= len(rec) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(rec[idx])
|
||||
}
|
||||
|
||||
// parsePOTA: "reference","name","active","entityId","locationDesc"
|
||||
func parsePOTA(_ context.Context, body io.Reader) ([]Ref, error) {
|
||||
r := csv.NewReader(body)
|
||||
r.FieldsPerRecord = -1
|
||||
header, err := r.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := headerIndex(header)
|
||||
iRef, iName, iActive, iEnt, iLoc := h["reference"], h["name"], h["active"], h["entityid"], h["locationdesc"]
|
||||
var out []Ref
|
||||
for {
|
||||
rec, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if iActive >= 0 && get(rec, iActive) == "0" {
|
||||
continue
|
||||
}
|
||||
dxcc, _ := strconv.Atoi(get(rec, iEnt))
|
||||
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), DXCC: dxcc, Group: get(rec, iLoc)})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseSOTA: first line is a title, then header
|
||||
// SummitCode,AssociationName,RegionName,SummitName,…
|
||||
func parseSOTA(_ context.Context, body io.Reader) ([]Ref, error) {
|
||||
r := csv.NewReader(body)
|
||||
r.FieldsPerRecord = -1
|
||||
// First record is the "SOTA Summits List (Date=…)" title line — skip it.
|
||||
if _, err := r.Read(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header, err := r.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := headerIndex(header)
|
||||
iRef, iName, iAssoc, iRegion := h["summitcode"], h["summitname"], h["associationname"], h["regionname"]
|
||||
var out []Ref
|
||||
for {
|
||||
rec, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), Group: get(rec, iAssoc), SubGrp: get(rec, iRegion)})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseWWFF: reference,status,name,…,dxccEnum,… (header-driven)
|
||||
func parseWWFF(_ context.Context, body io.Reader) ([]Ref, error) {
|
||||
r := csv.NewReader(body)
|
||||
r.FieldsPerRecord = -1
|
||||
header, err := r.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := headerIndex(header)
|
||||
iRef, iName, iStatus, iCountry := h["reference"], h["name"], h["status"], h["country"]
|
||||
iDXCC := h["dxccenum"]
|
||||
if iDXCC < 0 {
|
||||
iDXCC = h["dxcc"]
|
||||
}
|
||||
var out []Ref
|
||||
for {
|
||||
rec, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if iStatus >= 0 && !strings.EqualFold(get(rec, iStatus), "active") {
|
||||
continue
|
||||
}
|
||||
dxcc, _ := strconv.Atoi(get(rec, iDXCC))
|
||||
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), DXCC: dxcc, Group: get(rec, iCountry)})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package awardref
|
||||
|
||||
// Preset is a ready-made reference list a user can apply to an award in one
|
||||
// click (Canadian provinces, US states, …). Codes match the values that land
|
||||
// in the corresponding QSO field (e.g. ADIF STATE codes).
|
||||
type Preset struct {
|
||||
Key string `json:"key"` // stable id, e.g. "ca_provinces"
|
||||
Name string `json:"name"` // friendly label
|
||||
Field string `json:"field"` // suggested QSO field to scan
|
||||
DXCC int `json:"dxcc"` // suggested DXCC scope (0 = none)
|
||||
Refs []Ref `json:"refs"`
|
||||
}
|
||||
|
||||
// Presets is the catalogue of built-in reference lists, returned to the UI.
|
||||
func Presets() []Preset {
|
||||
return []Preset{
|
||||
caProvinces(),
|
||||
usStates(),
|
||||
}
|
||||
}
|
||||
|
||||
// PresetByKey returns a preset by its key (ok=false if unknown).
|
||||
func PresetByKey(key string) (Preset, bool) {
|
||||
for _, p := range Presets() {
|
||||
if p.Key == key {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Preset{}, false
|
||||
}
|
||||
|
||||
func ref(code, name string, dxcc int) Ref {
|
||||
return Ref{Code: code, Name: name, DXCC: dxcc, Valid: true}
|
||||
}
|
||||
|
||||
// caProvinces — RAC Canadian Provinces (DXCC 1 = Canada). Codes are ADIF STATE
|
||||
// values for VE provinces/territories.
|
||||
func caProvinces() Preset {
|
||||
const ca = 1
|
||||
return Preset{
|
||||
Key: "ca_provinces", Name: "Canadian Provinces (RAC)", Field: "state", DXCC: ca,
|
||||
Refs: []Ref{
|
||||
ref("AB", "Alberta", ca), ref("BC", "British Columbia", ca),
|
||||
ref("MB", "Manitoba", ca), ref("NB", "New Brunswick", ca),
|
||||
ref("NL", "Newfoundland and Labrador", ca), ref("NS", "Nova Scotia", ca),
|
||||
ref("NT", "Northwest Territories", ca), ref("NU", "Nunavut", ca),
|
||||
ref("ON", "Ontario", ca), ref("PE", "Prince Edward Island", ca),
|
||||
ref("QC", "Quebec", ca), ref("SK", "Saskatchewan", ca),
|
||||
ref("YT", "Yukon", ca),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// usStates — Worked All States (DXCC 291 = United States). 50 ADIF STATE codes.
|
||||
func usStates() Preset {
|
||||
const us = 291
|
||||
codes := [][2]string{
|
||||
{"AL", "Alabama"}, {"AK", "Alaska"}, {"AZ", "Arizona"}, {"AR", "Arkansas"},
|
||||
{"CA", "California"}, {"CO", "Colorado"}, {"CT", "Connecticut"}, {"DE", "Delaware"},
|
||||
{"FL", "Florida"}, {"GA", "Georgia"}, {"HI", "Hawaii"}, {"ID", "Idaho"},
|
||||
{"IL", "Illinois"}, {"IN", "Indiana"}, {"IA", "Iowa"}, {"KS", "Kansas"},
|
||||
{"KY", "Kentucky"}, {"LA", "Louisiana"}, {"ME", "Maine"}, {"MD", "Maryland"},
|
||||
{"MA", "Massachusetts"}, {"MI", "Michigan"}, {"MN", "Minnesota"}, {"MS", "Mississippi"},
|
||||
{"MO", "Missouri"}, {"MT", "Montana"}, {"NE", "Nebraska"}, {"NV", "Nevada"},
|
||||
{"NH", "New Hampshire"}, {"NJ", "New Jersey"}, {"NM", "New Mexico"}, {"NY", "New York"},
|
||||
{"NC", "North Carolina"}, {"ND", "North Dakota"}, {"OH", "Ohio"}, {"OK", "Oklahoma"},
|
||||
{"OR", "Oregon"}, {"PA", "Pennsylvania"}, {"RI", "Rhode Island"}, {"SC", "South Carolina"},
|
||||
{"SD", "South Dakota"}, {"TN", "Tennessee"}, {"TX", "Texas"}, {"UT", "Utah"},
|
||||
{"VT", "Vermont"}, {"VA", "Virginia"}, {"WA", "Washington"}, {"WV", "West Virginia"},
|
||||
{"WI", "Wisconsin"}, {"WY", "Wyoming"},
|
||||
}
|
||||
refs := make([]Ref, 0, len(codes))
|
||||
for _, c := range codes {
|
||||
refs = append(refs, ref(c[0], c[1], us))
|
||||
}
|
||||
return Preset{Key: "us_states", Name: "US States (WAS)", Field: "state", DXCC: us, Refs: refs}
|
||||
}
|
||||
Reference in New Issue
Block a user