From 25486b5c8fdfa5bdc8b7bb491eaf4a19cc81ea46 Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Tue, 23 Jun 2026 22:47:17 -0400 Subject: [PATCH] add speed filters --- pkg/types/readsb/filter.go | 32 ++++++++++++++++++++++++++++++++ pkg/types/readsb/readsb_test.go | 13 +++++++++++++ 2 files changed, 45 insertions(+) diff --git a/pkg/types/readsb/filter.go b/pkg/types/readsb/filter.go index e11e351..0d676da 100644 --- a/pkg/types/readsb/filter.go +++ b/pkg/types/readsb/filter.go @@ -104,6 +104,38 @@ func OnGround() AircraftFilter { return func(a *Aircraft) bool { return a.AltBaro.OnGround } } +// SpeedSource selects which speed reading a speed filter compares against. Its +// zero value is GroundSpeed. +type SpeedSource int + +const ( + // GroundSpeed measures against GS (ground speed, knots). + GroundSpeed SpeedSource = iota + // TrueAirspeed measures against TAS (true airspeed, knots). + TrueAirspeed +) + +func (s SpeedSource) of(a *Aircraft) float64 { + if s == TrueAirspeed { + return float64(a.TAS) + } + return a.GS +} + +// MinSpeed keeps aircraft at or above knots, measured by src. +func MinSpeed(knots float64, src SpeedSource) AircraftFilter { + return func(a *Aircraft) bool { return src.of(a) >= knots } +} + +// MaxSpeed keeps aircraft with a positive reading at or below knots, measured by +// src; aircraft with no reading are dropped. +func MaxSpeed(knots float64, src SpeedSource) AircraftFilter { + return func(a *Aircraft) bool { + v := src.of(a) + return v > 0 && v <= knots + } +} + // WithinNM keeps aircraft within the given range (nautical miles) of the receiver. func WithinNM(nm float64) AircraftFilter { return func(a *Aircraft) bool { return a.RDst > 0 && a.RDst <= nm } diff --git a/pkg/types/readsb/readsb_test.go b/pkg/types/readsb/readsb_test.go index 1ee57bf..7ba0bf3 100644 --- a/pkg/types/readsb/readsb_test.go +++ b/pkg/types/readsb/readsb_test.go @@ -68,6 +68,19 @@ func TestAircraftFilters(t *testing.T) { t.Errorf("WithinNM kept %s at %.1f nm", a.Hex, a.RDst) } } + fast := r.Filter(MinSpeed(250, GroundSpeed)) + for _, a := range fast { + if a.GS < 250 { + t.Errorf("MinSpeed kept %s at %.0f kt", a.Hex, a.GS) + } + } + // MaxSpeed drops aircraft with no reading, so every kept aircraft is positive. + slow := r.Filter(MaxSpeed(250, GroundSpeed)) + for _, a := range slow { + if a.GS <= 0 || a.GS > 250 { + t.Errorf("MaxSpeed kept %s at %.0f kt", a.Hex, a.GS) + } + } // Composition is AND: position AND high altitude is a subset of each. both := r.Filter(WithPosition(), MinAltitude(30000)) if len(both) > len(pos) || len(both) > len(high) {