diff --git a/core/bandmap/entries.go b/core/bandmap/entries.go index faa0692..0382ba5 100644 --- a/core/bandmap/entries.go +++ b/core/bandmap/entries.go @@ -11,9 +11,11 @@ import ( const ( // spots within this distance to an entry's frequency will be added to the entry - spotFrequencyDeltaThreshold float64 = 500 + spotFrequencyDeltaThreshold float64 = 300 // the frequency of an entry is aligend to this is grid of accuracy spotFrequencyStep float64 = 10 + // at least this number of spots of the same callsign on the same frequency are required for a valid spot + spotValidThreshold = 3 ) type Entry struct { @@ -43,24 +45,44 @@ func (e *Entry) Len() int { return len(e.spots) } -func (e *Entry) Matches(spot core.Spot) bool { - if spot.Call != e.Call { - return false - } +func (e *Entry) Matches(spot core.Spot) (core.SpotQuality, bool) { if spot.Band != e.Band { - return false + return core.UnknownSpotQuality, false } if spot.Mode != core.NoMode && e.Mode != core.NoMode && spot.Mode != e.Mode { - return false + return core.UnknownSpotQuality, false + } + + var callsignDistance int + if spot.Call == e.Call { + callsignDistance = 0 + } else { + callsignDistance = calculateCallsignDistance(spot.Call, e.Call) } frequencyDelta := math.Abs(float64(e.Frequency - spot.Frequency)) - return frequencyDelta <= spotFrequencyDeltaThreshold + onFrequency := frequencyDelta <= spotFrequencyDeltaThreshold + + quality := core.UnknownSpotQuality + if len(e.spots)+1 >= spotValidThreshold { + quality = core.ValidSpotQuality + } + + if callsignDistance == 0 && onFrequency { + return quality, true + } else if e.Quality == core.ValidSpotQuality && callsignDistance == 0 && !onFrequency { + return core.QSYSpotQuality, false + } else if e.Quality == core.ValidSpotQuality && callsignDistance <= similarCallsignThreshold && onFrequency { + return core.BustedSpotQuality, false + } else { + return core.UnknownSpotQuality, false + } } -func (e *Entry) Add(spot core.Spot) bool { - if !e.Matches(spot) { - return false +func (e *Entry) Add(spot core.Spot) (core.SpotQuality, bool) { + quality, match := e.Matches(spot) + if !match { + return quality, false } e.spots = append(e.spots, spot) @@ -71,8 +93,11 @@ func (e *Entry) Add(spot core.Spot) bool { if e.Source.Priority() > spot.Source.Priority() { e.Source = spot.Source } + if quality == core.ValidSpotQuality { + e.Quality = quality + } - return true + return quality, true } func (e *Entry) RemoveSpotsBefore(timestamp time.Time) bool { @@ -106,6 +131,9 @@ func (e *Entry) update() { e.LastHeard = lastHeard e.Source = source e.SpotCount = len(e.spots) + if e.SpotCount < spotValidThreshold && e.Quality == core.ValidSpotQuality { + e.Quality = core.UnknownSpotQuality + } } func (e *Entry) updateFrequency() bool { @@ -222,16 +250,22 @@ func (l *Entries) Len() int { } func (l *Entries) Add(spot core.Spot, now time.Time, weights core.BandmapWeights) { + entryQuality := core.UnknownSpotQuality for _, e := range l.entries { - if e.Add(spot) { + quality, added := e.Add(spot) + if added { e.Info = l.callinfo.GetInfo(spot.Call, spot.Band, spot.Mode, []string{}) e.Info.WeightedValue = l.calculateWeightedValue(e, now, weights) l.emitEntryUpdated(*e) return } + if entryQuality < quality { + entryQuality = quality + } } newEntry := NewEntry(spot) + newEntry.Quality = entryQuality if newEntry.Call.String() != "" { newEntry.Info = l.callinfo.GetInfo(newEntry.Call, newEntry.Band, newEntry.Mode, []string{}) newEntry.Info.WeightedValue = l.calculateWeightedValue(&newEntry, now, weights) diff --git a/core/bandmap/entries_test.go b/core/bandmap/entries_test.go index 2827c61..b4372dd 100644 --- a/core/bandmap/entries_test.go +++ b/core/bandmap/entries_test.go @@ -65,7 +65,7 @@ func TestEntry_Add_OnlySameCallAndSimilarFrequency(t *testing.T) { }, } - added := entry.Add(core.Spot{Call: callsign.MustParse(tc.call), Frequency: tc.frequency}) + _, added := entry.Add(core.Spot{Call: callsign.MustParse(tc.call), Frequency: tc.frequency}) assert.Equal(t, tc.valid, added) }) } @@ -382,6 +382,57 @@ func TestEntries_Notify(t *testing.T) { assert.Equal(t, "DL1ABC", listener.removed[0].Call.String()) } +func TestEntry_Matches(t *testing.T) { + now := time.Now() + spot := core.Spot{Call: callsign.MustParse("dl1abc"), Frequency: 3535000, Time: now.Add(-5 * time.Minute)} + entry := NewEntry(spot) + assert.Equal(t, core.UnknownSpotQuality, entry.Quality) + + similarSpot := core.Spot{Call: callsign.MustParse("dl2abc"), Frequency: 3535000, Time: now.Add(-2 * time.Minute)} + quality, match := entry.Matches(similarSpot) + assert.False(t, match) + assert.Equal(t, core.UnknownSpotQuality, quality) + _, added := entry.Add(similarSpot) + assert.False(t, added) + + quality, match = entry.Matches(spot) + assert.True(t, match) + assert.Equal(t, core.UnknownSpotQuality, quality) + + _, added = entry.Add(spot) + assert.True(t, added) + assert.Equal(t, core.UnknownSpotQuality, entry.Quality) + + quality, match = entry.Matches(spot) + assert.True(t, match) + assert.Equal(t, core.ValidSpotQuality, quality) + + _, added = entry.Add(spot) + assert.True(t, added) + assert.Equal(t, core.ValidSpotQuality, entry.Quality) + + qsySpot := core.Spot{Call: callsign.MustParse("dl1abc"), Frequency: 3545000, Time: now.Add(-2 * time.Minute)} + quality, match = entry.Matches(qsySpot) + assert.False(t, match) + assert.Equal(t, core.QSYSpotQuality, quality) + _, added = entry.Add(qsySpot) + assert.False(t, added) + + bustedSpot := core.Spot{Call: callsign.MustParse("dl2abc"), Frequency: 3535000, Time: now.Add(-2 * time.Minute)} + quality, match = entry.Matches(bustedSpot) + assert.False(t, match) + assert.Equal(t, core.BustedSpotQuality, quality) + _, added = entry.Add(bustedSpot) + assert.False(t, added) + + completeDifferentSpot := core.Spot{Call: callsign.MustParse("dl3xyz"), Frequency: 3535000, Time: now.Add(-2 * time.Minute)} + quality, match = entry.Matches(completeDifferentSpot) + assert.False(t, match) + assert.Equal(t, core.UnknownSpotQuality, quality) + _, added = entry.Add(completeDifferentSpot) + assert.False(t, added) +} + func TestFilterSlice(t *testing.T) { input := []int{1, 10, 5, 2, 9, 7, 6, 3, 4} diff --git a/core/bandmap/false_entry_check.go b/core/bandmap/false_entry_check.go index ebf41d5..5893977 100644 --- a/core/bandmap/false_entry_check.go +++ b/core/bandmap/false_entry_check.go @@ -46,15 +46,17 @@ func CheckFalseEntry(entry1, entry2 core.BandmapEntry) FalseEntryCheckResult { } } +func calculateCallsignDistance(call1, call2 callsign.Callsign) int { + options := levenshtein.DefaultOptions + return levenshtein.DistanceForStrings([]rune(call1.String()), []rune(call2.String()), options) +} + func checkLocallyVerified(entry core.BandmapEntry) bool { return entry.Source == core.WorkedSpot || entry.Source == core.ManualSpot } func checkCallsignSimilar(call1, call2 callsign.Callsign) bool { - options := levenshtein.DefaultOptions - distance := levenshtein.DistanceForStrings([]rune(call1.String()), []rune(call2.String()), options) - - return distance <= similarCallsignThreshold + return calculateCallsignDistance(call1, call2) <= similarCallsignThreshold } func checkFirstSpotCountIsFalse(count1, count2 int) bool { diff --git a/core/core.go b/core/core.go index f9164a0..2ae3499 100644 --- a/core/core.go +++ b/core/core.go @@ -772,6 +772,25 @@ func (t SpotType) Priority() int { return priority } +type SpotQuality int + +const ( + UnknownSpotQuality SpotQuality = iota + BustedSpotQuality + QSYSpotQuality + ValidSpotQuality +) + +const SpotQualityTags = "?BQV" + +func (q SpotQuality) Tag() string { + i := int(q) + if i > 0 && i < len(SpotQualityTags) { + return string(SpotQualityTags[q]) + } + return string(SpotQualityTags[0]) +} + type SpotFilter string const ( @@ -853,6 +872,7 @@ type BandmapEntry struct { LastHeard time.Time Source SpotType SpotCount int + Quality SpotQuality Info Callinfo } diff --git a/ui/spotsTableView.go b/ui/spotsTableView.go index 016ef2e..3385072 100644 --- a/ui/spotsTableView.go +++ b/ui/spotsTableView.go @@ -18,6 +18,7 @@ import ( const ( spotColumnFrequency = iota spotColumnCallsign + spotColumnQualityTag spotColumnPredictedExchange spotColumnPoints spotColumnMultis @@ -38,6 +39,7 @@ func setupSpotsTableView(v *spotsView, builder *gtk.Builder, controller SpotsCon v.table.AppendColumn(createSpotMarkupColumn("Frequency", spotColumnFrequency)) v.table.AppendColumn(createSpotMarkupColumn("Callsign", spotColumnCallsign)) + v.table.AppendColumn(createSpotTextColumn("T", spotColumnQualityTag)) v.table.AppendColumn(createSpotTextColumn("Exchange", spotColumnPredictedExchange)) v.table.AppendColumn(createSpotMarkupColumn("Pts", spotColumnPoints)) v.table.AppendColumn(createSpotMarkupColumn("Mult", spotColumnMultis)) @@ -135,6 +137,7 @@ func (v *spotsView) fillEntryToTableRow(row *gtk.TreeIter, entry core.BandmapEnt []int{ spotColumnFrequency, spotColumnCallsign, + spotColumnQualityTag, spotColumnPredictedExchange, spotColumnPoints, spotColumnMultis, @@ -148,6 +151,7 @@ func (v *spotsView) fillEntryToTableRow(row *gtk.TreeIter, entry core.BandmapEnt []any{ formatSpotFrequency(entry.Frequency, entry.ProximityFactor(v.currentFrame.Frequency), entry.OnFrequency(v.currentFrame.Frequency)), formatSpotCall(entry.Call, entry.ProximityFactor(v.currentFrame.Frequency), entry.OnFrequency(v.currentFrame.Frequency)), + entry.Quality.Tag(), entry.Info.ExchangeText, formatPoints(entry.Info.Points, entry.Info.Duplicate, 1), formatPoints(entry.Info.Multis, entry.Info.Duplicate, 0), @@ -229,12 +233,14 @@ func (v *spotsView) updateHighlightedColumns(entry core.BandmapEntry) error { []int{ spotColumnFrequency, spotColumnCallsign, + spotColumnQualityTag, spotColumnAge, spotColumnWeightedValue, }, []any{ formatSpotFrequency(entry.Frequency, entry.ProximityFactor(v.currentFrame.Frequency), entry.OnFrequency(v.currentFrame.Frequency)), formatSpotCall(entry.Call, entry.ProximityFactor(v.currentFrame.Frequency), entry.OnFrequency(v.currentFrame.Frequency)), + entry.Quality.Tag(), formatSpotAge(entry.LastHeard), fmt.Sprintf("%.1f", entry.Info.WeightedValue), },