236 lines
7.5 KiB
Go
236 lines
7.5 KiB
Go
package main
|
|
|
|
// "Have I worked this reference before?" — the NEW badge on a QSO's award
|
|
// references.
|
|
//
|
|
// # Where the answer comes from, and where it must NOT come from
|
|
//
|
|
// The obvious source is the materialised award_refs column: it is already on
|
|
// every row and one query reads it. It is also the wrong answer twice over.
|
|
// That column holds DISPLAY LABELS, not references — depending on the award's
|
|
// RefDisplay it can read "Krasnogvardeysky" or "ST-25 — Krasnogvardeysky", and
|
|
// DXCC stores a country name — and it is a derived cache that lags: a QSO whose
|
|
// references were back-filled (see BackfillRDA) counts on the Awards grid long
|
|
// before anything recomputes its column. The first build of this badge used it
|
|
// and marked a district worked on five bands as NEW.
|
|
//
|
|
// So the answer comes from the same place the Awards grid's answer comes from:
|
|
// award.MatchQSO over the logbook. What is engineered here is the COST of that,
|
|
// because the question is asked while the operator is logging:
|
|
//
|
|
// - the set is per award CODE, and built only for codes actually asked about;
|
|
// - it reuses the award snapshot when one is already in memory, which it
|
|
// usually is (the Awards tab, the cluster status index);
|
|
// - otherwise it STREAMS the logbook rather than materialising a second copy
|
|
// of it — the references of one award are a few thousand short strings;
|
|
// - and it is built in the BACKGROUND. A caller that asks before the set is
|
|
// ready is told so and gets no badge, never a wait and never a wrong badge.
|
|
//
|
|
// A freshly logged QSO's references are folded into the sets, so a normal
|
|
// evening of logging never rebuilds anything.
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"hamlog/internal/award"
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
// workedRefIndex is the per-code set of references already in the log, plus the
|
|
// logbook revision it describes.
|
|
type workedRefIndex struct {
|
|
rev string
|
|
byCode map[string]map[string]struct{}
|
|
building map[string]bool
|
|
}
|
|
|
|
// AwardRefNewResult answers one badge request.
|
|
type AwardRefNewResult struct {
|
|
// New maps "CODE@REF" to "never worked". An entry whose award set is still
|
|
// building is ABSENT rather than false: "not worked" and "not known yet"
|
|
// must not look the same to the badge.
|
|
New map[string]bool `json:"new"`
|
|
// Pending is true when at least one award set is still being built, and the
|
|
// caller should ask again shortly.
|
|
Pending bool `json:"pending"`
|
|
}
|
|
|
|
// AwardRefsNew reports which of the given "CODE@REF" entries have never been
|
|
// worked. Returns immediately, always.
|
|
func (a *App) AwardRefsNew(entries []string) AwardRefNewResult {
|
|
res := AwardRefNewResult{New: map[string]bool{}}
|
|
if a.qso == nil || len(entries) == 0 {
|
|
return res
|
|
}
|
|
rev, err := a.qso.Revision(a.ctx)
|
|
if err != nil {
|
|
return res // logbook unreachable: no badge is the honest answer
|
|
}
|
|
|
|
// Group the request by award code, so one code is looked up once however
|
|
// many of its references are on the QSO.
|
|
want := map[string][]string{}
|
|
for _, e := range entries {
|
|
code, _, ok := splitRefEntry(e)
|
|
if !ok {
|
|
continue
|
|
}
|
|
want[code] = append(want[code], e)
|
|
}
|
|
|
|
a.workedRefMu.Lock()
|
|
if a.workedRef.rev != rev || a.workedRef.byCode == nil {
|
|
// The logbook moved on: every set describes a log that no longer exists.
|
|
a.workedRef = workedRefIndex{rev: rev, byCode: map[string]map[string]struct{}{}, building: map[string]bool{}}
|
|
}
|
|
var build []string
|
|
for code := range want {
|
|
set, ready := a.workedRef.byCode[code]
|
|
if !ready {
|
|
if !a.workedRef.building[code] {
|
|
a.workedRef.building[code] = true
|
|
build = append(build, code)
|
|
}
|
|
res.Pending = true
|
|
continue
|
|
}
|
|
for _, e := range want[code] {
|
|
_, ref, _ := splitRefEntry(e)
|
|
_, worked := set[ref]
|
|
res.New[e] = !worked
|
|
}
|
|
}
|
|
a.workedRefMu.Unlock()
|
|
|
|
for _, code := range build {
|
|
go a.buildWorkedRefs(code, rev)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// splitRefEntry splits "RDA@ST-25" into its uppercased code and reference.
|
|
func splitRefEntry(e string) (code, ref string, ok bool) {
|
|
at := strings.Index(e, "@")
|
|
if at <= 0 {
|
|
return "", "", false
|
|
}
|
|
code = strings.ToUpper(strings.TrimSpace(e[:at]))
|
|
ref = strings.ToUpper(strings.TrimSpace(e[at+1:]))
|
|
return code, ref, code != "" && ref != ""
|
|
}
|
|
|
|
// buildWorkedRefs collects every reference of one award already in the log.
|
|
func (a *App) buildWorkedRefs(code string, rev string) {
|
|
set := map[string]struct{}{}
|
|
defer func() {
|
|
a.workedRefMu.Lock()
|
|
// Publish only against the revision we were asked for: a QSO logged while
|
|
// this ran has already reset the index, and a set built from the older log
|
|
// would claim to describe the newer one.
|
|
if a.workedRef.rev == rev && a.workedRef.byCode != nil {
|
|
a.workedRef.byCode[code] = set
|
|
}
|
|
delete(a.workedRef.building, code)
|
|
a.workedRefMu.Unlock()
|
|
}()
|
|
|
|
var def *award.Def
|
|
for _, d := range a.awardDefs() {
|
|
if strings.EqualFold(d.Code, code) {
|
|
dd := d
|
|
def = &dd
|
|
break
|
|
}
|
|
}
|
|
if def == nil {
|
|
return // unknown award: an empty set, so nothing is ever badged NEW
|
|
}
|
|
metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
|
|
collect := func(q *qso.QSO) {
|
|
for _, r := range award.MatchQSO(*def, metas, q) {
|
|
set[strings.ToUpper(strings.TrimSpace(r))] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// The snapshot is already enriched and already in memory whenever the Awards
|
|
// tab has been open — by far the common case, and free.
|
|
if snap := a.awardSnapshotIfLoaded(); snap != nil {
|
|
for i := range snap {
|
|
collect(&snap[i])
|
|
}
|
|
return
|
|
}
|
|
// Otherwise stream. One pass, nothing retained but the reference strings.
|
|
_ = a.qso.IterateForAwards(a.ctx, func(q qso.QSO) error {
|
|
a.enrichQSOForAwards(&q)
|
|
collect(&q)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// awardSnapshotIfLoaded returns the award snapshot ONLY if one is already built
|
|
// and current. Never builds one: the caller is a background convenience, and
|
|
// reading the whole logbook for it is the cost this file exists to avoid.
|
|
func (a *App) awardSnapshotIfLoaded() []qso.QSO {
|
|
rev, err := a.qso.Revision(a.ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
a.awardSnapMu.Lock()
|
|
defer a.awardSnapMu.Unlock()
|
|
if a.awardSnap != nil && a.awardSnapRev == rev {
|
|
return a.awardSnap
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// noteWorkedQSO folds a just-logged contact's references into the built sets, so
|
|
// logging does not throw away work already done.
|
|
func (a *App) noteWorkedQSO(q qso.QSO) {
|
|
a.workedRefMu.Lock()
|
|
codes := make(map[string]bool, len(a.workedRef.byCode))
|
|
for c := range a.workedRef.byCode {
|
|
codes[c] = true
|
|
}
|
|
a.workedRefMu.Unlock()
|
|
if len(codes) == 0 {
|
|
return
|
|
}
|
|
rev, err := a.qso.Revision(a.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
a.enrichQSOForAwards(&q)
|
|
found := map[string][]string{}
|
|
for _, d := range a.awardDefs() {
|
|
code := strings.ToUpper(d.Code)
|
|
if !codes[code] {
|
|
continue
|
|
}
|
|
metas := a.awardRefMetas([]award.Def{d})[code]
|
|
for _, r := range award.MatchQSO(d, metas, &q) {
|
|
found[code] = append(found[code], strings.ToUpper(strings.TrimSpace(r)))
|
|
}
|
|
}
|
|
a.workedRefMu.Lock()
|
|
defer a.workedRefMu.Unlock()
|
|
// Carry the sets forward to the new revision rather than dropping them: they
|
|
// were correct one contact ago, and that contact is what is being added.
|
|
a.workedRef.rev = rev
|
|
for code, refs := range found {
|
|
if set := a.workedRef.byCode[code]; set != nil {
|
|
for _, r := range refs {
|
|
set[r] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// invalidateWorkedRefs drops every set. For the changes a revision cannot see —
|
|
// award definitions edited, reference lists updated, QSLs confirmed.
|
|
func (a *App) invalidateWorkedRefs() {
|
|
a.workedRefMu.Lock()
|
|
a.workedRef = workedRefIndex{}
|
|
a.workedRefMu.Unlock()
|
|
}
|