Skip to content

Commit

Permalink
use the bandmap entry at the current frequency if no supercheck/callh…
Browse files Browse the repository at this point in the history
…istory entry is available for the current input
  • Loading branch information
ftl committed May 30, 2024
1 parent 9425031 commit b59957c
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 16 deletions.
3 changes: 2 additions & 1 deletion core/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 16 additions & 8 deletions core/bandmap/bandmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})

Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions core/bandmap/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
72 changes: 66 additions & 6 deletions core/callinfo/callinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ type AnnotatedCallsign struct {
PredictedExchange []string
Name string
UserText string
OnFrequency bool

Comparable interface{}
Compare func(interface{}, interface{}) bool
Expand Down
47 changes: 47 additions & 0 deletions core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
14 changes: 14 additions & 0 deletions core/entry/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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{}
Expand Down
4 changes: 4 additions & 0 deletions ui/callinfoView.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ func (v *callinfoView) renderCallsign(callsign core.AnnotatedCallsign) string {
renderedCallsign += fmt.Sprintf("<span %s>%s</span>", strings.Join([]string{attributeString, partAttributeString}, " "), partString)
}

if callsign.OnFrequency {
renderedCallsign = fmt.Sprintf("[%s]", renderedCallsign)
}

return renderedCallsign
}

Expand Down
3 changes: 2 additions & 1 deletion ui/entryView.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type EntryController interface {

Enter(string)
SelectMatch(int)
SelectBestMatch()
SendQuestion()
StopTX()

Expand Down Expand Up @@ -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()
}
Expand Down

0 comments on commit b59957c

Please sign in to comment.