From b59957c01bf50e72a85df4109c2c62b5d566f717 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 30 May 2024 16:59:59 +0200 Subject: [PATCH] use the bandmap entry at the current frequency if no supercheck/callhistory entry is available for the current input --- core/app/app.go | 3 +- core/bandmap/bandmap.go | 24 ++++++++----- core/bandmap/entries.go | 12 +++++++ core/callinfo/callinfo.go | 72 +++++++++++++++++++++++++++++++++++---- core/core.go | 1 + core/core_test.go | 47 +++++++++++++++++++++++++ core/entry/entry.go | 14 ++++++++ ui/callinfoView.go | 4 +++ ui/entryView.go | 3 +- 9 files changed, 164 insertions(+), 16 deletions(-) diff --git a/core/app/app.go b/core/app/app.go index 3a2ba5e..065fe0b 100644 --- a/core/app/app.go +++ b/core/app/app.go @@ -189,9 +189,10 @@ func (c *Controller) Startup() { c.QSOList.Notify(logbook.QSOsClearedListenerFunc(c.Rate.Clear)) c.QSOList.Notify(logbook.QSOAddedListenerFunc(c.Rate.Add)) - c.Callinfo = callinfo.New(c.dxccFinder, c.scpFinder, c.callHistoryFinder, c.QSOList, c.Score, c.Entry) + c.Callinfo = callinfo.New(c.dxccFinder, c.scpFinder, c.callHistoryFinder, c.QSOList, c.Score, c.Entry, c.asyncRunner) c.Entry.SetCallinfo(c.Callinfo) c.Bandmap.SetCallinfo(c.Callinfo) + c.Bandmap.Notify(c.Callinfo) c.Score.Notify(c.Callinfo) c.Settings.Notify(c.Entry) diff --git a/core/bandmap/bandmap.go b/core/bandmap/bandmap.go index ade0959..001f467 100644 --- a/core/bandmap/bandmap.go +++ b/core/bandmap/bandmap.go @@ -13,7 +13,7 @@ import ( const ( // DefaultUpdatePeriod: the bandmap is updated with this period - DefaultUpdatePeriod time.Duration = 5 * time.Second + DefaultUpdatePeriod time.Duration = 2 * time.Second // DefaultMaximumAge of entries in the bandmap // entries that were not seen within this period are removed from the bandmap DefaultMaximumAge time.Duration = 10 * time.Minute @@ -107,7 +107,12 @@ func (m *Bandmap) run() { func (m *Bandmap) update() { m.entries.CleanOut(m.maximumAge, m.clock.Now(), m.weights) - nearestEntry, nearestEntryFound := m.nextVisibleEntry(m.activeFrequency, func(entry core.BandmapEntry) bool { + entryOnFrequency, entryOnFrequencyAvailable := m.nextVisibleEntry(m.activeFrequency, 2, func(entry core.BandmapEntry) bool { + return entry.OnFrequency(m.activeFrequency) + }) + m.entries.emitEntryOnFrequency(entryOnFrequency, entryOnFrequencyAvailable) + + nearestEntry, nearestEntryFound := m.nextVisibleEntry(m.activeFrequency, 0, func(entry core.BandmapEntry) bool { return entry.Frequency != m.activeFrequency && entry.Source != core.WorkedSpot }) @@ -348,7 +353,7 @@ func (m *Bandmap) GotoNextEntryDown() { func (m *Bandmap) findAndSelectNextVisibleEntry(f func(entry core.BandmapEntry) bool) { m.do <- func() { - entry, found := m.nextVisibleEntry(m.activeFrequency, f) + entry, found := m.nextVisibleEntry(m.activeFrequency, 0, f) if found { m.entries.Select(entry.Index) } @@ -357,20 +362,23 @@ func (m *Bandmap) findAndSelectNextVisibleEntry(f func(entry core.BandmapEntry) func (m *Bandmap) findAndSelectNextVisibleEntryBy(order core.BandmapOrder, f func(entry core.BandmapEntry) bool) { m.do <- func() { - entry, found := m.nextVisibleEntryBy(order, f) + entry, found := m.nextVisibleEntryBy(order, 0, f) if found { m.entries.Select(entry.Index) } } } -func (m *Bandmap) nextVisibleEntry(frequency core.Frequency, f func(core.BandmapEntry) bool) (core.BandmapEntry, bool) { - return m.nextVisibleEntryBy(core.BandmapByDistance(frequency), f) +func (m *Bandmap) nextVisibleEntry(frequency core.Frequency, limit int, f func(core.BandmapEntry) bool) (core.BandmapEntry, bool) { + return m.nextVisibleEntryBy(core.BandmapByDistance(frequency), limit, f) } -func (m *Bandmap) nextVisibleEntryBy(order core.BandmapOrder, f func(core.BandmapEntry) bool) (core.BandmapEntry, bool) { +func (m *Bandmap) nextVisibleEntryBy(order core.BandmapOrder, limit int, f func(core.BandmapEntry) bool) (core.BandmapEntry, bool) { entries := m.entries.AllBy(order) - for i := 0; i < len(entries); i++ { + if limit == 0 || limit > len(entries) { + limit = len(entries) + } + for i := 0; i < limit; i++ { entry := entries[i] if !m.entryVisible(entry) { continue diff --git a/core/bandmap/entries.go b/core/bandmap/entries.go index aefc537..0087991 100644 --- a/core/bandmap/entries.go +++ b/core/bandmap/entries.go @@ -176,6 +176,10 @@ type EntrySelectedListener interface { EntrySelected(core.BandmapEntry) } +type EntryOnFrequencyListener interface { + EntryOnFrequency(core.BandmapEntry, bool) +} + type Entries struct { entries []*Entry bands []core.Band @@ -247,6 +251,14 @@ func (l *Entries) emitEntrySelected(e Entry) { } } +func (l *Entries) emitEntryOnFrequency(e core.BandmapEntry, available bool) { + for _, listener := range l.listeners { + if nearestEntryListener, ok := listener.(EntryOnFrequencyListener); ok { + nearestEntryListener.EntryOnFrequency(e, available) + } + } +} + func (l *Entries) Clear() { l.entries = make([]*Entry, 0, 100) } diff --git a/core/callinfo/callinfo.go b/core/callinfo/callinfo.go index 1c04b6b..b71165a 100644 --- a/core/callinfo/callinfo.go +++ b/core/callinfo/callinfo.go @@ -15,9 +15,10 @@ import ( "github.com/ftl/hellocontest/core" ) -func New(entities DXCCFinder, callsigns CallsignFinder, callHistory CallHistoryFinder, dupeChecker DupeChecker, valuer Valuer, exchangeFilter ExchangeFilter) *Callinfo { +func New(entities DXCCFinder, callsigns CallsignFinder, callHistory CallHistoryFinder, dupeChecker DupeChecker, valuer Valuer, exchangeFilter ExchangeFilter, asyncRunner core.AsyncRunner) *Callinfo { result := &Callinfo{ view: new(nullView), + asyncRunner: asyncRunner, entities: entities, callsigns: callsigns, callHistory: callHistory, @@ -31,7 +32,8 @@ func New(entities DXCCFinder, callsigns CallsignFinder, callHistory CallHistoryF } type Callinfo struct { - view View + view View + asyncRunner core.AsyncRunner entities DXCCFinder callsigns CallsignFinder @@ -48,7 +50,11 @@ type Callinfo struct { theirExchangeFields []core.ExchangeField totalScore core.BandScore - bestMatches []string + matchOnFrequency core.AnnotatedCallsign + matchOnFrequencyAvailable bool + bestMatch core.AnnotatedCallsign + bestMatchAvailable bool + bestMatches []string } // DXCCFinder returns a list of matching prefixes for the given string and indicates if there was a match at all. @@ -120,10 +126,47 @@ func (c *Callinfo) ScoreUpdated(score core.Score) { c.totalScore = score.Result() } +func (c *Callinfo) EntryOnFrequency(entry core.BandmapEntry, available bool) { + c.asyncRunner(func() { + log.Printf("EntryOnFrequency: %v %t", entry, available) + c.matchOnFrequencyAvailable = available + + if available && c.matchOnFrequency.Callsign.String() == entry.Call.String() { + // go on + } else if available { + normalizedCall := strings.TrimSpace(strings.ToUpper(c.lastCallsign)) + exactMatch := normalizedCall == entry.Call.String() + c.matchOnFrequency = core.AnnotatedCallsign{ + Callsign: entry.Call, + Assembly: core.MatchingAssembly{{OP: core.Matching, Value: entry.Call.String()}}, + Duplicate: entry.Info.Duplicate, + Worked: entry.Info.Worked, + ExactMatch: exactMatch, + Points: entry.Info.Points, + Multis: entry.Info.Multis, + PredictedExchange: entry.Info.PredictedExchange, + OnFrequency: true, + } + } else { + c.matchOnFrequency = core.AnnotatedCallsign{} + } + + c.showBestMatch() + }) +} + func (c *Callinfo) BestMatches() []string { return c.bestMatches } +func (c *Callinfo) BestMatch() string { + bestMatch, available := c.findBestMatch() + if !available { + return "" + } + return bestMatch.Callsign.String() +} + func (c *Callinfo) PredictedExchange() []string { return c.predictedExchange } @@ -181,16 +224,18 @@ func (c *Callinfo) ShowInfo(call string, band core.Band, mode core.Mode, exchang supercheck := c.calculateSupercheck(call) c.bestMatches = make([]string, 0, len(supercheck)) - var bestMatch core.AnnotatedCallsign + c.bestMatch = core.AnnotatedCallsign{} + c.bestMatchAvailable = false for i, match := range supercheck { c.bestMatches = append(c.bestMatches, match.Callsign.String()) if i == 0 { - bestMatch = match + c.bestMatch = match + c.bestMatchAvailable = true } } c.showDXCCEntity(entity) - c.view.SetBestMatchingCallsign(bestMatch) + c.showBestMatch() c.view.SetUserInfo(callinfo.UserText) c.view.SetValue(callinfo.Points, callinfo.Multis, callinfo.Value) for i := range c.theirExchangeFields { @@ -238,6 +283,21 @@ func (c *Callinfo) showDXCCEntity(entity dxcc.Prefix) { c.view.SetDXCC(dxccName, entity.Continent, int(entity.ITUZone), int(entity.CQZone), !entity.NotARRLCompliant) } +func (c *Callinfo) findBestMatch() (core.AnnotatedCallsign, bool) { + match := c.matchOnFrequency + + if c.bestMatchAvailable { + match = c.bestMatch + } + + return match, (match.Callsign.String() != "") +} + +func (c *Callinfo) showBestMatch() { + bestMatch, _ := c.findBestMatch() + c.view.SetBestMatchingCallsign(bestMatch) +} + func (c *Callinfo) calculateSupercheck(s string) []core.AnnotatedCallsign { normalizedInput := strings.TrimSpace(strings.ToUpper(s)) scpMatches, err := c.callsigns.Find(s) diff --git a/core/core.go b/core/core.go index 944796c..e343163 100644 --- a/core/core.go +++ b/core/core.go @@ -254,6 +254,7 @@ type AnnotatedCallsign struct { PredictedExchange []string Name string UserText string + OnFrequency bool Comparable interface{} Compare func(interface{}, interface{}) bool diff --git a/core/core_test.go b/core/core_test.go index bf83710..c69c75c 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -146,3 +146,50 @@ func TestBandmapEntry_ProximityFactor(t *testing.T) { }) } } + +func TestBandmapEntry_OnFrequency(t *testing.T) { + const frequency Frequency = 7035000 + tt := []struct { + desc string + frequency Frequency + expected bool + }{ + { + desc: "same frequency", + frequency: frequency, + expected: true, + }, + { + desc: "lower frequency in proximity", + frequency: frequency - Frequency(spotFrequencyDeltaThreshold-0.1), + expected: true, + }, + { + desc: "higher frequency in proximity", + frequency: frequency + Frequency(spotFrequencyDeltaThreshold-0.1), + expected: true, + }, + { + desc: "frequency to low", + frequency: frequency - Frequency(spotFrequencyDeltaThreshold+0.1), + expected: false, + }, + { + desc: "frequency to high", + frequency: frequency + Frequency(spotFrequencyDeltaThreshold+0.1), + expected: false, + }, + } + for _, tc := range tt { + t.Run(tc.desc, func(t *testing.T) { + entry := BandmapEntry{ + Call: callsign.MustParse("dl1abc"), + Frequency: frequency, + } + + actual := entry.OnFrequency(tc.frequency) + + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/core/entry/entry.go b/core/entry/entry.go index 921e9f5..2f98137 100644 --- a/core/entry/entry.go +++ b/core/entry/entry.go @@ -74,6 +74,7 @@ type Keyer interface { type Callinfo interface { ShowInfo(call string, band core.Band, mode core.Mode, exchange []string) BestMatches() []string + BestMatch() string PredictedExchange() []string } @@ -366,6 +367,18 @@ func (c *Controller) SelectMatch(index int) { c.GotoNextField() } +func (c *Controller) SelectBestMatch() { + match := c.callinfo.BestMatch() + if match == "" { + return + } + + c.activeField = core.CallsignField + c.Enter(match) + c.view.SetCallsign(c.input.callsign) + c.GotoNextField() +} + func (c *Controller) Enter(text string) { switch c.activeField { case core.CallsignField: @@ -973,6 +986,7 @@ type nullCallinfo struct{} func (n *nullCallinfo) ShowInfo(string, core.Band, core.Mode, []string) {} func (n *nullCallinfo) BestMatches() []string { return []string{} } +func (n *nullCallinfo) BestMatch() string { return "" } func (n *nullCallinfo) PredictedExchange() []string { return []string{} } type nullBandmap struct{} diff --git a/ui/callinfoView.go b/ui/callinfoView.go index a8e9f5d..c4ede30 100644 --- a/ui/callinfoView.go +++ b/ui/callinfoView.go @@ -211,6 +211,10 @@ func (v *callinfoView) renderCallsign(callsign core.AnnotatedCallsign) string { renderedCallsign += fmt.Sprintf("%s", strings.Join([]string{attributeString, partAttributeString}, " "), partString) } + if callsign.OnFrequency { + renderedCallsign = fmt.Sprintf("[%s]", renderedCallsign) + } + return renderedCallsign } diff --git a/ui/entryView.go b/ui/entryView.go index b90b735..862d67e 100644 --- a/ui/entryView.go +++ b/ui/entryView.go @@ -20,6 +20,7 @@ type EntryController interface { Enter(string) SelectMatch(int) + SelectBestMatch() SendQuestion() StopTX() @@ -132,7 +133,7 @@ func (v *entryView) onEntryKeyPress(_ interface{}, event *gdk.Event) bool { return true case gdk.KEY_Return: if alt { - v.controller.SelectMatch(0) + v.controller.SelectBestMatch() } else { v.controller.Log() }