feedparser_rs/namespace/
georss.rs

1//! GeoRSS namespace support for geographic location data
2//!
3//! Supports parsing GeoRSS Simple elements for specifying geographic locations
4//! in RSS and Atom feeds. GeoRSS is commonly used in mapping applications,
5//! location-based services, and geocoded content.
6//!
7//! # Supported Elements
8//!
9//! - `georss:point` - Single latitude/longitude point
10//! - `georss:line` - Line string (multiple points)
11//! - `georss:polygon` - Polygon (closed shape)
12//! - `georss:box` - Bounding box (lower-left + upper-right)
13//!
14//! # Specification
15//!
16//! GeoRSS Simple: <http://www.georss.org/simple>
17
18use crate::limits::ParserLimits;
19use crate::types::Entry;
20
21/// `GeoRSS` namespace URI
22pub const GEORSS: &str = "http://www.georss.org/georss";
23
24/// Type of geographic shape
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum GeoType {
27    /// Single point (latitude, longitude)
28    #[default]
29    Point,
30    /// Line connecting multiple points
31    Line,
32    /// Closed polygon shape
33    Polygon,
34    /// Bounding box (lower-left, upper-right corners)
35    Box,
36}
37
38/// Geographic location data from `GeoRSS`
39#[derive(Debug, Clone, Default, PartialEq)]
40pub struct GeoLocation {
41    /// Type of geographic shape
42    pub geo_type: GeoType,
43    /// Coordinate pairs as (latitude, longitude)
44    ///
45    /// - Point: 1 coordinate pair
46    /// - Line: 2+ coordinate pairs
47    /// - Polygon: 3+ coordinate pairs (first == last for closed polygon)
48    /// - Box: 2 coordinate pairs (lower-left, upper-right)
49    pub coordinates: Vec<(f64, f64)>,
50    /// Coordinate reference system (e.g., "EPSG:4326" for WGS84)
51    ///
52    /// Default is WGS84 (latitude/longitude) if not specified
53    pub srs_name: Option<String>,
54}
55
56impl GeoLocation {
57    /// Creates new point location
58    ///
59    /// # Arguments
60    ///
61    /// * `lat` - Latitude in decimal degrees
62    /// * `lon` - Longitude in decimal degrees
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use feedparser_rs::namespace::georss::GeoLocation;
68    ///
69    /// let loc = GeoLocation::point(45.256, -71.92);
70    /// assert_eq!(loc.coordinates.len(), 1);
71    /// ```
72    #[must_use]
73    pub fn point(lat: f64, lon: f64) -> Self {
74        Self {
75            geo_type: GeoType::Point,
76            coordinates: vec![(lat, lon)],
77            srs_name: None,
78        }
79    }
80
81    /// Creates new line location
82    ///
83    /// # Arguments
84    ///
85    /// * `coords` - Vector of (latitude, longitude) pairs
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use feedparser_rs::namespace::georss::GeoLocation;
91    ///
92    /// let coords = vec![(45.256, -71.92), (46.0, -72.0)];
93    /// let loc = GeoLocation::line(coords);
94    /// assert_eq!(loc.coordinates.len(), 2);
95    /// ```
96    #[must_use]
97    pub const fn line(coords: Vec<(f64, f64)>) -> Self {
98        Self {
99            geo_type: GeoType::Line,
100            coordinates: coords,
101            srs_name: None,
102        }
103    }
104
105    /// Creates new polygon location
106    ///
107    /// # Arguments
108    ///
109    /// * `coords` - Vector of (latitude, longitude) pairs
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use feedparser_rs::namespace::georss::GeoLocation;
115    ///
116    /// let coords = vec![
117    ///     (45.0, -71.0),
118    ///     (46.0, -71.0),
119    ///     (46.0, -72.0),
120    ///     (45.0, -71.0), // Close the polygon
121    /// ];
122    /// let loc = GeoLocation::polygon(coords);
123    /// ```
124    #[must_use]
125    pub const fn polygon(coords: Vec<(f64, f64)>) -> Self {
126        Self {
127            geo_type: GeoType::Polygon,
128            coordinates: coords,
129            srs_name: None,
130        }
131    }
132
133    /// Creates new bounding box location
134    ///
135    /// # Arguments
136    ///
137    /// * `lower_lat` - Lower-left latitude
138    /// * `lower_lon` - Lower-left longitude
139    /// * `upper_lat` - Upper-right latitude
140    /// * `upper_lon` - Upper-right longitude
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use feedparser_rs::namespace::georss::GeoLocation;
146    ///
147    /// let loc = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
148    /// assert_eq!(loc.coordinates.len(), 2);
149    /// ```
150    #[must_use]
151    pub fn bbox(lower_lat: f64, lower_lon: f64, upper_lat: f64, upper_lon: f64) -> Self {
152        Self {
153            geo_type: GeoType::Box,
154            coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
155            srs_name: None,
156        }
157    }
158}
159
160/// Parse `GeoRSS` element and update entry
161///
162/// # Arguments
163///
164/// * `tag` - Element local name (e.g., "point", "line", "polygon", "box")
165/// * `text` - Element text content
166/// * `entry` - Entry to update
167/// * `_limits` - Parser limits (unused but kept for API consistency)
168///
169/// # Returns
170///
171/// `true` if element was recognized and handled, `false` otherwise
172pub fn handle_entry_element(
173    tag: &[u8],
174    text: &str,
175    entry: &mut Entry,
176    _limits: &ParserLimits,
177) -> bool {
178    match tag {
179        b"point" => {
180            if let Some(loc) = parse_point(text) {
181                entry.geo = Some(loc);
182            }
183            true
184        }
185        b"line" => {
186            if let Some(loc) = parse_line(text) {
187                entry.geo = Some(loc);
188            }
189            true
190        }
191        b"polygon" => {
192            if let Some(loc) = parse_polygon(text) {
193                entry.geo = Some(loc);
194            }
195            true
196        }
197        b"box" => {
198            if let Some(loc) = parse_box(text) {
199                entry.geo = Some(loc);
200            }
201            true
202        }
203        _ => false,
204    }
205}
206
207/// Parse georss:point element
208///
209/// Format: "lat lon" (space-separated)
210/// Example: "45.256 -71.92"
211fn parse_point(text: &str) -> Option<GeoLocation> {
212    let coords = parse_coordinates(text)?;
213    if coords.len() == 1 {
214        Some(GeoLocation {
215            geo_type: GeoType::Point,
216            coordinates: coords,
217            srs_name: None,
218        })
219    } else {
220        None
221    }
222}
223
224/// Parse georss:line element
225///
226/// Format: "lat1 lon1 lat2 lon2 ..." (space-separated)
227/// Example: "45.256 -71.92 46.0 -72.0"
228fn parse_line(text: &str) -> Option<GeoLocation> {
229    let coords = parse_coordinates(text)?;
230    if coords.len() >= 2 {
231        Some(GeoLocation {
232            geo_type: GeoType::Line,
233            coordinates: coords,
234            srs_name: None,
235        })
236    } else {
237        None
238    }
239}
240
241/// Parse georss:polygon element
242///
243/// Format: "lat1 lon1 lat2 lon2 lat3 lon3 ..." (space-separated)
244/// Example: "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0"
245fn parse_polygon(text: &str) -> Option<GeoLocation> {
246    let coords = parse_coordinates(text)?;
247    if coords.len() >= 3 {
248        Some(GeoLocation {
249            geo_type: GeoType::Polygon,
250            coordinates: coords,
251            srs_name: None,
252        })
253    } else {
254        None
255    }
256}
257
258/// Parse georss:box element
259///
260/// Format: space-separated values (lower-left, upper-right)
261/// Example: "45.0 -72.0 46.0 -71.0"
262fn parse_box(text: &str) -> Option<GeoLocation> {
263    let coords = parse_coordinates(text)?;
264    if coords.len() == 2 {
265        Some(GeoLocation {
266            geo_type: GeoType::Box,
267            coordinates: coords,
268            srs_name: None,
269        })
270    } else {
271        None
272    }
273}
274
275/// Parse space-separated coordinate pairs
276///
277/// Format: "lat1 lon1 lat2 lon2 ..." (pairs of floats)
278fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
279    let parts: Vec<&str> = text.split_whitespace().collect();
280
281    // Must have even number of values (lat/lon pairs)
282    if parts.is_empty() || !parts.len().is_multiple_of(2) {
283        return None;
284    }
285
286    let mut coords = Vec::with_capacity(parts.len() / 2);
287
288    for chunk in parts.chunks(2) {
289        let lat = chunk[0].parse::<f64>().ok()?;
290        let lon = chunk[1].parse::<f64>().ok()?;
291
292        // Basic validation: latitude should be -90 to 90, longitude -180 to 180
293        if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
294            return None;
295        }
296
297        coords.push((lat, lon));
298    }
299
300    Some(coords)
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_parse_point() {
309        let loc = parse_point("45.256 -71.92").unwrap();
310        assert_eq!(loc.geo_type, GeoType::Point);
311        assert_eq!(loc.coordinates.len(), 1);
312        assert_eq!(loc.coordinates[0], (45.256, -71.92));
313    }
314
315    #[test]
316    fn test_parse_point_invalid() {
317        assert!(parse_point("45.256").is_none());
318        assert!(parse_point("45.256 -71.92 extra").is_none());
319        assert!(parse_point("not numbers").is_none());
320        assert!(parse_point("").is_none());
321    }
322
323    #[test]
324    fn test_parse_line() {
325        let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
326        assert_eq!(loc.geo_type, GeoType::Line);
327        assert_eq!(loc.coordinates.len(), 2);
328        assert_eq!(loc.coordinates[0], (45.256, -71.92));
329        assert_eq!(loc.coordinates[1], (46.0, -72.0));
330    }
331
332    #[test]
333    fn test_parse_line_single_point() {
334        // Line needs at least 2 points
335        assert!(parse_line("45.256 -71.92").is_none());
336    }
337
338    #[test]
339    fn test_parse_polygon() {
340        let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
341        assert_eq!(loc.geo_type, GeoType::Polygon);
342        assert_eq!(loc.coordinates.len(), 4);
343        assert_eq!(loc.coordinates[0], (45.0, -71.0));
344        assert_eq!(loc.coordinates[3], (45.0, -71.0)); // Closed polygon
345    }
346
347    #[test]
348    fn test_parse_box() {
349        let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
350        assert_eq!(loc.geo_type, GeoType::Box);
351        assert_eq!(loc.coordinates.len(), 2);
352        assert_eq!(loc.coordinates[0], (45.0, -72.0)); // Lower-left
353        assert_eq!(loc.coordinates[1], (46.0, -71.0)); // Upper-right
354    }
355
356    #[test]
357    fn test_parse_box_invalid() {
358        // Box needs exactly 2 points (4 values)
359        assert!(parse_box("45.0 -72.0").is_none());
360        assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
361    }
362
363    #[test]
364    fn test_coordinate_validation() {
365        // Invalid latitude (> 90)
366        assert!(parse_point("91.0 0.0").is_none());
367        // Invalid latitude (< -90)
368        assert!(parse_point("-91.0 0.0").is_none());
369        // Invalid longitude (> 180)
370        assert!(parse_point("0.0 181.0").is_none());
371        // Invalid longitude (< -180)
372        assert!(parse_point("0.0 -181.0").is_none());
373    }
374
375    #[test]
376    fn test_handle_entry_element_point() {
377        let mut entry = Entry::default();
378        let limits = ParserLimits::default();
379
380        let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
381        assert!(handled);
382        assert!(entry.geo.is_some());
383
384        let geo = entry.geo.as_ref().unwrap();
385        assert_eq!(geo.geo_type, GeoType::Point);
386        assert_eq!(geo.coordinates[0], (45.256, -71.92));
387    }
388
389    #[test]
390    fn test_handle_entry_element_line() {
391        let mut entry = Entry::default();
392        let limits = ParserLimits::default();
393
394        let handled =
395            handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
396        assert!(handled);
397        assert!(entry.geo.is_some());
398        assert_eq!(entry.geo.as_ref().unwrap().geo_type, GeoType::Line);
399    }
400
401    #[test]
402    fn test_handle_entry_element_unknown() {
403        let mut entry = Entry::default();
404        let limits = ParserLimits::default();
405
406        let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
407        assert!(!handled);
408        assert!(entry.geo.is_none());
409    }
410
411    #[test]
412    fn test_geo_location_constructors() {
413        let point = GeoLocation::point(45.0, -71.0);
414        assert_eq!(point.geo_type, GeoType::Point);
415        assert_eq!(point.coordinates.len(), 1);
416
417        let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
418        assert_eq!(line.geo_type, GeoType::Line);
419        assert_eq!(line.coordinates.len(), 2);
420
421        let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
422        assert_eq!(polygon.geo_type, GeoType::Polygon);
423        assert_eq!(polygon.coordinates.len(), 3);
424
425        let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
426        assert_eq!(bbox.geo_type, GeoType::Box);
427        assert_eq!(bbox.coordinates.len(), 2);
428    }
429
430    #[test]
431    fn test_whitespace_handling() {
432        let loc = parse_point("  45.256   -71.92  ").unwrap();
433        assert_eq!(loc.coordinates[0], (45.256, -71.92));
434    }
435}