use crate::limits::ParserLimits;
use crate::types::{Entry, FeedMeta};
pub const GEORSS: &str = "http://www.georss.org/georss";
pub const GML: &str = "http://www.opengis.net/gml";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GeoType {
#[default]
Point,
Line,
Polygon,
Box,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GeoLocation {
pub geo_type: GeoType,
pub coordinates: Vec<(f64, f64)>,
pub srs_name: Option<String>,
pub elev: Option<f64>,
pub feature_type_tag: Option<String>,
pub feature_name: Option<String>,
pub relationship_tag: Option<String>,
}
impl GeoLocation {
#[must_use]
pub fn point(lat: f64, lon: f64) -> Self {
Self {
geo_type: GeoType::Point,
coordinates: vec![(lat, lon)],
..Default::default()
}
}
#[must_use]
pub fn line(coords: Vec<(f64, f64)>) -> Self {
Self {
geo_type: GeoType::Line,
coordinates: coords,
..Default::default()
}
}
#[must_use]
pub fn polygon(coords: Vec<(f64, f64)>) -> Self {
Self {
geo_type: GeoType::Polygon,
coordinates: coords,
..Default::default()
}
}
#[must_use]
pub fn bbox(lower_lat: f64, lower_lon: f64, upper_lat: f64, upper_lon: f64) -> Self {
Self {
geo_type: GeoType::Box,
coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
..Default::default()
}
}
}
pub fn handle_entry_geo_element(tag: &[u8], text: &str, entry: &mut Entry) -> bool {
match tag {
b"lat" => {
entry.geo_lat = Some(text.to_string());
try_build_entry_where(entry);
true
}
b"long" | b"lon" => {
entry.geo_long = Some(text.to_string());
try_build_entry_where(entry);
true
}
_ => false,
}
}
pub fn handle_feed_geo_element(tag: &[u8], text: &str, feed: &mut FeedMeta) -> bool {
match tag {
b"lat" => {
feed.geo_lat = Some(text.to_string());
try_build_feed_where(feed);
true
}
b"long" | b"lon" => {
feed.geo_long = Some(text.to_string());
try_build_feed_where(feed);
true
}
_ => false,
}
}
fn try_build_entry_where(entry: &mut Entry) {
if let (Some(lat_str), Some(lon_str)) = (entry.geo_lat.as_deref(), entry.geo_long.as_deref())
&& let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
&& (-90.0..=90.0).contains(&lat)
&& (-180.0..=180.0).contains(&lon)
{
entry.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
}
}
fn try_build_feed_where(feed: &mut FeedMeta) {
if let (Some(lat_str), Some(lon_str)) = (feed.geo_lat.as_deref(), feed.geo_long.as_deref())
&& let (Ok(lat), Ok(lon)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>())
&& (-90.0..=90.0).contains(&lat)
&& (-180.0..=180.0).contains(&lon)
{
feed.r#where = Some(Box::new(GeoLocation::point(lat, lon)));
}
}
pub fn merge_geometry(target: &mut Option<Box<GeoLocation>>, loc: GeoLocation) {
let existing = target.get_or_insert_with(|| Box::new(GeoLocation::default()));
existing.geo_type = loc.geo_type;
existing.coordinates = loc.coordinates;
existing.srs_name = loc.srs_name;
}
pub fn handle_entry_element(
tag: &[u8],
text: &str,
entry: &mut Entry,
_limits: &ParserLimits,
) -> bool {
match tag {
b"point" => {
if let Some(loc) = parse_point(text) {
merge_geometry(&mut entry.r#where, loc);
}
true
}
b"line" => {
if let Some(loc) = parse_line(text) {
merge_geometry(&mut entry.r#where, loc);
}
true
}
b"polygon" => {
if let Some(loc) = parse_polygon(text) {
merge_geometry(&mut entry.r#where, loc);
}
true
}
b"box" => {
if let Some(loc) = parse_box(text) {
merge_geometry(&mut entry.r#where, loc);
}
true
}
b"elev" => {
if let Ok(v) = text.trim().parse::<f64>()
&& v.is_finite()
{
entry
.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.elev = Some(v);
}
true
}
b"featuretypetag" => {
entry
.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.feature_type_tag = Some(text.to_string());
true
}
b"featurename" => {
entry
.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.feature_name = Some(text.to_string());
true
}
b"relationshiptag" => {
entry
.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.relationship_tag = Some(text.to_string());
true
}
_ => false,
}
}
pub fn handle_feed_element(
tag: &[u8],
text: &str,
feed: &mut FeedMeta,
_limits: &ParserLimits,
) -> bool {
match tag {
b"point" => {
if let Some(loc) = parse_point(text) {
merge_geometry(&mut feed.r#where, loc);
}
true
}
b"line" => {
if let Some(loc) = parse_line(text) {
merge_geometry(&mut feed.r#where, loc);
}
true
}
b"polygon" => {
if let Some(loc) = parse_polygon(text) {
merge_geometry(&mut feed.r#where, loc);
}
true
}
b"box" => {
if let Some(loc) = parse_box(text) {
merge_geometry(&mut feed.r#where, loc);
}
true
}
b"elev" => {
if let Ok(v) = text.trim().parse::<f64>()
&& v.is_finite()
{
feed.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.elev = Some(v);
}
true
}
b"featuretypetag" => {
feed.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.feature_type_tag = Some(text.to_string());
true
}
b"featurename" => {
feed.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.feature_name = Some(text.to_string());
true
}
b"relationshiptag" => {
feed.r#where
.get_or_insert_with(|| Box::new(GeoLocation::default()))
.relationship_tag = Some(text.to_string());
true
}
_ => false,
}
}
fn parse_point(text: &str) -> Option<GeoLocation> {
let coords = parse_coordinates(text)?;
if coords.len() == 1 {
Some(GeoLocation {
geo_type: GeoType::Point,
coordinates: coords,
..Default::default()
})
} else {
None
}
}
fn parse_line(text: &str) -> Option<GeoLocation> {
let coords = parse_coordinates(text)?;
if coords.len() >= 2 {
Some(GeoLocation {
geo_type: GeoType::Line,
coordinates: coords,
..Default::default()
})
} else {
None
}
}
fn parse_polygon(text: &str) -> Option<GeoLocation> {
let coords = parse_coordinates(text)?;
if coords.len() >= 3 {
Some(GeoLocation {
geo_type: GeoType::Polygon,
coordinates: coords,
..Default::default()
})
} else {
None
}
}
fn parse_box(text: &str) -> Option<GeoLocation> {
let coords = parse_coordinates(text)?;
if coords.len() == 2 {
Some(GeoLocation {
geo_type: GeoType::Box,
coordinates: coords,
..Default::default()
})
} else {
None
}
}
fn parse_coordinates(text: &str) -> Option<Vec<(f64, f64)>> {
parse_coordinates_ordered(text, true, 2).into_option()
}
enum CoordParse {
Ok(Vec<(f64, f64)>),
DimsMismatch,
Invalid,
}
impl CoordParse {
fn into_option(self) -> Option<Vec<(f64, f64)>> {
match self {
Self::Ok(coords) => Some(coords),
Self::DimsMismatch | Self::Invalid => None,
}
}
}
fn parse_coordinates_ordered(text: &str, lat_lon_order: bool, dims: usize) -> CoordParse {
let dims = if dims == 3 { 3 } else { 2 };
let normalized = text.replace(',', " ");
let parts: Vec<&str> = normalized.split_whitespace().collect();
if parts.is_empty() {
return CoordParse::Invalid;
}
if !parts.len().is_multiple_of(dims) {
return CoordParse::DimsMismatch;
}
let mut coords = Vec::with_capacity(parts.len() / dims);
for chunk in parts.chunks(dims) {
let Ok(a) = chunk[0].parse::<f64>() else {
return CoordParse::Invalid;
};
let Ok(b) = chunk[1].parse::<f64>() else {
return CoordParse::Invalid;
};
let (lat, lon) = if lat_lon_order { (a, b) } else { (b, a) };
if lat_lon_order {
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
return CoordParse::Invalid;
}
} else if !lat.is_finite() || !lon.is_finite() {
return CoordParse::Invalid;
}
coords.push((lat, lon));
}
CoordParse::Ok(coords)
}
const GEOGRAPHIC_EPSG_CODES: &[u32] = &[
3819, 3821, 3824, 3889, 3906, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011,
4012, 4013, 4014, 4015, 4016, 4018, 4019, 4020, 4021, 4022, 4023, 4024, 4025, 4027, 4028, 4029,
4030, 4031, 4032, 4033, 4034, 4035, 4036, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4052, 4053,
4054, 4055, 4075, 4081, 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131,
4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 4147,
4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4160, 4161, 4162, 4163,
4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180,
4181, 4182, 4183, 4184, 4185, 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, 4198,
4199, 4200, 4201, 4202, 4203, 4204, 4205, 4206, 4207, 4208, 4209, 4210, 4211, 4212, 4213, 4214,
4215, 4216, 4218, 4219, 4220, 4221, 4222, 4223, 4224, 4225, 4226, 4227, 4228, 4229, 4230, 4231,
4232, 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240, 4241, 4242, 4243, 4244, 4245, 4246, 4247,
4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263,
4264, 4265, 4266, 4267, 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279,
4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293, 4294, 4295, 4296,
4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, 4307, 4308, 4309, 4310, 4311, 4312, 4313,
4314, 4315, 4316, 4317, 4318, 4319, 4322, 4324, 4326, 4463, 4470, 4475, 4483, 4490, 4555, 4558,
4600, 4601, 4602, 4603, 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615,
4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4626, 4627, 4628, 4629, 4630, 4631,
4632, 4633, 4634, 4635, 4636, 4637, 4638, 4639, 4640, 4641, 4642, 4643, 4644, 4645, 4646, 4657,
4658, 4659, 4660, 4661, 4662, 4663, 4664, 4665, 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673,
4674, 4675, 4676, 4677, 4678, 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, 4689,
4690, 4691, 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, 4702, 4703, 4704, 4705,
4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, 4714, 4715, 4716, 4717, 4718, 4719, 4720, 4721,
4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729, 4730, 4731, 4732, 4733, 4734, 4735, 4736, 4737,
4738, 4739, 4740, 4741, 4742, 4743, 4744, 4745, 4746, 4747, 4748, 4749, 4750, 4751, 4752, 4753,
4754, 4755, 4756, 4757, 4758, 4759, 4760, 4761, 4762, 4763, 4764, 4765, 4801, 4802, 4803, 4804,
4805, 4806, 4807, 4808, 4809, 4810, 4811, 4813, 4814, 4815, 4816, 4817, 4818, 4819, 4820, 4821,
4823, 4824, 4901, 4902, 4903, 4904, 4979,
];
fn is_geographic_epsg(code: u32) -> bool {
GEOGRAPHIC_EPSG_CODES.binary_search(&code).is_ok()
}
fn extract_epsg_code(srs_name: &str) -> Option<u32> {
let trimmed = srs_name.trim();
if !trimmed.to_ascii_uppercase().contains("EPSG") {
return None;
}
trimmed
.rsplit([':', '/', '#'])
.map(str::trim)
.find(|segment| !segment.is_empty())
.and_then(|segment| segment.parse().ok())
}
fn srs_uses_lat_lon_order(srs_name: Option<&str>) -> bool {
match srs_name {
None => true,
Some(name) if name.to_ascii_uppercase().contains("CRS84") => false,
Some(name) => extract_epsg_code(name).is_none_or(is_geographic_epsg),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GmlDimsMismatch;
pub fn build_gml_geometry(
geo_type: GeoType,
srs_name: Option<String>,
text: &str,
dims: usize,
) -> Result<Option<GeoLocation>, GmlDimsMismatch> {
let min_points = match geo_type {
GeoType::Point => 1,
GeoType::Line => 2,
GeoType::Polygon => 3,
GeoType::Box => return Ok(None),
};
let lat_lon_order = srs_uses_lat_lon_order(srs_name.as_deref());
let coords = match parse_coordinates_ordered(text, lat_lon_order, dims) {
CoordParse::Ok(coords) => coords,
CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
CoordParse::Invalid => return Ok(None),
};
if coords.len() < min_points || (geo_type == GeoType::Point && coords.len() != 1) {
return Ok(None);
}
Ok(Some(GeoLocation {
geo_type,
coordinates: coords,
srs_name,
..Default::default()
}))
}
pub fn build_gml_envelope(
srs_name: Option<String>,
lower_text: &str,
upper_text: &str,
dims: usize,
) -> Result<Option<GeoLocation>, GmlDimsMismatch> {
let lat_lon_order = srs_uses_lat_lon_order(srs_name.as_deref());
let lower = match parse_coordinates_ordered(lower_text, lat_lon_order, dims) {
CoordParse::Ok(coords) => coords,
CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
CoordParse::Invalid => return Ok(None),
};
let upper = match parse_coordinates_ordered(upper_text, lat_lon_order, dims) {
CoordParse::Ok(coords) => coords,
CoordParse::DimsMismatch => return Err(GmlDimsMismatch),
CoordParse::Invalid => return Ok(None),
};
if lower.len() != 1 || upper.len() != 1 {
return Ok(None);
}
Ok(Some(GeoLocation {
geo_type: GeoType::Box,
coordinates: vec![lower[0], upper[0]],
srs_name,
..Default::default()
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_point() {
let loc = parse_point("45.256 -71.92").unwrap();
assert_eq!(loc.geo_type, GeoType::Point);
assert_eq!(loc.coordinates.len(), 1);
assert_eq!(loc.coordinates[0], (45.256, -71.92));
}
#[test]
fn test_parse_point_invalid() {
assert!(parse_point("45.256").is_none());
assert!(parse_point("45.256 -71.92 extra").is_none());
assert!(parse_point("not numbers").is_none());
assert!(parse_point("").is_none());
}
#[test]
fn test_parse_line() {
let loc = parse_line("45.256 -71.92 46.0 -72.0").unwrap();
assert_eq!(loc.geo_type, GeoType::Line);
assert_eq!(loc.coordinates.len(), 2);
assert_eq!(loc.coordinates[0], (45.256, -71.92));
assert_eq!(loc.coordinates[1], (46.0, -72.0));
}
#[test]
fn test_parse_line_single_point() {
assert!(parse_line("45.256 -71.92").is_none());
}
#[test]
fn test_parse_polygon() {
let loc = parse_polygon("45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0").unwrap();
assert_eq!(loc.geo_type, GeoType::Polygon);
assert_eq!(loc.coordinates.len(), 4);
assert_eq!(loc.coordinates[0], (45.0, -71.0));
assert_eq!(loc.coordinates[3], (45.0, -71.0)); }
#[test]
fn test_parse_box() {
let loc = parse_box("45.0 -72.0 46.0 -71.0").unwrap();
assert_eq!(loc.geo_type, GeoType::Box);
assert_eq!(loc.coordinates.len(), 2);
assert_eq!(loc.coordinates[0], (45.0, -72.0)); assert_eq!(loc.coordinates[1], (46.0, -71.0)); }
#[test]
fn test_parse_box_invalid() {
assert!(parse_box("45.0 -72.0").is_none());
assert!(parse_box("45.0 -72.0 46.0 -71.0 extra values").is_none());
}
#[test]
fn test_coordinate_validation() {
assert!(parse_point("91.0 0.0").is_none());
assert!(parse_point("-91.0 0.0").is_none());
assert!(parse_point("0.0 181.0").is_none());
assert!(parse_point("0.0 -181.0").is_none());
}
#[test]
fn test_handle_entry_element_point() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
assert!(handled);
assert!(entry.r#where.is_some());
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.geo_type, GeoType::Point);
assert_eq!(geo.coordinates[0], (45.256, -71.92));
}
#[test]
fn test_handle_entry_element_line() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled =
handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
assert!(handled);
assert!(entry.r#where.is_some());
assert_eq!(entry.r#where.as_ref().unwrap().geo_type, GeoType::Line);
}
#[test]
fn test_handle_entry_element_unknown() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
assert!(!handled);
assert!(entry.r#where.is_none());
}
#[test]
fn test_geo_location_constructors() {
let point = GeoLocation::point(45.0, -71.0);
assert_eq!(point.geo_type, GeoType::Point);
assert_eq!(point.coordinates.len(), 1);
let line = GeoLocation::line(vec![(45.0, -71.0), (46.0, -72.0)]);
assert_eq!(line.geo_type, GeoType::Line);
assert_eq!(line.coordinates.len(), 2);
let polygon = GeoLocation::polygon(vec![(45.0, -71.0), (46.0, -71.0), (45.0, -71.0)]);
assert_eq!(polygon.geo_type, GeoType::Polygon);
assert_eq!(polygon.coordinates.len(), 3);
let bbox = GeoLocation::bbox(45.0, -72.0, 46.0, -71.0);
assert_eq!(bbox.geo_type, GeoType::Box);
assert_eq!(bbox.coordinates.len(), 2);
}
#[test]
fn test_whitespace_handling() {
let loc = parse_point(" 45.256 -71.92 ").unwrap();
assert_eq!(loc.coordinates[0], (45.256, -71.92));
}
#[test]
fn test_handle_feed_element_point() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
assert!(handled);
assert!(feed.r#where.is_some());
let geo = feed.r#where.as_ref().unwrap();
assert_eq!(geo.geo_type, GeoType::Point);
assert_eq!(geo.coordinates[0], (45.256, -71.92));
}
#[test]
fn test_handle_feed_element_line() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
assert!(handled);
assert!(feed.r#where.is_some());
assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Line);
}
#[test]
fn test_handle_feed_element_polygon() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(
b"polygon",
"45.0 -71.0 46.0 -71.0 46.0 -72.0 45.0 -71.0",
&mut feed,
&limits,
);
assert!(handled);
assert!(feed.r#where.is_some());
assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Polygon);
}
#[test]
fn test_handle_feed_element_box() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
assert!(handled);
assert!(feed.r#where.is_some());
assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Box);
}
#[test]
fn test_handle_feed_element_unknown() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
assert!(!handled);
assert!(feed.r#where.is_none());
}
#[test]
fn test_handle_feed_element_invalid_data() {
let mut feed = FeedMeta::default();
let limits = ParserLimits::default();
let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
assert!(handled);
assert!(feed.r#where.is_none());
}
#[test]
fn test_handle_entry_element_elev() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
assert!(handled);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.elev, Some(1337.5));
}
#[test]
fn test_handle_entry_element_feature_name() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
assert!(handled);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
}
#[test]
fn test_handle_entry_element_feature_type_tag() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"featuretypetag", "mountain", &mut entry, &limits);
assert!(handled);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.feature_type_tag.as_deref(), Some("mountain"));
}
#[test]
fn test_handle_entry_element_relationship_tag() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled =
handle_entry_element(b"relationshiptag", "is-located-at", &mut entry, &limits);
assert!(handled);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.relationship_tag.as_deref(), Some("is-located-at"));
}
#[test]
fn test_extended_attrs_without_geometry() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
handle_entry_element(b"featurename", "Unknown Location", &mut entry, &limits);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.feature_name.as_deref(), Some("Unknown Location"));
assert!(geo.coordinates.is_empty());
}
#[test]
fn test_extended_attrs_invalid_elev() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
let handled = handle_entry_element(b"elev", "not-a-number", &mut entry, &limits);
assert!(handled);
assert!(entry.r#where.is_none());
}
#[test]
fn test_extended_attrs_elev_non_finite_ignored() {
let limits = ParserLimits::default();
for value in ["NaN", "Infinity", "-Infinity"] {
let mut entry = Entry::default();
let handled = handle_entry_element(b"elev", value, &mut entry, &limits);
assert!(handled, "element must be recognized for value {value}");
assert!(
entry.r#where.is_none(),
"non-finite elev '{value}' must not create GeoLocation"
);
}
}
#[test]
fn test_extended_attrs_before_geometry() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
handle_entry_element(b"featurename", "Reverse Order", &mut entry, &limits);
handle_entry_element(b"elev", "500.0", &mut entry, &limits);
handle_entry_element(b"point", "40.0 -74.0", &mut entry, &limits);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.geo_type, GeoType::Point);
assert_eq!(geo.coordinates[0], (40.0, -74.0));
assert_eq!(geo.feature_name.as_deref(), Some("Reverse Order"));
assert_eq!(geo.elev, Some(500.0));
}
#[test]
fn test_extended_attrs_after_geometry() {
let mut entry = Entry::default();
let limits = ParserLimits::default();
handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
let geo = entry.r#where.as_ref().unwrap();
assert_eq!(geo.geo_type, GeoType::Point);
assert_eq!(geo.coordinates[0], (45.256, -71.92));
assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
assert_eq!(geo.elev, Some(1337.5));
}
#[test]
fn test_extract_epsg_code() {
assert_eq!(extract_epsg_code("EPSG:4326"), Some(4326));
assert_eq!(extract_epsg_code("urn:ogc:def:crs:EPSG::4326"), Some(4326));
assert_eq!(
extract_epsg_code("http://www.opengis.net/def/crs/EPSG/0/4326"),
Some(4326)
);
assert_eq!(
extract_epsg_code("http://www.opengis.net/gml/srs/epsg.xml#3857"),
Some(3857)
);
assert_eq!(extract_epsg_code(" EPSG:3857 "), Some(3857));
assert_eq!(extract_epsg_code("http://www.opengis.net/gml"), None);
assert_eq!(extract_epsg_code("not-a-crs"), None);
}
#[test]
fn test_srs_uses_lat_lon_order() {
assert!(srs_uses_lat_lon_order(None));
assert!(srs_uses_lat_lon_order(Some("EPSG:4326")));
assert!(srs_uses_lat_lon_order(Some("urn:ogc:def:crs:EPSG::4326")));
assert!(!srs_uses_lat_lon_order(Some("EPSG:3857")));
assert!(srs_uses_lat_lon_order(Some("some-custom-crs")));
assert!(!srs_uses_lat_lon_order(Some(
"urn:ogc:def:crs:OGC:1.3:CRS84"
)));
assert!(!srs_uses_lat_lon_order(Some("OGC:CRS84")));
}
#[test]
fn test_build_gml_geometry_point_epsg4326() {
let loc = build_gml_geometry(
GeoType::Point,
Some("EPSG:4326".to_string()),
"45.256 -71.92",
2,
);
let loc = loc.unwrap().unwrap();
assert_eq!(loc.geo_type, GeoType::Point);
assert_eq!(loc.coordinates, vec![(45.256, -71.92)]);
assert_eq!(loc.srs_name.as_deref(), Some("EPSG:4326"));
}
#[test]
fn test_build_gml_geometry_point_no_srs_name_defaults_lat_lon() {
let loc = build_gml_geometry(GeoType::Point, None, "45.256 -71.92", 2);
let loc = loc.unwrap().unwrap();
assert_eq!(loc.coordinates, vec![(45.256, -71.92)]);
assert_eq!(loc.srs_name, None);
}
#[test]
fn test_build_gml_geometry_swaps_projected_crs_realistic_meters() {
let loc = build_gml_geometry(
GeoType::Point,
Some("EPSG:3857".to_string()),
"-8004866.0 5675670.0",
2,
);
assert_eq!(
loc.unwrap().unwrap().coordinates,
vec![(5_675_670.0, -8_004_866.0)]
);
}
#[test]
fn test_build_gml_geometry_projected_crs_rejects_non_finite() {
let result =
build_gml_geometry(GeoType::Point, Some("EPSG:3857".to_string()), "NaN NaN", 2);
assert_eq!(result, Ok(None));
}
#[test]
fn test_build_gml_geometry_linestring() {
let loc = build_gml_geometry(
GeoType::Line,
Some("urn:ogc:def:crs:EPSG::4326".to_string()),
"45.256 -71.92 46.0 -72.0",
2,
);
let loc = loc.unwrap().unwrap();
assert_eq!(loc.geo_type, GeoType::Line);
assert_eq!(loc.coordinates.len(), 2);
}
#[test]
fn test_build_gml_geometry_srs_dimension_3_drops_elevation() {
let loc = build_gml_geometry(GeoType::Line, None, "45.0 -71.0 10.0 46.0 -72.0 20.0", 3);
assert_eq!(
loc.unwrap().unwrap().coordinates,
vec![(45.0, -71.0), (46.0, -72.0)]
);
}
#[test]
fn test_build_gml_geometry_srs_dimension_mismatch_sets_bozo() {
let result = build_gml_geometry(GeoType::Point, None, "45.0 -71.0 10.0 46.0 -72.0", 3);
assert_eq!(result, Err(GmlDimsMismatch));
}
#[test]
fn test_build_gml_geometry_comma_separated_coordinates() {
let loc = build_gml_geometry(GeoType::Point, None, "45.256,-71.92", 2);
assert_eq!(loc.unwrap().unwrap().coordinates, vec![(45.256, -71.92)]);
}
#[test]
fn test_build_gml_geometry_polygon_too_few_points() {
let result = build_gml_geometry(GeoType::Polygon, None, "45.0 -71.0 46.0 -71.0", 2);
assert_eq!(result, Ok(None));
}
#[test]
fn test_build_gml_geometry_box_unsupported() {
let result = build_gml_geometry(GeoType::Box, None, "45.0 -71.0 46.0 -71.0", 2);
assert_eq!(result, Ok(None));
}
#[test]
fn test_build_gml_geometry_malformed_text() {
assert_eq!(
build_gml_geometry(GeoType::Point, None, "not numbers", 2),
Ok(None)
);
assert_eq!(build_gml_geometry(GeoType::Point, None, "", 2), Ok(None));
}
#[test]
fn test_build_gml_envelope() {
let loc = build_gml_envelope(None, "42.9 -71.9", "43.1 -71.5", 2);
let loc = loc.unwrap().unwrap();
assert_eq!(loc.geo_type, GeoType::Box);
assert_eq!(loc.coordinates, vec![(42.9, -71.9), (43.1, -71.5)]);
}
#[test]
fn test_build_gml_envelope_swaps_projected_crs() {
let loc = build_gml_envelope(
Some("EPSG:3857".to_string()),
"-8004866.0 5675670.0",
"-8000000.0 5680000.0",
2,
);
assert_eq!(
loc.unwrap().unwrap().coordinates,
vec![(5_675_670.0, -8_004_866.0), (5_680_000.0, -8_000_000.0)]
);
}
#[test]
fn test_build_gml_envelope_srs_dimension_3_drops_elevation() {
let loc = build_gml_envelope(None, "42.9 -71.9 10.0", "43.1 -71.5 20.0", 3);
assert_eq!(
loc.unwrap().unwrap().coordinates,
vec![(42.9, -71.9), (43.1, -71.5)]
);
}
#[test]
fn test_build_gml_envelope_malformed_corner() {
assert_eq!(
build_gml_envelope(None, "not numbers", "43.1 -71.5", 2),
Ok(None)
);
assert_eq!(build_gml_envelope(None, "42.9 -71.9", "", 2), Ok(None));
}
#[test]
fn test_build_gml_envelope_corner_wrong_arity() {
let result = build_gml_envelope(None, "42.9 -71.9 43.1 -71.5", "43.1 -71.5", 2);
assert_eq!(result, Ok(None));
}
#[test]
fn test_build_gml_envelope_dims_mismatch_sets_bozo() {
let result = build_gml_envelope(None, "42.9 -71.9", "43.1 -71.5 20.0", 3);
assert_eq!(result, Err(GmlDimsMismatch));
}
}