1use crate::limits::ParserLimits;
19use crate::types::{Entry, FeedMeta};
20
21pub const GEORSS: &str = "http://www.georss.org/georss";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum GeoType {
27 #[default]
29 Point,
30 Line,
32 Polygon,
34 Box,
36}
37
38#[derive(Debug, Clone, Default, PartialEq)]
40pub struct GeoLocation {
41 pub geo_type: GeoType,
43 pub coordinates: Vec<(f64, f64)>,
50 pub srs_name: Option<String>,
54 pub elev: Option<f64>,
56 pub feature_type_tag: Option<String>,
58 pub feature_name: Option<String>,
60 pub relationship_tag: Option<String>,
62}
63
64impl GeoLocation {
65 #[must_use]
81 pub fn point(lat: f64, lon: f64) -> Self {
82 Self {
83 geo_type: GeoType::Point,
84 coordinates: vec![(lat, lon)],
85 ..Default::default()
86 }
87 }
88
89 #[must_use]
105 pub fn line(coords: Vec<(f64, f64)>) -> Self {
106 Self {
107 geo_type: GeoType::Line,
108 coordinates: coords,
109 ..Default::default()
110 }
111 }
112
113 #[must_use]
133 pub fn polygon(coords: Vec<(f64, f64)>) -> Self {
134 Self {
135 geo_type: GeoType::Polygon,
136 coordinates: coords,
137 ..Default::default()
138 }
139 }
140
141 #[must_use]
159 pub fn bbox(lower_lat: f64, lower_lon: f64, upper_lat: f64, upper_lon: f64) -> Self {
160 Self {
161 geo_type: GeoType::Box,
162 coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
163 ..Default::default()
164 }
165 }
166}
167
168pub fn handle_entry_geo_element(tag: &[u8], text: &str, entry: &mut Entry) -> bool {
183 match tag {
184 b"lat" => {
185 entry.geo_lat = Some(text.to_string());
186 try_build_entry_where(entry);
187 true
188 }
189 b"long" | b"lon" => {
190 entry.geo_long = Some(text.to_string());
191 try_build_entry_where(entry);
192 true
193 }
194 _ => false,
195 }
196}
197
198pub fn handle_feed_geo_element(tag: &[u8], text: &str, feed: &mut FeedMeta) -> bool {
213 match tag {
214 b"lat" => {
215 feed.geo_lat = Some(text.to_string());
216 try_build_feed_where(feed);
217 true
218 }
219 b"long" | b"lon" => {
220 feed.geo_long = Some(text.to_string());
221 try_build_feed_where(feed);
222 true
223 }
224 _ => false,
225 }
226}
227
228fn try_build_entry_where(entry: &mut Entry) {
229 if let (Some(lat_str), Some(lon_str)) = (entry.geo_lat.as_deref(), entry.geo_long.as_deref())
230 && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
231 && (-90.0..=90.0).contains(&lat)
232 && (-180.0..=180.0).contains(&lon)
233 {
234 entry.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
235 }
236}
237
238fn try_build_feed_where(feed: &mut FeedMeta) {
239 if let (Some(lat_str), Some(lon_str)) = (feed.geo_lat.as_deref(), feed.geo_long.as_deref())
240 && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
241 && (-90.0..=90.0).contains(&lat)
242 && (-180.0..=180.0).contains(&lon)
243 {
244 feed.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
245 }
246}
247
248pub fn handle_entry_element(
261 tag: &[u8],
262 text: &str,
263 entry: &mut Entry,
264 _limits: &ParserLimits,
265) -> bool {
266 match tag {
267 b"point" => {
268 if let Some(loc) = parse_point(text) {
269 let existing = entry
270 .r#where
271 .get_or_insert_with(|| Box::new(GeoLocation::default()));
272 existing.geo_type = loc.geo_type;
273 existing.coordinates = loc.coordinates;
274 existing.srs_name = loc.srs_name;
275 }
276 true
277 }
278 b"line" => {
279 if let Some(loc) = parse_line(text) {
280 let existing = entry
281 .r#where
282 .get_or_insert_with(|| Box::new(GeoLocation::default()));
283 existing.geo_type = loc.geo_type;
284 existing.coordinates = loc.coordinates;
285 existing.srs_name = loc.srs_name;
286 }
287 true
288 }
289 b"polygon" => {
290 if let Some(loc) = parse_polygon(text) {
291 let existing = entry
292 .r#where
293 .get_or_insert_with(|| Box::new(GeoLocation::default()));
294 existing.geo_type = loc.geo_type;
295 existing.coordinates = loc.coordinates;
296 existing.srs_name = loc.srs_name;
297 }
298 true
299 }
300 b"box" => {
301 if let Some(loc) = parse_box(text) {
302 let existing = entry
303 .r#where
304 .get_or_insert_with(|| Box::new(GeoLocation::default()));
305 existing.geo_type = loc.geo_type;
306 existing.coordinates = loc.coordinates;
307 existing.srs_name = loc.srs_name;
308 }
309 true
310 }
311 b"elev" => {
312 if let Ok(v) = text.trim().parse::<f64>()
313 && v.is_finite()
314 {
315 entry
316 .r#where
317 .get_or_insert_with(|| Box::new(GeoLocation::default()))
318 .elev = Some(v);
319 }
320 true
321 }
322 b"featuretypetag" => {
323 entry
324 .r#where
325 .get_or_insert_with(|| Box::new(GeoLocation::default()))
326 .feature_type_tag = Some(text.to_string());
327 true
328 }
329 b"featurename" => {
330 entry
331 .r#where
332 .get_or_insert_with(|| Box::new(GeoLocation::default()))
333 .feature_name = Some(text.to_string());
334 true
335 }
336 b"relationshiptag" => {
337 entry
338 .r#where
339 .get_or_insert_with(|| Box::new(GeoLocation::default()))
340 .relationship_tag = Some(text.to_string());
341 true
342 }
343 _ => false,
344 }
345}
346
347pub fn handle_feed_element(
360 tag: &[u8],
361 text: &str,
362 feed: &mut FeedMeta,
363 _limits: &ParserLimits,
364) -> bool {
365 match tag {
366 b"point" => {
367 if let Some(loc) = parse_point(text) {
368 let existing = feed
369 .r#where
370 .get_or_insert_with(|| Box::new(GeoLocation::default()));
371 existing.geo_type = loc.geo_type;
372 existing.coordinates = loc.coordinates;
373 existing.srs_name = loc.srs_name;
374 }
375 true
376 }
377 b"line" => {
378 if let Some(loc) = parse_line(text) {
379 let existing = feed
380 .r#where
381 .get_or_insert_with(|| Box::new(GeoLocation::default()));
382 existing.geo_type = loc.geo_type;
383 existing.coordinates = loc.coordinates;
384 existing.srs_name = loc.srs_name;
385 }
386 true
387 }
388 b"polygon" => {
389 if let Some(loc) = parse_polygon(text) {
390 let existing = feed
391 .r#where
392 .get_or_insert_with(|| Box::new(GeoLocation::default()));
393 existing.geo_type = loc.geo_type;
394 existing.coordinates = loc.coordinates;
395 existing.srs_name = loc.srs_name;
396 }
397 true
398 }
399 b"box" => {
400 if let Some(loc) = parse_box(text) {
401 let existing = feed
402 .r#where
403 .get_or_insert_with(|| Box::new(GeoLocation::default()));
404 existing.geo_type = loc.geo_type;
405 existing.coordinates = loc.coordinates;
406 existing.srs_name = loc.srs_name;
407 }
408 true
409 }
410 b"elev" => {
411 if let Ok(v) = text.trim().parse::<f64>()
412 && v.is_finite()
413 {
414 feed.r#where
415 .get_or_insert_with(|| Box::new(GeoLocation::default()))
416 .elev = Some(v);
417 }
418 true
419 }
420 b"featuretypetag" => {
421 feed.r#where
422 .get_or_insert_with(|| Box::new(GeoLocation::default()))
423 .feature_type_tag = Some(text.to_string());
424 true
425 }
426 b"featurename" => {
427 feed.r#where
428 .get_or_insert_with(|| Box::new(GeoLocation::default()))
429 .feature_name = Some(text.to_string());
430 true
431 }
432 b"relationshiptag" => {
433 feed.r#where
434 .get_or_insert_with(|| Box::new(GeoLocation::default()))
435 .relationship_tag = Some(text.to_string());
436 true
437 }
438 _ => false,
439 }
440}
441
442fn parse_point(text: &str) -> Option<GeoLocation> {
447 let coords = parse_coordinates(text)?;
448 if coords.len() == 1 {
449 Some(GeoLocation {
450 geo_type: GeoType::Point,
451 coordinates: coords,
452 ..Default::default()
453 })
454 } else {
455 None
456 }
457}
458
459fn parse_line(text: &str) -> Option<GeoLocation> {
464 let coords = parse_coordinates(text)?;
465 if coords.len() >= 2 {
466 Some(GeoLocation {
467 geo_type: GeoType::Line,
468 coordinates: coords,
469 ..Default::default()
470 })
471 } else {
472 None
473 }
474}
475
476fn parse_polygon(text: &str) -> Option<GeoLocation> {
481 let coords = parse_coordinates(text)?;
482 if coords.len() >= 3 {
483 Some(GeoLocation {
484 geo_type: GeoType::Polygon,
485 coordinates: coords,
486 ..Default::default()
487 })
488 } else {
489 None
490 }
491}
492
493fn parse_box(text: &str) -> Option<GeoLocation> {
498 let coords = parse_coordinates(text)?;
499 if coords.len() == 2 {
500 Some(GeoLocation {
501 geo_type: GeoType::Box,
502 coordinates: coords,
503 ..Default::default()
504 })
505 } else {
506 None
507 }
508}
509
510fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
514 let parts: Vec<&str> = text.split_whitespace().collect();
515
516 if parts.is_empty() || !parts.len().is_multiple_of(2) {
518 return None;
519 }
520
521 let mut coords = Vec::with_capacity(parts.len() / 2);
522
523 for chunk in parts.chunks(2) {
524 let lat = chunk[0].parse::<f64>().ok()?;
525 let lon = chunk[1].parse::<f64>().ok()?;
526
527 if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
529 return None;
530 }
531
532 coords.push((lat, lon));
533 }
534
535 Some(coords)
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_parse_point() {
544 let loc = parse_point("45.256 -71.92").unwrap();
545 assert_eq!(loc.geo_type, GeoType::Point);
546 assert_eq!(loc.coordinates.len(), 1);
547 assert_eq!(loc.coordinates[0], (45.256, -71.92));
548 }
549
550 #[test]
551 fn test_parse_point_invalid() {
552 assert!(parse_point("45.256").is_none());
553 assert!(parse_point("45.256 -71.92 extra").is_none());
554 assert!(parse_point("not numbers").is_none());
555 assert!(parse_point("").is_none());
556 }
557
558 #[test]
559 fn test_parse_line() {
560 let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
561 assert_eq!(loc.geo_type, GeoType::Line);
562 assert_eq!(loc.coordinates.len(), 2);
563 assert_eq!(loc.coordinates[0], (45.256, -71.92));
564 assert_eq!(loc.coordinates[1], (46.0, -72.0));
565 }
566
567 #[test]
568 fn test_parse_line_single_point() {
569 assert!(parse_line("45.256 -71.92").is_none());
571 }
572
573 #[test]
574 fn test_parse_polygon() {
575 let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
576 assert_eq!(loc.geo_type, GeoType::Polygon);
577 assert_eq!(loc.coordinates.len(), 4);
578 assert_eq!(loc.coordinates[0], (45.0, -71.0));
579 assert_eq!(loc.coordinates[3], (45.0, -71.0)); }
581
582 #[test]
583 fn test_parse_box() {
584 let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
585 assert_eq!(loc.geo_type, GeoType::Box);
586 assert_eq!(loc.coordinates.len(), 2);
587 assert_eq!(loc.coordinates[0], (45.0, -72.0)); assert_eq!(loc.coordinates[1], (46.0, -71.0)); }
590
591 #[test]
592 fn test_parse_box_invalid() {
593 assert!(parse_box("45.0 -72.0").is_none());
595 assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
596 }
597
598 #[test]
599 fn test_coordinate_validation() {
600 assert!(parse_point("91.0 0.0").is_none());
602 assert!(parse_point("-91.0 0.0").is_none());
604 assert!(parse_point("0.0 181.0").is_none());
606 assert!(parse_point("0.0 -181.0").is_none());
608 }
609
610 #[test]
611 fn test_handle_entry_element_point() {
612 let mut entry = Entry::default();
613 let limits = ParserLimits::default();
614
615 let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
616 assert!(handled);
617 assert!(entry.r#where.is_some());
618
619 let geo = entry.r#where.as_ref().unwrap();
620 assert_eq!(geo.geo_type, GeoType::Point);
621 assert_eq!(geo.coordinates[0], (45.256, -71.92));
622 }
623
624 #[test]
625 fn test_handle_entry_element_line() {
626 let mut entry = Entry::default();
627 let limits = ParserLimits::default();
628
629 let handled =
630 handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
631 assert!(handled);
632 assert!(entry.r#where.is_some());
633 assert_eq!(entry.r#where.as_ref().unwrap().geo_type, GeoType::Line);
634 }
635
636 #[test]
637 fn test_handle_entry_element_unknown() {
638 let mut entry = Entry::default();
639 let limits = ParserLimits::default();
640
641 let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
642 assert!(!handled);
643 assert!(entry.r#where.is_none());
644 }
645
646 #[test]
647 fn test_geo_location_constructors() {
648 let point = GeoLocation::point(45.0, -71.0);
649 assert_eq!(point.geo_type, GeoType::Point);
650 assert_eq!(point.coordinates.len(), 1);
651
652 let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
653 assert_eq!(line.geo_type, GeoType::Line);
654 assert_eq!(line.coordinates.len(), 2);
655
656 let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
657 assert_eq!(polygon.geo_type, GeoType::Polygon);
658 assert_eq!(polygon.coordinates.len(), 3);
659
660 let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
661 assert_eq!(bbox.geo_type, GeoType::Box);
662 assert_eq!(bbox.coordinates.len(), 2);
663 }
664
665 #[test]
666 fn test_whitespace_handling() {
667 let loc = parse_point(" 45.256 -71.92 ").unwrap();
668 assert_eq!(loc.coordinates[0], (45.256, -71.92));
669 }
670
671 #[test]
672 fn test_handle_feed_element_point() {
673 let mut feed = FeedMeta::default();
674 let limits = ParserLimits::default();
675
676 let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
677 assert!(handled);
678 assert!(feed.r#where.is_some());
679
680 let geo = feed.r#where.as_ref().unwrap();
681 assert_eq!(geo.geo_type, GeoType::Point);
682 assert_eq!(geo.coordinates[0], (45.256, -71.92));
683 }
684
685 #[test]
686 fn test_handle_feed_element_line() {
687 let mut feed = FeedMeta::default();
688 let limits = ParserLimits::default();
689
690 let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
691 assert!(handled);
692 assert!(feed.r#where.is_some());
693 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Line);
694 }
695
696 #[test]
697 fn test_handle_feed_element_polygon() {
698 let mut feed = FeedMeta::default();
699 let limits = ParserLimits::default();
700
701 let handled = handle_feed_element(
702 b"polygon",
703 "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0",
704 &mut feed,
705 &limits,
706 );
707 assert!(handled);
708 assert!(feed.r#where.is_some());
709 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Polygon);
710 }
711
712 #[test]
713 fn test_handle_feed_element_box() {
714 let mut feed = FeedMeta::default();
715 let limits = ParserLimits::default();
716
717 let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
718 assert!(handled);
719 assert!(feed.r#where.is_some());
720 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Box);
721 }
722
723 #[test]
724 fn test_handle_feed_element_unknown() {
725 let mut feed = FeedMeta::default();
726 let limits = ParserLimits::default();
727
728 let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
729 assert!(!handled);
730 assert!(feed.r#where.is_none());
731 }
732
733 #[test]
734 fn test_handle_feed_element_invalid_data() {
735 let mut feed = FeedMeta::default();
736 let limits = ParserLimits::default();
737
738 let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
739 assert!(handled);
740 assert!(feed.r#where.is_none());
741 }
742
743 #[test]
744 fn test_handle_entry_element_elev() {
745 let mut entry = Entry::default();
746 let limits = ParserLimits::default();
747
748 let handled = handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
749 assert!(handled);
750 let geo = entry.r#where.as_ref().unwrap();
751 assert_eq!(geo.elev, Some(1337.5));
752 }
753
754 #[test]
755 fn test_handle_entry_element_feature_name() {
756 let mut entry = Entry::default();
757 let limits = ParserLimits::default();
758
759 let handled = handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
760 assert!(handled);
761 let geo = entry.r#where.as_ref().unwrap();
762 assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
763 }
764
765 #[test]
766 fn test_handle_entry_element_feature_type_tag() {
767 let mut entry = Entry::default();
768 let limits = ParserLimits::default();
769
770 let handled = handle_entry_element(b"featuretypetag", "mountain", &mut entry, &limits);
771 assert!(handled);
772 let geo = entry.r#where.as_ref().unwrap();
773 assert_eq!(geo.feature_type_tag.as_deref(), Some("mountain"));
774 }
775
776 #[test]
777 fn test_handle_entry_element_relationship_tag() {
778 let mut entry = Entry::default();
779 let limits = ParserLimits::default();
780
781 let handled =
782 handle_entry_element(b"relationshiptag", "is-located-at", &mut entry, &limits);
783 assert!(handled);
784 let geo = entry.r#where.as_ref().unwrap();
785 assert_eq!(geo.relationship_tag.as_deref(), Some("is-located-at"));
786 }
787
788 #[test]
789 fn test_extended_attrs_without_geometry() {
790 let mut entry = Entry::default();
791 let limits = ParserLimits::default();
792
793 handle_entry_element(b"featurename", "Unknown Location", &mut entry, &limits);
794 let geo = entry.r#where.as_ref().unwrap();
795 assert_eq!(geo.feature_name.as_deref(), Some("Unknown Location"));
796 assert!(geo.coordinates.is_empty());
797 }
798
799 #[test]
800 fn test_extended_attrs_invalid_elev() {
801 let mut entry = Entry::default();
802 let limits = ParserLimits::default();
803
804 let handled = handle_entry_element(b"elev", "not-a-number", &mut entry, &limits);
805 assert!(handled);
806 assert!(entry.r#where.is_none());
808 }
809
810 #[test]
811 fn test_extended_attrs_elev_non_finite_ignored() {
812 let limits = ParserLimits::default();
813
814 for value in ["NaN", "Infinity", "-Infinity"] {
815 let mut entry = Entry::default();
816 let handled = handle_entry_element(b"elev", value, &mut entry, &limits);
817 assert!(handled, "element must be recognized for value {value}");
818 assert!(
819 entry.r#where.is_none(),
820 "non-finite elev '{value}' must not create GeoLocation"
821 );
822 }
823 }
824
825 #[test]
826 fn test_extended_attrs_before_geometry() {
827 let mut entry = Entry::default();
828 let limits = ParserLimits::default();
829
830 handle_entry_element(b"featurename", "Reverse Order", &mut entry, &limits);
831 handle_entry_element(b"elev", "500.0", &mut entry, &limits);
832 handle_entry_element(b"point", "40.0 -74.0", &mut entry, &limits);
833
834 let geo = entry.r#where.as_ref().unwrap();
835 assert_eq!(geo.geo_type, GeoType::Point);
836 assert_eq!(geo.coordinates[0], (40.0, -74.0));
837 assert_eq!(geo.feature_name.as_deref(), Some("Reverse Order"));
838 assert_eq!(geo.elev, Some(500.0));
839 }
840
841 #[test]
842 fn test_extended_attrs_after_geometry() {
843 let mut entry = Entry::default();
844 let limits = ParserLimits::default();
845
846 handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
847 handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
848 handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
849
850 let geo = entry.r#where.as_ref().unwrap();
851 assert_eq!(geo.geo_type, GeoType::Point);
852 assert_eq!(geo.coordinates[0], (45.256, -71.92));
853 assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
854 assert_eq!(geo.elev, Some(1337.5));
855 }
856}