3 Commits

Author SHA1 Message Date
rmcguire 26d7a2c0b7 update README for filters
Publish / release (push) Successful in 24s
2026-06-26 22:37:47 -04:00
rmcguire 84d1ab1401 add position filter
Publish / release (push) Successful in 29s
2026-06-26 22:08:28 -04:00
rmcguire 6dd9def324 add gitignore 2026-06-23 23:04:57 -04:00
4 changed files with 70 additions and 4 deletions
+1
View File
@@ -0,0 +1 @@
wingbits
+21 -4
View File
@@ -145,10 +145,27 @@ heavies, _ := c.Aircraft(ctx,
) )
``` ```
Available filters include `WithPosition`, `WithHex`, `WithCallsign`, | Filter Name | Description | Sample Usage |
`WithSquawk`, `WithCategory`, `WithType`, `InEmergency`, `IsMLAT`, |---|---|---|
`MinAltitude`, `MaxAltitude`, `OnGround`, `WithinNM`, `SeenWithin`, `MinRSSI`, | `WithPosition` | Keeps only aircraft that currently report a lat/lon. | `readsb.WithPosition()` |
plus the `Not` and `Any` combinators. | `WithHex` | Keeps aircraft whose ICAO address matches any of the given values. | `readsb.WithHex("A1B2C3", "D4E5F6")` |
| `WithCallsign` | Keeps aircraft whose (trimmed) callsign matches any value. | `readsb.WithCallsign("UAL2350")` |
| `WithSquawk` | Keeps aircraft transmitting any of the given Mode A squawk codes. | `readsb.WithSquawk("7700")` |
| `WithCategory` | Keeps aircraft of any of the given emitter categories. | `readsb.WithCategory(readsb.CatLarge)` |
| `WithType` | Keeps aircraft of any of the given source types. | `readsb.WithType(readsb.TypeMLAT)` |
| `InEmergency` | Keeps only aircraft squawking a non-routine emergency/priority code. | `readsb.InEmergency()` |
| `IsMLAT` | Keeps only aircraft whose position was derived by multilateration. | `readsb.IsMLAT()` |
| `MinAltitude` | Keeps airborne aircraft at or above the given barometric altitude (feet). | `readsb.MinAltitude(30000)` |
| `MaxAltitude` | Keeps aircraft at or below the given barometric altitude (feet); on-ground aircraft always pass. | `readsb.MaxAltitude(10000)` |
| `OnGround` | Keeps only aircraft reporting an on-ground barometric altitude. | `readsb.OnGround()` |
| `MinSpeed` | Keeps aircraft at or above knots, measured by src (GroundSpeed or TrueAirspeed). | `readsb.MinSpeed(200, readsb.GroundSpeed)` |
| `MaxSpeed` | Keeps aircraft with a positive reading at or below knots, measured by src; aircraft with no reading are dropped. | `readsb.MaxSpeed(500, readsb.TrueAirspeed)` |
| `WithinNM` | Keeps aircraft within the given range (nautical miles) of the receiver. | `readsb.WithinNM(30)` |
| `WithinNMOf` | Keeps aircraft whose current position is within nm nautical miles of the given point (decimal degrees). | `readsb.WithinNMOf(40.7, -74.0, 50)` |
| `SeenWithin` | Keeps aircraft heard from within the given duration. | `readsb.SeenWithin(5*time.Minute)` |
| `MinRSSI` | Keeps aircraft whose average signal strength is at or above dbfs (e.g. -24). | `readsb.MinRSSI(-24)` |
| `Not` | Inverts a filter. | `readsb.Not(readsb.OnGround())` |
| `Any` | Keeps aircraft matching at least one of the supplied filters (logical OR). | `readsb.Any(f1, f2)` |
### Streaming ### Streaming
+23
View File
@@ -2,6 +2,7 @@ package readsb
import ( import (
"iter" "iter"
"math"
"slices" "slices"
"strings" "strings"
"time" "time"
@@ -141,6 +142,28 @@ func WithinNM(nm float64) AircraftFilter {
return func(a *Aircraft) bool { return a.RDst > 0 && a.RDst <= nm } return func(a *Aircraft) bool { return a.RDst > 0 && a.RDst <= nm }
} }
// WithinNMOf keeps aircraft whose current position is within nm nautical miles
// of the given point (decimal degrees). Unlike WithinNM, which measures from the
// receiver, this measures from an arbitrary point; aircraft with no position are
// dropped.
func WithinNMOf(lat, lon, nm float64) AircraftFilter {
return func(a *Aircraft) bool {
return a.HasPosition() && haversineNM(lat, lon, a.Lat, a.Lon) <= nm
}
}
// haversineNM returns the great-circle distance in nautical miles between two
// points given in decimal degrees.
func haversineNM(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusNM = 3440.065 // mean Earth radius in nautical miles
dLat, dLon := rad(lat2-lat1), rad(lon2-lon1)
h := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(rad(lat1))*math.Cos(rad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
return earthRadiusNM * 2 * math.Asin(math.Sqrt(h))
}
func rad(deg float64) float64 { return deg * math.Pi / 180 }
// SeenWithin keeps aircraft heard from within the given duration. // SeenWithin keeps aircraft heard from within the given duration.
func SeenWithin(d time.Duration) AircraftFilter { func SeenWithin(d time.Duration) AircraftFilter {
return func(a *Aircraft) bool { return a.SeenFor() <= d } return func(a *Aircraft) bool { return a.SeenFor() <= d }
+25
View File
@@ -88,6 +88,31 @@ func TestAircraftFilters(t *testing.T) {
} }
} }
func TestWithinNMOf(t *testing.T) {
// One degree of latitude is ~60 nm; sanity-check the haversine helper.
if d := haversineNM(0, 0, 1, 0); d < 59 || d > 61 {
t.Errorf("haversineNM(1 deg lat) = %.2f nm, want ~60", d)
}
r := decodeFile[AircraftReport](t, "aircraft.json")
var lat, lon float64
for _, a := range r.Aircraft {
if a.HasPosition() {
lat, lon = a.Lat, a.Lon
break
}
}
near := r.Filter(WithinNMOf(lat, lon, 100))
for _, a := range near {
if !a.HasPosition() || haversineNM(lat, lon, a.Lat, a.Lon) > 100 {
t.Errorf("WithinNMOf kept %s at %.1f nm", a.Hex, haversineNM(lat, lon, a.Lat, a.Lon))
}
}
// The point came from a real aircraft, so at least that one must match.
if len(near) == 0 {
t.Error("WithinNMOf returned no aircraft for an in-data point")
}
}
func TestAltBaroGround(t *testing.T) { func TestAltBaroGround(t *testing.T) {
var a AltBaro var a AltBaro
if err := json.Unmarshal([]byte(`"ground"`), &a); err != nil { if err := json.Unmarshal([]byte(`"ground"`), &a); err != nil {