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, FeedMeta};
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(Box::new(loc));
182            }
183            true
184        }
185        b"line" => {
186            if let Some(loc) = parse_line(text) {
187                entry.geo = Some(Box::new(loc));
188            }
189            true
190        }
191        b"polygon" => {
192            if let Some(loc) = parse_polygon(text) {
193                entry.geo = Some(Box::new(loc));
194            }
195            true
196        }
197        b"box" => {
198            if let Some(loc) = parse_box(text) {
199                entry.geo = Some(Box::new(loc));
200            }
201            true
202        }
203        _ => false,
204    }
205}
206
207/// Parse `GeoRSS` element and update feed metadata
208///
209/// # Arguments
210///
211/// * `tag` - Element local name (e.g., "point", "line", "polygon", "box")
212/// * `text` - Element text content
213/// * `feed` - Feed metadata to update
214/// * `_limits` - Parser limits (unused but kept for API consistency)
215///
216/// # Returns
217///
218/// `true` if element was recognized and handled, `false` otherwise
219pub fn handle_feed_element(
220    tag: &[u8],
221    text: &str,
222    feed: &mut FeedMeta,
223    _limits: &ParserLimits,
224) -> bool {
225    match tag {
226        b"point" => {
227            if let Some(loc) = parse_point(text) {
228                feed.geo = Some(Box::new(loc));
229            }
230            true
231        }
232        b"line" => {
233            if let Some(loc) = parse_line(text) {
234                feed.geo = Some(Box::new(loc));
235            }
236            true
237        }
238        b"polygon" => {
239            if let Some(loc) = parse_polygon(text) {
240                feed.geo = Some(Box::new(loc));
241            }
242            true
243        }
244        b"box" => {
245            if let Some(loc) = parse_box(text) {
246                feed.geo = Some(Box::new(loc));
247            }
248            true
249        }
250        _ => false,
251    }
252}
253
254/// Parse georss:point element
255///
256/// Format: "lat lon" (space-separated)
257/// Example: "45.256 -71.92"
258fn parse_point(text: &str) -> Option<GeoLocation> {
259    let coords = parse_coordinates(text)?;
260    if coords.len() == 1 {
261        Some(GeoLocation {
262            geo_type: GeoType::Point,
263            coordinates: coords,
264            srs_name: None,
265        })
266    } else {
267        None
268    }
269}
270
271/// Parse georss:line element
272///
273/// Format: "lat1 lon1 lat2 lon2 ..." (space-separated)
274/// Example: "45.256 -71.92 46.0 -72.0"
275fn parse_line(text: &str) -> Option<GeoLocation> {
276    let coords = parse_coordinates(text)?;
277    if coords.len() >= 2 {
278        Some(GeoLocation {
279            geo_type: GeoType::Line,
280            coordinates: coords,
281            srs_name: None,
282        })
283    } else {
284        None
285    }
286}
287
288/// Parse georss:polygon element
289///
290/// Format: "lat1 lon1 lat2 lon2 lat3 lon3 ..." (space-separated)
291/// Example: "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0"
292fn parse_polygon(text: &str) -> Option<GeoLocation> {
293    let coords = parse_coordinates(text)?;
294    if coords.len() >= 3 {
295        Some(GeoLocation {
296            geo_type: GeoType::Polygon,
297            coordinates: coords,
298            srs_name: None,
299        })
300    } else {
301        None
302    }
303}
304
305/// Parse georss:box element
306///
307/// Format: space-separated values (lower-left, upper-right)
308/// Example: "45.0 -72.0 46.0 -71.0"
309fn parse_box(text: &str) -> Option<GeoLocation> {
310    let coords = parse_coordinates(text)?;
311    if coords.len() == 2 {
312        Some(GeoLocation {
313            geo_type: GeoType::Box,
314            coordinates: coords,
315            srs_name: None,
316        })
317    } else {
318        None
319    }
320}
321
322/// Parse space-separated coordinate pairs
323///
324/// Format: "lat1 lon1 lat2 lon2 ..." (pairs of floats)
325fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
326    let parts: Vec<&str> = text.split_whitespace().collect();
327
328    // Must have even number of values (lat/lon pairs)
329    if parts.is_empty() || !parts.len().is_multiple_of(2) {
330        return None;
331    }
332
333    let mut coords = Vec::with_capacity(parts.len() / 2);
334
335    for chunk in parts.chunks(2) {
336        let lat = chunk[0].parse::<f64>().ok()?;
337        let lon = chunk[1].parse::<f64>().ok()?;
338
339        // Basic validation: latitude should be -90 to 90, longitude -180 to 180
340        if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
341            return None;
342        }
343
344        coords.push((lat, lon));
345    }
346
347    Some(coords)
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn test_parse_point() {
356        let loc = parse_point("45.256 -71.92").unwrap();
357        assert_eq!(loc.geo_type, GeoType::Point);
358        assert_eq!(loc.coordinates.len(), 1);
359        assert_eq!(loc.coordinates[0], (45.256, -71.92));
360    }
361
362    #[test]
363    fn test_parse_point_invalid() {
364        assert!(parse_point("45.256").is_none());
365        assert!(parse_point("45.256 -71.92 extra").is_none());
366        assert!(parse_point("not numbers").is_none());
367        assert!(parse_point("").is_none());
368    }
369
370    #[test]
371    fn test_parse_line() {
372        let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
373        assert_eq!(loc.geo_type, GeoType::Line);
374        assert_eq!(loc.coordinates.len(), 2);
375        assert_eq!(loc.coordinates[0], (45.256, -71.92));
376        assert_eq!(loc.coordinates[1], (46.0, -72.0));
377    }
378
379    #[test]
380    fn test_parse_line_single_point() {
381        // Line needs at least 2 points
382        assert!(parse_line("45.256 -71.92").is_none());
383    }
384
385    #[test]
386    fn test_parse_polygon() {
387        let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
388        assert_eq!(loc.geo_type, GeoType::Polygon);
389        assert_eq!(loc.coordinates.len(), 4);
390        assert_eq!(loc.coordinates[0], (45.0, -71.0));
391        assert_eq!(loc.coordinates[3], (45.0, -71.0)); // Closed polygon
392    }
393
394    #[test]
395    fn test_parse_box() {
396        let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
397        assert_eq!(loc.geo_type, GeoType::Box);
398        assert_eq!(loc.coordinates.len(), 2);
399        assert_eq!(loc.coordinates[0], (45.0, -72.0)); // Lower-left
400        assert_eq!(loc.coordinates[1], (46.0, -71.0)); // Upper-right
401    }
402
403    #[test]
404    fn test_parse_box_invalid() {
405        // Box needs exactly 2 points (4 values)
406        assert!(parse_box("45.0 -72.0").is_none());
407        assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
408    }
409
410    #[test]
411    fn test_coordinate_validation() {
412        // Invalid latitude (> 90)
413        assert!(parse_point("91.0 0.0").is_none());
414        // Invalid latitude (< -90)
415        assert!(parse_point("-91.0 0.0").is_none());
416        // Invalid longitude (> 180)
417        assert!(parse_point("0.0 181.0").is_none());
418        // Invalid longitude (< -180)
419        assert!(parse_point("0.0 -181.0").is_none());
420    }
421
422    #[test]
423    fn test_handle_entry_element_point() {
424        let mut entry = Entry::default();
425        let limits = ParserLimits::default();
426
427        let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
428        assert!(handled);
429        assert!(entry.geo.is_some());
430
431        let geo = entry.geo.as_ref().unwrap();
432        assert_eq!(geo.geo_type, GeoType::Point);
433        assert_eq!(geo.coordinates[0], (45.256, -71.92));
434    }
435
436    #[test]
437    fn test_handle_entry_element_line() {
438        let mut entry = Entry::default();
439        let limits = ParserLimits::default();
440
441        let handled =
442            handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
443        assert!(handled);
444        assert!(entry.geo.is_some());
445        assert_eq!(entry.geo.as_ref().unwrap().geo_type, GeoType::Line);
446    }
447
448    #[test]
449    fn test_handle_entry_element_unknown() {
450        let mut entry = Entry::default();
451        let limits = ParserLimits::default();
452
453        let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
454        assert!(!handled);
455        assert!(entry.geo.is_none());
456    }
457
458    #[test]
459    fn test_geo_location_constructors() {
460        let point = GeoLocation::point(45.0, -71.0);
461        assert_eq!(point.geo_type, GeoType::Point);
462        assert_eq!(point.coordinates.len(), 1);
463
464        let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
465        assert_eq!(line.geo_type, GeoType::Line);
466        assert_eq!(line.coordinates.len(), 2);
467
468        let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
469        assert_eq!(polygon.geo_type, GeoType::Polygon);
470        assert_eq!(polygon.coordinates.len(), 3);
471
472        let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
473        assert_eq!(bbox.geo_type, GeoType::Box);
474        assert_eq!(bbox.coordinates.len(), 2);
475    }
476
477    #[test]
478    fn test_whitespace_handling() {
479        let loc = parse_point("  45.256   -71.92  ").unwrap();
480        assert_eq!(loc.coordinates[0], (45.256, -71.92));
481    }
482
483    #[test]
484    fn test_handle_feed_element_point() {
485        let mut feed = FeedMeta::default();
486        let limits = ParserLimits::default();
487
488        let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
489        assert!(handled);
490        assert!(feed.geo.is_some());
491
492        let geo = feed.geo.as_ref().unwrap();
493        assert_eq!(geo.geo_type, GeoType::Point);
494        assert_eq!(geo.coordinates[0], (45.256, -71.92));
495    }
496
497    #[test]
498    fn test_handle_feed_element_line() {
499        let mut feed = FeedMeta::default();
500        let limits = ParserLimits::default();
501
502        let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
503        assert!(handled);
504        assert!(feed.geo.is_some());
505        assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Line);
506    }
507
508    #[test]
509    fn test_handle_feed_element_polygon() {
510        let mut feed = FeedMeta::default();
511        let limits = ParserLimits::default();
512
513        let handled = handle_feed_element(
514            b"polygon",
515            "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0",
516            &mut feed,
517            &limits,
518        );
519        assert!(handled);
520        assert!(feed.geo.is_some());
521        assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Polygon);
522    }
523
524    #[test]
525    fn test_handle_feed_element_box() {
526        let mut feed = FeedMeta::default();
527        let limits = ParserLimits::default();
528
529        let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
530        assert!(handled);
531        assert!(feed.geo.is_some());
532        assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Box);
533    }
534
535    #[test]
536    fn test_handle_feed_element_unknown() {
537        let mut feed = FeedMeta::default();
538        let limits = ParserLimits::default();
539
540        let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
541        assert!(!handled);
542        assert!(feed.geo.is_none());
543    }
544
545    #[test]
546    fn test_handle_feed_element_invalid_data() {
547        let mut feed = FeedMeta::default();
548        let limits = ParserLimits::default();
549
550        let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
551        assert!(handled);
552        assert!(feed.geo.is_none());
553    }
554}