aprs_is/filter.rs
1//! APRS-IS server-side filter expressions.
2
3/// Structured APRS-IS filter expression.
4///
5/// Per <http://www.aprs-is.net/javAPRSFilter.aspx>, APRS-IS servers
6/// accept a small query language for selecting which packets to deliver
7/// to a client connection. Each filter is one or more tokens separated
8/// by spaces. This enum covers the commonly-used forms; use
9/// [`AprsIsFilter::raw`] to drop in any literal filter string for
10/// advanced cases.
11#[derive(Debug, Clone, PartialEq)]
12pub enum AprsIsFilter {
13 /// Range filter `r/lat/lon/distance_km` — packets from stations
14 /// within the given radius.
15 Range {
16 /// Centre latitude in degrees (positive = North).
17 lat: f64,
18 /// Centre longitude in degrees (positive = East).
19 lon: f64,
20 /// Radius in kilometres.
21 distance_km: f64,
22 },
23 /// Area / box filter `a/lat1/lon1/lat2/lon2` — packets within a
24 /// lat/lon bounding box (NW and SE corners).
25 Area {
26 /// Northwest latitude.
27 lat1: f64,
28 /// Northwest longitude.
29 lon1: f64,
30 /// Southeast latitude.
31 lat2: f64,
32 /// Southeast longitude.
33 lon2: f64,
34 },
35 /// Prefix filter `p/aa/bb/cc` — packets whose source callsign
36 /// begins with any of the given prefixes.
37 Prefix(Vec<String>),
38 /// Budlist filter `b/call1/call2` — packets from exactly these
39 /// stations.
40 Budlist(Vec<String>),
41 /// Object filter `o/obj1/obj2` — object reports with these names.
42 Object(Vec<String>),
43 /// Type filter `t/poimntqsu` — characters select which frame types
44 /// are wanted (p=position, o=object, i=item, m=message, n=nws,
45 /// t=telemetry, q=query, s=status, u=user-defined).
46 Type(String),
47 /// Symbol filter `s/sym1sym2/...` — symbols to include.
48 Symbol(String),
49 /// "Friend" / range-around-station filter `f/call/distance_km`.
50 Friend {
51 /// Station to centre on.
52 callsign: String,
53 /// Distance in km.
54 distance_km: f64,
55 },
56 /// Group message filter `g/name` — bulletins addressed to this
57 /// group.
58 Group(String),
59 /// Raw literal filter string for advanced / uncommon cases.
60 Raw(String),
61}
62
63impl AprsIsFilter {
64 /// Build a raw literal filter expression.
65 #[must_use]
66 pub fn raw(s: impl Into<String>) -> Self {
67 Self::Raw(s.into())
68 }
69
70 /// Format this filter as the exact wire-format string APRS-IS
71 /// servers expect after the `filter ` keyword in the login line.
72 #[must_use]
73 pub fn as_wire(&self) -> String {
74 match self {
75 Self::Range {
76 lat,
77 lon,
78 distance_km,
79 } => format!("r/{lat}/{lon}/{distance_km}"),
80 Self::Area {
81 lat1,
82 lon1,
83 lat2,
84 lon2,
85 } => format!("a/{lat1}/{lon1}/{lat2}/{lon2}"),
86 Self::Prefix(parts) => format!("p/{}", parts.join("/")),
87 Self::Budlist(parts) => format!("b/{}", parts.join("/")),
88 Self::Object(parts) => format!("o/{}", parts.join("/")),
89 Self::Type(chars) => format!("t/{chars}"),
90 Self::Symbol(chars) => format!("s/{chars}"),
91 Self::Friend {
92 callsign,
93 distance_km,
94 } => format!("f/{callsign}/{distance_km}"),
95 Self::Group(name) => format!("g/{name}"),
96 Self::Raw(s) => s.clone(),
97 }
98 }
99
100 /// Combine multiple filter clauses into a single filter string by
101 /// joining with spaces — APRS-IS allows an OR of any number of
102 /// clauses in a single `filter` directive.
103 #[must_use]
104 pub fn join(filters: &[Self]) -> String {
105 filters
106 .iter()
107 .map(Self::as_wire)
108 .collect::<Vec<_>>()
109 .join(" ")
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn aprs_is_filter_range_wire_format() {
119 let f = AprsIsFilter::Range {
120 lat: 35.25,
121 lon: -97.75,
122 distance_km: 100.0,
123 };
124 assert_eq!(f.as_wire(), "r/35.25/-97.75/100");
125 }
126
127 #[test]
128 fn aprs_is_filter_type_and_prefix() {
129 let f = AprsIsFilter::Type("po".to_owned());
130 assert_eq!(f.as_wire(), "t/po");
131 let f = AprsIsFilter::Prefix(vec!["KK".to_owned(), "W1".to_owned()]);
132 assert_eq!(f.as_wire(), "p/KK/W1");
133 }
134
135 #[test]
136 fn aprs_is_filter_join_multiple() {
137 let filters = vec![
138 AprsIsFilter::Range {
139 lat: 35.0,
140 lon: -97.0,
141 distance_km: 50.0,
142 },
143 AprsIsFilter::Type("p".to_owned()),
144 ];
145 let joined = AprsIsFilter::join(&filters);
146 assert!(joined.contains("r/35"), "missing range clause: {joined:?}");
147 assert!(joined.contains("t/p"), "missing type clause: {joined:?}");
148 assert!(joined.contains(' '), "missing separator: {joined:?}");
149 }
150
151 #[test]
152 fn aprs_is_filter_raw_passthrough() {
153 let f = AprsIsFilter::raw("m/50");
154 assert_eq!(f.as_wire(), "m/50");
155 }
156}