+17
-1
@@ -5,6 +5,20 @@ All notable changes to this project are documented here.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-06-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `And` aircraft filter combinator (logical AND) — the value form of the
|
||||||
|
implicit AND that `Filter`/`All` apply to their arguments. Pairs with `Or`/`Not`
|
||||||
|
for nesting, e.g. `Or(And(a, b), And(c, d))`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Renamed the `Any` filter combinator to `Or`, giving a consistent
|
||||||
|
`And`/`Or`/`Not` vocabulary. **Breaking:** callers using `Any` must switch to
|
||||||
|
`Or`.
|
||||||
|
|
||||||
## [0.3.2] - 2026-06-26
|
## [0.3.2] - 2026-06-26
|
||||||
|
|
||||||
- `WithinNMOf` aircraft filter for distance from an arbitrary point.
|
- `WithinNMOf` aircraft filter for distance from an arbitrary point.
|
||||||
@@ -56,7 +70,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
the `pkg/client` HTTP client with one-shot queries and channel/iterator
|
the `pkg/client` HTTP client with one-shot queries and channel/iterator
|
||||||
polling for every endpoint, the `cmd/wingbits` CLI, and a README.
|
polling for every endpoint, the `cmd/wingbits` CLI, and a README.
|
||||||
|
|
||||||
[Unreleased]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.1...HEAD
|
[Unreleased]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.4.0...HEAD
|
||||||
|
[0.4.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.2...v0.4.0
|
||||||
|
[0.3.2]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.1...v0.3.2
|
||||||
[0.3.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.0...v0.3.1
|
[0.3.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.0...v0.3.1
|
||||||
[0.3.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.1...v0.3.0
|
[0.3.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.1...v0.3.0
|
||||||
[0.2.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.0...v0.2.1
|
[0.2.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.0...v0.2.1
|
||||||
|
|||||||
@@ -140,9 +140,15 @@ local, _ := c.Aircraft(ctx,
|
|||||||
|
|
||||||
// OR-composition and negation:
|
// OR-composition and negation:
|
||||||
heavies, _ := c.Aircraft(ctx,
|
heavies, _ := c.Aircraft(ctx,
|
||||||
readsb.Any(readsb.WithCategory("A5"), readsb.WithType("mlat")),
|
readsb.Or(readsb.WithCategory("A5"), readsb.WithType("mlat")),
|
||||||
readsb.Not(readsb.OnGround()),
|
readsb.Not(readsb.OnGround()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// And is the value form of the implicit AND, for nesting inside Or:
|
||||||
|
nearby, _ := c.Aircraft(ctx, readsb.Or(
|
||||||
|
readsb.And(readsb.WithCategory("A7"), readsb.WithinNMOf(40.7, -74.0, 5)),
|
||||||
|
readsb.And(readsb.WithCategory("A7"), readsb.WithinNMOf(41.9, -72.7, 5)),
|
||||||
|
))
|
||||||
```
|
```
|
||||||
|
|
||||||
| Filter Name | Description | Sample Usage |
|
| Filter Name | Description | Sample Usage |
|
||||||
@@ -165,7 +171,8 @@ heavies, _ := c.Aircraft(ctx,
|
|||||||
| `SeenWithin` | Keeps aircraft heard from within the given duration. | `readsb.SeenWithin(5*time.Minute)` |
|
| `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)` |
|
| `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())` |
|
| `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)` |
|
| `And` | Keeps aircraft matching every supplied filter (logical AND). | `readsb.And(f1, f2)` |
|
||||||
|
| `Or` | Keeps aircraft matching at least one of the supplied filters (logical OR). | `readsb.Or(f1, f2)` |
|
||||||
|
|
||||||
### Streaming
|
### Streaming
|
||||||
|
|
||||||
|
|||||||
@@ -174,13 +174,22 @@ func MinRSSI(dbfs float64) AircraftFilter {
|
|||||||
return func(a *Aircraft) bool { return a.RSSI >= dbfs }
|
return func(a *Aircraft) bool { return a.RSSI >= dbfs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// And, Or, and Not compose filters as boolean operators. Filter/All already AND
|
||||||
|
// their arguments; And is the value form, letting you nest, e.g.
|
||||||
|
// Or(And(a, b), And(c, d)).
|
||||||
|
|
||||||
// Not inverts a filter.
|
// Not inverts a filter.
|
||||||
func Not(f AircraftFilter) AircraftFilter {
|
func Not(f AircraftFilter) AircraftFilter {
|
||||||
return func(a *Aircraft) bool { return !f(a) }
|
return func(a *Aircraft) bool { return !f(a) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any keeps aircraft matching at least one of the supplied filters (logical OR).
|
// And keeps aircraft matching every supplied filter (logical AND).
|
||||||
func Any(filters ...AircraftFilter) AircraftFilter {
|
func And(filters ...AircraftFilter) AircraftFilter {
|
||||||
|
return func(a *Aircraft) bool { return keep(a, filters) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or keeps aircraft matching at least one of the supplied filters (logical OR).
|
||||||
|
func Or(filters ...AircraftFilter) AircraftFilter {
|
||||||
return func(a *Aircraft) bool {
|
return func(a *Aircraft) bool {
|
||||||
for _, f := range filters {
|
for _, f := range filters {
|
||||||
if f(a) {
|
if f(a) {
|
||||||
|
|||||||
@@ -113,21 +113,34 @@ func TestWithinNMOf(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnyOR(t *testing.T) {
|
func TestOr(t *testing.T) {
|
||||||
// Two non-overlapping 5 nm circles; Any should keep aircraft in either.
|
// Two non-overlapping 5 nm circles; Or should keep aircraft in either.
|
||||||
r := &AircraftReport{Aircraft: []Aircraft{
|
r := &AircraftReport{Aircraft: []Aircraft{
|
||||||
{Hex: "nearA", Lat: 40.01, Lon: -75.0},
|
{Hex: "nearA", Lat: 40.01, Lon: -75.0},
|
||||||
{Hex: "nearB", Lat: 41.0, Lon: -76.01},
|
{Hex: "nearB", Lat: 41.0, Lon: -76.01},
|
||||||
{Hex: "between", Lat: 40.5, Lon: -75.5},
|
{Hex: "between", Lat: 40.5, Lon: -75.5},
|
||||||
{Hex: "nopos"},
|
{Hex: "nopos"},
|
||||||
}}
|
}}
|
||||||
near := r.Filter(Any(WithinNMOf(40, -75, 5), WithinNMOf(41, -76, 5)))
|
near := r.Filter(Or(WithinNMOf(40, -75, 5), WithinNMOf(41, -76, 5)))
|
||||||
got := map[string]bool{}
|
got := map[string]bool{}
|
||||||
for _, a := range near {
|
for _, a := range near {
|
||||||
got[a.Hex] = true
|
got[a.Hex] = true
|
||||||
}
|
}
|
||||||
if !got["nearA"] || !got["nearB"] || got["between"] || got["nopos"] || len(got) != 2 {
|
if !got["nearA"] || !got["nearB"] || got["between"] || got["nopos"] || len(got) != 2 {
|
||||||
t.Errorf("Any(WithinNMOf...) OR mismatch, matched: %v", got)
|
t.Errorf("Or(WithinNMOf...) mismatch, matched: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnd(t *testing.T) {
|
||||||
|
// And matches only aircraft passing every filter; here position AND in-circle.
|
||||||
|
r := &AircraftReport{Aircraft: []Aircraft{
|
||||||
|
{Hex: "in", Lat: 40.01, Lon: -75.0},
|
||||||
|
{Hex: "out", Lat: 50.0, Lon: -75.0},
|
||||||
|
{Hex: "nopos"},
|
||||||
|
}}
|
||||||
|
near := r.Filter(And(WithPosition(), WithinNMOf(40, -75, 5)))
|
||||||
|
if len(near) != 1 || near[0].Hex != "in" {
|
||||||
|
t.Errorf("And mismatch, matched: %+v", near)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user