1use crate::limits::ParserLimits;
27use crate::types::{Entry, FeedMeta};
28
29pub const GEORSS: &str = "http://www.georss.org/georss";
31
32pub const GML: &str = "http://www.opengis.net/gml";
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum GeoType {
38 #[default]
40 Point,
41 Line,
43 Polygon,
45 Box,
47}
48
49#[derive(Debug, Clone, Default, PartialEq)]
51pub struct GeoLocation {
52 pub geo_type: GeoType,
54 pub coordinates: Vec<(f64, f64)>,
61 pub srs_name: Option<String>,
65 pub elev: Option<f64>,
67 pub feature_type_tag: Option<String>,
69 pub feature_name: Option<String>,
71 pub relationship_tag: Option<String>,
73}
74
75impl GeoLocation {
76 #[must_use]
92 pub fn point(lat: f64, lon: f64) -> Self {
93 Self {
94 geo_type: GeoType::Point,
95 coordinates: vec![(lat, lon)],
96 ..Default::default()
97 }
98 }
99
100 #[must_use]
116 pub fn line(coords: Vec<(f64, f64)>) -> Self {
117 Self {
118 geo_type: GeoType::Line,
119 coordinates: coords,
120 ..Default::default()
121 }
122 }
123
124 #[must_use]
144 pub fn polygon(coords: Vec<(f64, f64)>) -> Self {
145 Self {
146 geo_type: GeoType::Polygon,
147 coordinates: coords,
148 ..Default::default()
149 }
150 }
151
152 #[must_use]
170 pub fn bbox(lower_lat: f64, lower_lon: f64, upper_lat: f64, upper_lon: f64) -> Self {
171 Self {
172 geo_type: GeoType::Box,
173 coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
174 ..Default::default()
175 }
176 }
177}
178
179pub fn handle_entry_geo_element(tag: &[u8], text: &str, entry: &mut Entry) -> bool {
194 match tag {
195 b"lat" => {
196 entry.geo_lat = Some(text.to_string());
197 try_build_entry_where(entry);
198 true
199 }
200 b"long" | b"lon" => {
201 entry.geo_long = Some(text.to_string());
202 try_build_entry_where(entry);
203 true
204 }
205 _ => false,
206 }
207}
208
209pub fn handle_feed_geo_element(tag: &[u8], text: &str, feed: &mut FeedMeta) -> bool {
224 match tag {
225 b"lat" => {
226 feed.geo_lat = Some(text.to_string());
227 try_build_feed_where(feed);
228 true
229 }
230 b"long" | b"lon" => {
231 feed.geo_long = Some(text.to_string());
232 try_build_feed_where(feed);
233 true
234 }
235 _ => false,
236 }
237}
238
239fn try_build_entry_where(entry: &mut Entry) {
240 if let (Some(lat_str), Some(lon_str)) = (entry.geo_lat.as_deref(), entry.geo_long.as_deref())
241 && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
242 && (-90.0..=90.0).contains(&lat)
243 && (-180.0..=180.0).contains(&lon)
244 {
245 entry.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
246 }
247}
248
249fn try_build_feed_where(feed: &mut FeedMeta) {
250 if let (Some(lat_str), Some(lon_str)) = (feed.geo_lat.as_deref(), feed.geo_long.as_deref())
251 && let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
252 && (-90.0..=90.0).contains(&lat)
253 && (-180.0..=180.0).contains(&lon)
254 {
255 feed.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
256 }
257}
258
259pub fn merge_geometry(target: &mut Option<Box<GeoLocation>>, loc: GeoLocation) {
270 let existing = target.get_or_insert_with(|| Box::new(GeoLocation::default()));
271 existing.geo_type = loc.geo_type;
272 existing.coordinates = loc.coordinates;
273 existing.srs_name = loc.srs_name;
274}
275
276pub fn handle_entry_element(
289 tag: &[u8],
290 text: &str,
291 entry: &mut Entry,
292 _limits: &ParserLimits,
293) -> bool {
294 match tag {
295 b"point" => {
296 if let Some(loc) = parse_point(text) {
297 merge_geometry(&mut entry.r#where, loc);
298 }
299 true
300 }
301 b"line" => {
302 if let Some(loc) = parse_line(text) {
303 merge_geometry(&mut entry.r#where, loc);
304 }
305 true
306 }
307 b"polygon" => {
308 if let Some(loc) = parse_polygon(text) {
309 merge_geometry(&mut entry.r#where, loc);
310 }
311 true
312 }
313 b"box" => {
314 if let Some(loc) = parse_box(text) {
315 merge_geometry(&mut entry.r#where, loc);
316 }
317 true
318 }
319 b"elev" => {
320 if let Ok(v) = text.trim().parse::<f64>()
321 && v.is_finite()
322 {
323 entry
324 .r#where
325 .get_or_insert_with(|| Box::new(GeoLocation::default()))
326 .elev = Some(v);
327 }
328 true
329 }
330 b"featuretypetag" => {
331 entry
332 .r#where
333 .get_or_insert_with(|| Box::new(GeoLocation::default()))
334 .feature_type_tag = Some(text.to_string());
335 true
336 }
337 b"featurename" => {
338 entry
339 .r#where
340 .get_or_insert_with(|| Box::new(GeoLocation::default()))
341 .feature_name = Some(text.to_string());
342 true
343 }
344 b"relationshiptag" => {
345 entry
346 .r#where
347 .get_or_insert_with(|| Box::new(GeoLocation::default()))
348 .relationship_tag = Some(text.to_string());
349 true
350 }
351 _ => false,
352 }
353}
354
355pub fn handle_feed_element(
368 tag: &[u8],
369 text: &str,
370 feed: &mut FeedMeta,
371 _limits: &ParserLimits,
372) -> bool {
373 match tag {
374 b"point" => {
375 if let Some(loc) = parse_point(text) {
376 merge_geometry(&mut feed.r#where, loc);
377 }
378 true
379 }
380 b"line" => {
381 if let Some(loc) = parse_line(text) {
382 merge_geometry(&mut feed.r#where, loc);
383 }
384 true
385 }
386 b"polygon" => {
387 if let Some(loc) = parse_polygon(text) {
388 merge_geometry(&mut feed.r#where, loc);
389 }
390 true
391 }
392 b"box" => {
393 if let Some(loc) = parse_box(text) {
394 merge_geometry(&mut feed.r#where, loc);
395 }
396 true
397 }
398 b"elev" => {
399 if let Ok(v) = text.trim().parse::<f64>()
400 && v.is_finite()
401 {
402 feed.r#where
403 .get_or_insert_with(|| Box::new(GeoLocation::default()))
404 .elev = Some(v);
405 }
406 true
407 }
408 b"featuretypetag" => {
409 feed.r#where
410 .get_or_insert_with(|| Box::new(GeoLocation::default()))
411 .feature_type_tag = Some(text.to_string());
412 true
413 }
414 b"featurename" => {
415 feed.r#where
416 .get_or_insert_with(|| Box::new(GeoLocation::default()))
417 .feature_name = Some(text.to_string());
418 true
419 }
420 b"relationshiptag" => {
421 feed.r#where
422 .get_or_insert_with(|| Box::new(GeoLocation::default()))
423 .relationship_tag = Some(text.to_string());
424 true
425 }
426 _ => false,
427 }
428}
429
430fn parse_point(text: &str) -> Option<GeoLocation> {
435 let coords = parse_coordinates(text)?;
436 if coords.len() == 1 {
437 Some(GeoLocation {
438 geo_type: GeoType::Point,
439 coordinates: coords,
440 ..Default::default()
441 })
442 } else {
443 None
444 }
445}
446
447fn parse_line(text: &str) -> Option<GeoLocation> {
452 let coords = parse_coordinates(text)?;
453 if coords.len() >= 2 {
454 Some(GeoLocation {
455 geo_type: GeoType::Line,
456 coordinates: coords,
457 ..Default::default()
458 })
459 } else {
460 None
461 }
462}
463
464fn parse_polygon(text: &str) -> Option<GeoLocation> {
469 let coords = parse_coordinates(text)?;
470 if coords.len() >= 3 {
471 Some(GeoLocation {
472 geo_type: GeoType::Polygon,
473 coordinates: coords,
474 ..Default::default()
475 })
476 } else {
477 None
478 }
479}
480
481fn parse_box(text: &str) -> Option<GeoLocation> {
486 let coords = parse_coordinates(text)?;
487 if coords.len() == 2 {
488 Some(GeoLocation {
489 geo_type: GeoType::Box,
490 coordinates: coords,
491 ..Default::default()
492 })
493 } else {
494 None
495 }
496}
497
498fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
502 parse_coordinates_ordered(text, true, 2).into_option()
503}
504
505enum CoordParse {
511 Ok(Vec<(f64, f64)>),
513 DimsMismatch,
515 Invalid,
517}
518
519impl CoordParse {
520 fn into_option(self) -> Option<Vec<(f64, f64)>> {
521 match self {
522 Self::Ok(coords) => Some(coords),
523 Self::DimsMismatch | Self::Invalid => None,
524 }
525 }
526}
527
528fn parse_coordinates_ordered(text: &str, lat_lon_order: bool, dims: usize) -> CoordParse {
545 let dims = if dims == 3 { 3 } else { 2 };
546 let normalized = text.replace(',', " ");
547 let parts: Vec<&str> = normalized.split_whitespace().collect();
548
549 if parts.is_empty() {
550 return CoordParse::Invalid;
551 }
552 if !parts.len().is_multiple_of(dims) {
553 return CoordParse::DimsMismatch;
554 }
555
556 let mut coords = Vec::with_capacity(parts.len() / dims);
557
558 for chunk in parts.chunks(dims) {
559 let Ok(a) = chunk[0].parse::<f64>() else {
560 return CoordParse::Invalid;
561 };
562 let Ok(b) = chunk[1].parse::<f64>() else {
563 return CoordParse::Invalid;
564 };
565 let (lat, lon) = if lat_lon_order { (a, b) } else { (b, a) };
569
570 if lat_lon_order {
571 if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
572 return CoordParse::Invalid;
573 }
574 } else if !lat.is_finite() || !lon.is_finite() {
575 return CoordParse::Invalid;
576 }
577
578 coords.push((lat, lon));
579 }
580
581 CoordParse::Ok(coords)
582}
583
584const GEOGRAPHIC_EPSG_CODES: &[u32] = &[
590 3819, 3821, 3824, 3889, 3906, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011,
591 4012, 4013, 4014, 4015, 4016, 4018, 4019, 4020, 4021, 4022, 4023, 4024, 4025, 4027, 4028, 4029,
592 4030, 4031, 4032, 4033, 4034, 4035, 4036, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4052, 4053,
593 4054, 4055, 4075, 4081, 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131,
594 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 4147,
595 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4160, 4161, 4162, 4163,
596 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180,
597 4181, 4182, 4183, 4184, 4185, 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, 4198,
598 4199, 4200, 4201, 4202, 4203, 4204, 4205, 4206, 4207, 4208, 4209, 4210, 4211, 4212, 4213, 4214,
599 4215, 4216, 4218, 4219, 4220, 4221, 4222, 4223, 4224, 4225, 4226, 4227, 4228, 4229, 4230, 4231,
600 4232, 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240, 4241, 4242, 4243, 4244, 4245, 4246, 4247,
601 4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263,
602 4264, 4265, 4266, 4267, 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279,
603 4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293, 4294, 4295, 4296,
604 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, 4307, 4308, 4309, 4310, 4311, 4312, 4313,
605 4314, 4315, 4316, 4317, 4318, 4319, 4322, 4324, 4326, 4463, 4470, 4475, 4483, 4490, 4555, 4558,
606 4600, 4601, 4602, 4603, 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615,
607 4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4626, 4627, 4628, 4629, 4630, 4631,
608 4632, 4633, 4634, 4635, 4636, 4637, 4638, 4639, 4640, 4641, 4642, 4643, 4644, 4645, 4646, 4657,
609 4658, 4659, 4660, 4661, 4662, 4663, 4664, 4665, 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673,
610 4674, 4675, 4676, 4677, 4678, 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, 4689,
611 4690, 4691, 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, 4702, 4703, 4704, 4705,
612 4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, 4714, 4715, 4716, 4717, 4718, 4719, 4720, 4721,
613 4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729, 4730, 4731, 4732, 4733, 4734, 4735, 4736, 4737,
614 4738, 4739, 4740, 4741, 4742, 4743, 4744, 4745, 4746, 4747, 4748, 4749, 4750, 4751, 4752, 4753,
615 4754, 4755, 4756, 4757, 4758, 4759, 4760, 4761, 4762, 4763, 4764, 4765, 4801, 4802, 4803, 4804,
616 4805, 4806, 4807, 4808, 4809, 4810, 4811, 4813, 4814, 4815, 4816, 4817, 4818, 4819, 4820, 4821,
617 4823, 4824, 4901, 4902, 4903, 4904, 4979,
618];
619
620fn is_geographic_epsg(code: u32) -> bool {
622 GEOGRAPHIC_EPSG_CODES.binary_search(&code).is_ok()
623}
624
625fn extract_epsg_code(srs_name: &str) -> Option<u32> {
634 let trimmed = srs_name.trim();
635 if !trimmed.to_ascii_uppercase().contains("EPSG") {
636 return None;
637 }
638 trimmed
639 .rsplit([':', '/', '#'])
640 .map(str::trim)
641 .find(|segment| !segment.is_empty())
642 .and_then(|segment| segment.parse().ok())
643}
644
645fn srs_uses_lat_lon_order(srs_name: Option<&str>) -> bool {
659 match srs_name {
660 None => true,
661 Some(name) if name.to_ascii_uppercase().contains("CRS84") => false,
662 Some(name) => extract_epsg_code(name).is_none_or(is_geographic_epsg),
663 }
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
676pub struct GmlDimsMismatch;
677
678pub fn build_gml_geometry(
736 geo_type: GeoType,
737 srs_name: Option<String>,
738 text: &str,
739 dims: usize,
740) -> Result<Option<GeoLocation>, GmlDimsMismatch> {
741 let min_points = match geo_type {
742 GeoType::Point => 1,
743 GeoType::Line => 2,
744 GeoType::Polygon => 3,
745 GeoType::Box => return Ok(None),
746 };
747
748 let lat_lon_order = srs_uses_lat_lon_order(srs_name.as_deref());
749 let coords = match parse_coordinates_ordered(text, lat_lon_order, dims) {
750 CoordParse::Ok(coords) => coords,
751 CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
752 CoordParse::Invalid => return Ok(None),
753 };
754 if coords.len() < min_points || (geo_type == GeoType::Point && coords.len() != 1) {
755 return Ok(None);
756 }
757
758 Ok(Some(GeoLocation {
759 geo_type,
760 coordinates: coords,
761 srs_name,
762 ..Default::default()
763 }))
764}
765
766pub fn build_gml_envelope(
801 srs_name: Option<String>,
802 lower_text: &str,
803 upper_text: &str,
804 dims: usize,
805) -> Result<Option<GeoLocation>, GmlDimsMismatch> {
806 let lat_lon_order = srs_uses_lat_lon_order(srs_name.as_deref());
807 let lower = match parse_coordinates_ordered(lower_text, lat_lon_order, dims) {
808 CoordParse::Ok(coords) => coords,
809 CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
810 CoordParse::Invalid => return Ok(None),
811 };
812 let upper = match parse_coordinates_ordered(upper_text, lat_lon_order, dims) {
813 CoordParse::Ok(coords) => coords,
814 CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
815 CoordParse::Invalid => return Ok(None),
816 };
817
818 if lower.len() != 1 || upper.len() != 1 {
819 return Ok(None);
820 }
821
822 Ok(Some(GeoLocation {
823 geo_type: GeoType::Box,
824 coordinates: vec![lower[0], upper[0]],
825 srs_name,
826 ..Default::default()
827 }))
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn test_parse_point() {
836 let loc = parse_point("45.256 -71.92").unwrap();
837 assert_eq!(loc.geo_type, GeoType::Point);
838 assert_eq!(loc.coordinates.len(), 1);
839 assert_eq!(loc.coordinates[0], (45.256, -71.92));
840 }
841
842 #[test]
843 fn test_parse_point_invalid() {
844 assert!(parse_point("45.256").is_none());
845 assert!(parse_point("45.256 -71.92 extra").is_none());
846 assert!(parse_point("not numbers").is_none());
847 assert!(parse_point("").is_none());
848 }
849
850 #[test]
851 fn test_parse_line() {
852 let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
853 assert_eq!(loc.geo_type, GeoType::Line);
854 assert_eq!(loc.coordinates.len(), 2);
855 assert_eq!(loc.coordinates[0], (45.256, -71.92));
856 assert_eq!(loc.coordinates[1], (46.0, -72.0));
857 }
858
859 #[test]
860 fn test_parse_line_single_point() {
861 assert!(parse_line("45.256 -71.92").is_none());
863 }
864
865 #[test]
866 fn test_parse_polygon() {
867 let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
868 assert_eq!(loc.geo_type, GeoType::Polygon);
869 assert_eq!(loc.coordinates.len(), 4);
870 assert_eq!(loc.coordinates[0], (45.0, -71.0));
871 assert_eq!(loc.coordinates[3], (45.0, -71.0)); }
873
874 #[test]
875 fn test_parse_box() {
876 let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
877 assert_eq!(loc.geo_type, GeoType::Box);
878 assert_eq!(loc.coordinates.len(), 2);
879 assert_eq!(loc.coordinates[0], (45.0, -72.0)); assert_eq!(loc.coordinates[1], (46.0, -71.0)); }
882
883 #[test]
884 fn test_parse_box_invalid() {
885 assert!(parse_box("45.0 -72.0").is_none());
887 assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
888 }
889
890 #[test]
891 fn test_coordinate_validation() {
892 assert!(parse_point("91.0 0.0").is_none());
894 assert!(parse_point("-91.0 0.0").is_none());
896 assert!(parse_point("0.0 181.0").is_none());
898 assert!(parse_point("0.0 -181.0").is_none());
900 }
901
902 #[test]
903 fn test_handle_entry_element_point() {
904 let mut entry = Entry::default();
905 let limits = ParserLimits::default();
906
907 let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
908 assert!(handled);
909 assert!(entry.r#where.is_some());
910
911 let geo = entry.r#where.as_ref().unwrap();
912 assert_eq!(geo.geo_type, GeoType::Point);
913 assert_eq!(geo.coordinates[0], (45.256, -71.92));
914 }
915
916 #[test]
917 fn test_handle_entry_element_line() {
918 let mut entry = Entry::default();
919 let limits = ParserLimits::default();
920
921 let handled =
922 handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
923 assert!(handled);
924 assert!(entry.r#where.is_some());
925 assert_eq!(entry.r#where.as_ref().unwrap().geo_type, GeoType::Line);
926 }
927
928 #[test]
929 fn test_handle_entry_element_unknown() {
930 let mut entry = Entry::default();
931 let limits = ParserLimits::default();
932
933 let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
934 assert!(!handled);
935 assert!(entry.r#where.is_none());
936 }
937
938 #[test]
939 fn test_geo_location_constructors() {
940 let point = GeoLocation::point(45.0, -71.0);
941 assert_eq!(point.geo_type, GeoType::Point);
942 assert_eq!(point.coordinates.len(), 1);
943
944 let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
945 assert_eq!(line.geo_type, GeoType::Line);
946 assert_eq!(line.coordinates.len(), 2);
947
948 let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
949 assert_eq!(polygon.geo_type, GeoType::Polygon);
950 assert_eq!(polygon.coordinates.len(), 3);
951
952 let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
953 assert_eq!(bbox.geo_type, GeoType::Box);
954 assert_eq!(bbox.coordinates.len(), 2);
955 }
956
957 #[test]
958 fn test_whitespace_handling() {
959 let loc = parse_point(" 45.256 -71.92 ").unwrap();
960 assert_eq!(loc.coordinates[0], (45.256, -71.92));
961 }
962
963 #[test]
964 fn test_handle_feed_element_point() {
965 let mut feed = FeedMeta::default();
966 let limits = ParserLimits::default();
967
968 let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
969 assert!(handled);
970 assert!(feed.r#where.is_some());
971
972 let geo = feed.r#where.as_ref().unwrap();
973 assert_eq!(geo.geo_type, GeoType::Point);
974 assert_eq!(geo.coordinates[0], (45.256, -71.92));
975 }
976
977 #[test]
978 fn test_handle_feed_element_line() {
979 let mut feed = FeedMeta::default();
980 let limits = ParserLimits::default();
981
982 let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
983 assert!(handled);
984 assert!(feed.r#where.is_some());
985 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Line);
986 }
987
988 #[test]
989 fn test_handle_feed_element_polygon() {
990 let mut feed = FeedMeta::default();
991 let limits = ParserLimits::default();
992
993 let handled = handle_feed_element(
994 b"polygon",
995 "45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0",
996 &mut feed,
997 &limits,
998 );
999 assert!(handled);
1000 assert!(feed.r#where.is_some());
1001 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Polygon);
1002 }
1003
1004 #[test]
1005 fn test_handle_feed_element_box() {
1006 let mut feed = FeedMeta::default();
1007 let limits = ParserLimits::default();
1008
1009 let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
1010 assert!(handled);
1011 assert!(feed.r#where.is_some());
1012 assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Box);
1013 }
1014
1015 #[test]
1016 fn test_handle_feed_element_unknown() {
1017 let mut feed = FeedMeta::default();
1018 let limits = ParserLimits::default();
1019
1020 let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
1021 assert!(!handled);
1022 assert!(feed.r#where.is_none());
1023 }
1024
1025 #[test]
1026 fn test_handle_feed_element_invalid_data() {
1027 let mut feed = FeedMeta::default();
1028 let limits = ParserLimits::default();
1029
1030 let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
1031 assert!(handled);
1032 assert!(feed.r#where.is_none());
1033 }
1034
1035 #[test]
1036 fn test_handle_entry_element_elev() {
1037 let mut entry = Entry::default();
1038 let limits = ParserLimits::default();
1039
1040 let handled = handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
1041 assert!(handled);
1042 let geo = entry.r#where.as_ref().unwrap();
1043 assert_eq!(geo.elev, Some(1337.5));
1044 }
1045
1046 #[test]
1047 fn test_handle_entry_element_feature_name() {
1048 let mut entry = Entry::default();
1049 let limits = ParserLimits::default();
1050
1051 let handled = handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
1052 assert!(handled);
1053 let geo = entry.r#where.as_ref().unwrap();
1054 assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
1055 }
1056
1057 #[test]
1058 fn test_handle_entry_element_feature_type_tag() {
1059 let mut entry = Entry::default();
1060 let limits = ParserLimits::default();
1061
1062 let handled = handle_entry_element(b"featuretypetag", "mountain", &mut entry, &limits);
1063 assert!(handled);
1064 let geo = entry.r#where.as_ref().unwrap();
1065 assert_eq!(geo.feature_type_tag.as_deref(), Some("mountain"));
1066 }
1067
1068 #[test]
1069 fn test_handle_entry_element_relationship_tag() {
1070 let mut entry = Entry::default();
1071 let limits = ParserLimits::default();
1072
1073 let handled =
1074 handle_entry_element(b"relationshiptag", "is-located-at", &mut entry, &limits);
1075 assert!(handled);
1076 let geo = entry.r#where.as_ref().unwrap();
1077 assert_eq!(geo.relationship_tag.as_deref(), Some("is-located-at"));
1078 }
1079
1080 #[test]
1081 fn test_extended_attrs_without_geometry() {
1082 let mut entry = Entry::default();
1083 let limits = ParserLimits::default();
1084
1085 handle_entry_element(b"featurename", "Unknown Location", &mut entry, &limits);
1086 let geo = entry.r#where.as_ref().unwrap();
1087 assert_eq!(geo.feature_name.as_deref(), Some("Unknown Location"));
1088 assert!(geo.coordinates.is_empty());
1089 }
1090
1091 #[test]
1092 fn test_extended_attrs_invalid_elev() {
1093 let mut entry = Entry::default();
1094 let limits = ParserLimits::default();
1095
1096 let handled = handle_entry_element(b"elev", "not-a-number", &mut entry, &limits);
1097 assert!(handled);
1098 assert!(entry.r#where.is_none());
1100 }
1101
1102 #[test]
1103 fn test_extended_attrs_elev_non_finite_ignored() {
1104 let limits = ParserLimits::default();
1105
1106 for value in ["NaN", "Infinity", "-Infinity"] {
1107 let mut entry = Entry::default();
1108 let handled = handle_entry_element(b"elev", value, &mut entry, &limits);
1109 assert!(handled, "element must be recognized for value {value}");
1110 assert!(
1111 entry.r#where.is_none(),
1112 "non-finite elev '{value}' must not create GeoLocation"
1113 );
1114 }
1115 }
1116
1117 #[test]
1118 fn test_extended_attrs_before_geometry() {
1119 let mut entry = Entry::default();
1120 let limits = ParserLimits::default();
1121
1122 handle_entry_element(b"featurename", "Reverse Order", &mut entry, &limits);
1123 handle_entry_element(b"elev", "500.0", &mut entry, &limits);
1124 handle_entry_element(b"point", "40.0 -74.0", &mut entry, &limits);
1125
1126 let geo = entry.r#where.as_ref().unwrap();
1127 assert_eq!(geo.geo_type, GeoType::Point);
1128 assert_eq!(geo.coordinates[0], (40.0, -74.0));
1129 assert_eq!(geo.feature_name.as_deref(), Some("Reverse Order"));
1130 assert_eq!(geo.elev, Some(500.0));
1131 }
1132
1133 #[test]
1134 fn test_extended_attrs_after_geometry() {
1135 let mut entry = Entry::default();
1136 let limits = ParserLimits::default();
1137
1138 handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
1139 handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
1140 handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
1141
1142 let geo = entry.r#where.as_ref().unwrap();
1143 assert_eq!(geo.geo_type, GeoType::Point);
1144 assert_eq!(geo.coordinates[0], (45.256, -71.92));
1145 assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
1146 assert_eq!(geo.elev, Some(1337.5));
1147 }
1148
1149 #[test]
1150 fn test_extract_epsg_code() {
1151 assert_eq!(extract_epsg_code("EPSG:4326"), Some(4326));
1152 assert_eq!(extract_epsg_code("urn:ogc:def:crs:EPSG::4326"), Some(4326));
1153 assert_eq!(
1154 extract_epsg_code("http://www.opengis.net/def/crs/EPSG/0/4326"),
1155 Some(4326)
1156 );
1157 assert_eq!(
1159 extract_epsg_code("http://www.opengis.net/gml/srs/epsg.xml#3857"),
1160 Some(3857)
1161 );
1162 assert_eq!(extract_epsg_code(" EPSG:3857 "), Some(3857));
1164 assert_eq!(extract_epsg_code("http://www.opengis.net/gml"), None);
1165 assert_eq!(extract_epsg_code("not-a-crs"), None);
1166 }
1167
1168 #[test]
1169 fn test_srs_uses_lat_lon_order() {
1170 assert!(srs_uses_lat_lon_order(None));
1171 assert!(srs_uses_lat_lon_order(Some("EPSG:4326")));
1172 assert!(srs_uses_lat_lon_order(Some("urn:ogc:def:crs:EPSG::4326")));
1173 assert!(!srs_uses_lat_lon_order(Some("EPSG:3857")));
1174 assert!(srs_uses_lat_lon_order(Some("some-custom-crs")));
1175 assert!(!srs_uses_lat_lon_order(Some(
1177 "urn:ogc:def:crs:OGC:1.3:CRS84"
1178 )));
1179 assert!(!srs_uses_lat_lon_order(Some("OGC:CRS84")));
1180 }
1181
1182 #[test]
1183 fn test_build_gml_geometry_point_epsg4326() {
1184 let loc = build_gml_geometry(
1185 GeoType::Point,
1186 Some("EPSG:4326".to_string()),
1187 "45.256 -71.92",
1188 2,
1189 );
1190 let loc = loc.unwrap().unwrap();
1191 assert_eq!(loc.geo_type, GeoType::Point);
1192 assert_eq!(loc.coordinates, vec![(45.256, -71.92)]);
1193 assert_eq!(loc.srs_name.as_deref(), Some("EPSG:4326"));
1194 }
1195
1196 #[test]
1197 fn test_build_gml_geometry_point_no_srs_name_defaults_lat_lon() {
1198 let loc = build_gml_geometry(GeoType::Point, None, "45.256 -71.92", 2);
1199 let loc = loc.unwrap().unwrap();
1200 assert_eq!(loc.coordinates, vec![(45.256, -71.92)]);
1201 assert_eq!(loc.srs_name, None);
1202 }
1203
1204 #[test]
1205 fn test_build_gml_geometry_swaps_projected_crs_realistic_meters() {
1206 let loc = build_gml_geometry(
1210 GeoType::Point,
1211 Some("EPSG:3857".to_string()),
1212 "-8004866.0 5675670.0",
1213 2,
1214 );
1215 assert_eq!(
1216 loc.unwrap().unwrap().coordinates,
1217 vec![(5_675_670.0, -8_004_866.0)]
1218 );
1219 }
1220
1221 #[test]
1222 fn test_build_gml_geometry_projected_crs_rejects_non_finite() {
1223 let result =
1224 build_gml_geometry(GeoType::Point, Some("EPSG:3857".to_string()), "NaN NaN", 2);
1225 assert_eq!(result, Ok(None));
1226 }
1227
1228 #[test]
1229 fn test_build_gml_geometry_linestring() {
1230 let loc = build_gml_geometry(
1231 GeoType::Line,
1232 Some("urn:ogc:def:crs:EPSG::4326".to_string()),
1233 "45.256 -71.92 46.0 -72.0",
1234 2,
1235 );
1236 let loc = loc.unwrap().unwrap();
1237 assert_eq!(loc.geo_type, GeoType::Line);
1238 assert_eq!(loc.coordinates.len(), 2);
1239 }
1240
1241 #[test]
1242 fn test_build_gml_geometry_srs_dimension_3_drops_elevation() {
1243 let loc = build_gml_geometry(GeoType::Line, None, "45.0 -71.0 10.0 46.0 -72.0 20.0", 3);
1246 assert_eq!(
1247 loc.unwrap().unwrap().coordinates,
1248 vec![(45.0, -71.0), (46.0, -72.0)]
1249 );
1250 }
1251
1252 #[test]
1253 fn test_build_gml_geometry_srs_dimension_mismatch_sets_bozo() {
1254 let result = build_gml_geometry(GeoType::Point, None, "45.0 -71.0 10.0 46.0 -72.0", 3);
1258 assert_eq!(result, Err(GmlDimsMismatch));
1259 }
1260
1261 #[test]
1262 fn test_build_gml_geometry_comma_separated_coordinates() {
1263 let loc = build_gml_geometry(GeoType::Point, None, "45.256,-71.92", 2);
1264 assert_eq!(loc.unwrap().unwrap().coordinates, vec![(45.256, -71.92)]);
1265 }
1266
1267 #[test]
1268 fn test_build_gml_geometry_polygon_too_few_points() {
1269 let result = build_gml_geometry(GeoType::Polygon, None, "45.0 -71.0 46.0 -71.0", 2);
1270 assert_eq!(result, Ok(None));
1271 }
1272
1273 #[test]
1274 fn test_build_gml_geometry_box_unsupported() {
1275 let result = build_gml_geometry(GeoType::Box, None, "45.0 -71.0 46.0 -71.0", 2);
1276 assert_eq!(result, Ok(None));
1277 }
1278
1279 #[test]
1280 fn test_build_gml_geometry_malformed_text() {
1281 assert_eq!(
1282 build_gml_geometry(GeoType::Point, None, "not numbers", 2),
1283 Ok(None)
1284 );
1285 assert_eq!(build_gml_geometry(GeoType::Point, None, "", 2), Ok(None));
1286 }
1287
1288 #[test]
1289 fn test_build_gml_envelope() {
1290 let loc = build_gml_envelope(None, "42.9 -71.9", "43.1 -71.5", 2);
1291 let loc = loc.unwrap().unwrap();
1292 assert_eq!(loc.geo_type, GeoType::Box);
1293 assert_eq!(loc.coordinates, vec![(42.9, -71.9), (43.1, -71.5)]);
1294 }
1295
1296 #[test]
1297 fn test_build_gml_envelope_swaps_projected_crs() {
1298 let loc = build_gml_envelope(
1299 Some("EPSG:3857".to_string()),
1300 "-8004866.0 5675670.0",
1301 "-8000000.0 5680000.0",
1302 2,
1303 );
1304 assert_eq!(
1305 loc.unwrap().unwrap().coordinates,
1306 vec![(5_675_670.0, -8_004_866.0), (5_680_000.0, -8_000_000.0)]
1307 );
1308 }
1309
1310 #[test]
1311 fn test_build_gml_envelope_srs_dimension_3_drops_elevation() {
1312 let loc = build_gml_envelope(None, "42.9 -71.9 10.0", "43.1 -71.5 20.0", 3);
1313 assert_eq!(
1314 loc.unwrap().unwrap().coordinates,
1315 vec![(42.9, -71.9), (43.1, -71.5)]
1316 );
1317 }
1318
1319 #[test]
1320 fn test_build_gml_envelope_malformed_corner() {
1321 assert_eq!(
1322 build_gml_envelope(None, "not numbers", "43.1 -71.5", 2),
1323 Ok(None)
1324 );
1325 assert_eq!(build_gml_envelope(None, "42.9 -71.9", "", 2), Ok(None));
1326 }
1327
1328 #[test]
1329 fn test_build_gml_envelope_corner_wrong_arity() {
1330 let result = build_gml_envelope(None, "42.9 -71.9 43.1 -71.5", "43.1 -71.5", 2);
1332 assert_eq!(result, Ok(None));
1333 }
1334
1335 #[test]
1336 fn test_build_gml_envelope_dims_mismatch_sets_bozo() {
1337 let result = build_gml_envelope(None, "42.9 -71.9", "43.1 -71.5 20.0", 3);
1339 assert_eq!(result, Err(GmlDimsMismatch));
1340 }
1341}