use rustial_math::GeoCoord;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub struct Point {
pub coord: GeoCoord,
}
#[derive(Debug, Clone)]
pub struct LineString {
pub coords: Vec<GeoCoord>,
}
#[derive(Debug, Clone)]
pub struct Polygon {
pub exterior: Vec<GeoCoord>,
pub interiors: Vec<Vec<GeoCoord>>,
}
#[derive(Debug, Clone)]
pub struct MultiPoint {
pub points: Vec<Point>,
}
#[derive(Debug, Clone)]
pub struct MultiLineString {
pub lines: Vec<LineString>,
}
#[derive(Debug, Clone)]
pub struct MultiPolygon {
pub polygons: Vec<Polygon>,
}
#[derive(Debug, Clone)]
pub enum Geometry {
Point(Point),
LineString(LineString),
Polygon(Polygon),
MultiPoint(MultiPoint),
MultiLineString(MultiLineString),
MultiPolygon(MultiPolygon),
GeometryCollection(Vec<Geometry>),
}
impl Geometry {
pub fn type_name(&self) -> &'static str {
match self {
Geometry::Point(_) => "Point",
Geometry::LineString(_) => "LineString",
Geometry::Polygon(_) => "Polygon",
Geometry::MultiPoint(_) => "MultiPoint",
Geometry::MultiLineString(_) => "MultiLineString",
Geometry::MultiPolygon(_) => "MultiPolygon",
Geometry::GeometryCollection(_) => "GeometryCollection",
}
}
pub fn is_empty(&self) -> bool {
match self {
Geometry::Point(_) => false,
Geometry::LineString(ls) => ls.coords.is_empty(),
Geometry::Polygon(p) => p.exterior.is_empty(),
Geometry::MultiPoint(mp) => mp.points.is_empty(),
Geometry::MultiLineString(mls) => mls.lines.is_empty(),
Geometry::MultiPolygon(mp) => mp.polygons.is_empty(),
Geometry::GeometryCollection(gc) => gc.iter().all(|g| g.is_empty()),
}
}
pub fn coord_count(&self) -> usize {
match self {
Geometry::Point(_) => 1,
Geometry::LineString(ls) => ls.coords.len(),
Geometry::Polygon(p) => {
p.exterior.len() + p.interiors.iter().map(|h| h.len()).sum::<usize>()
}
Geometry::MultiPoint(mp) => mp.points.len(),
Geometry::MultiLineString(mls) => mls.lines.iter().map(|l| l.coords.len()).sum(),
Geometry::MultiPolygon(mp) => mp
.polygons
.iter()
.map(|p| p.exterior.len() + p.interiors.iter().map(|h| h.len()).sum::<usize>())
.sum(),
Geometry::GeometryCollection(gc) => gc.iter().map(|g| g.coord_count()).sum(),
}
}
}
#[derive(Debug, Clone)]
pub enum PropertyValue {
Null,
Bool(bool),
Number(f64),
String(String),
}
impl PropertyValue {
pub fn is_null(&self) -> bool {
matches!(self, PropertyValue::Null)
}
pub fn as_bool(&self) -> Option<bool> {
match self {
PropertyValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
PropertyValue::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
PropertyValue::String(s) => Some(s),
_ => None,
}
}
}
impl fmt::Display for PropertyValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropertyValue::Null => write!(f, "null"),
PropertyValue::Bool(b) => write!(f, "{b}"),
PropertyValue::Number(n) => write!(f, "{n}"),
PropertyValue::String(s) => write!(f, "{s}"),
}
}
}
impl PartialEq for PropertyValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(PropertyValue::Null, PropertyValue::Null) => true,
(PropertyValue::Bool(a), PropertyValue::Bool(b)) => a == b,
(PropertyValue::Number(a), PropertyValue::Number(b)) => a == b,
(PropertyValue::String(a), PropertyValue::String(b)) => a == b,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct Feature {
pub geometry: Geometry,
pub properties: HashMap<String, PropertyValue>,
}
impl Feature {
pub fn property(&self, key: &str) -> Option<&PropertyValue> {
self.properties.get(key)
}
}
#[derive(Debug, Clone, Default)]
pub struct FeatureCollection {
pub features: Vec<Feature>,
}
impl FeatureCollection {
pub fn len(&self) -> usize {
self.features.len()
}
pub fn is_empty(&self) -> bool {
self.features.is_empty()
}
pub fn total_coords(&self) -> usize {
self.features.iter().map(|f| f.geometry.coord_count()).sum()
}
pub fn iter(&self) -> std::slice::Iter<'_, Feature> {
self.features.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Feature> {
self.features.iter_mut()
}
}
impl<'a> IntoIterator for &'a FeatureCollection {
type Item = &'a Feature;
type IntoIter = std::slice::Iter<'a, Feature>;
fn into_iter(self) -> Self::IntoIter {
self.features.iter()
}
}
impl<'a> IntoIterator for &'a mut FeatureCollection {
type Item = &'a mut Feature;
type IntoIter = std::slice::IterMut<'a, Feature>;
fn into_iter(self) -> Self::IntoIter {
self.features.iter_mut()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_point() -> Geometry {
Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(51.1, 17.0),
})
}
fn sample_linestring() -> Geometry {
Geometry::LineString(LineString {
coords: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(2.0, 2.0),
],
})
}
fn sample_polygon() -> Geometry {
Geometry::Polygon(Polygon {
exterior: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.0),
],
interiors: vec![vec![
GeoCoord::from_lat_lon(0.2, 0.2),
GeoCoord::from_lat_lon(0.2, 0.4),
GeoCoord::from_lat_lon(0.4, 0.4),
GeoCoord::from_lat_lon(0.4, 0.2),
GeoCoord::from_lat_lon(0.2, 0.2),
]],
})
}
#[test]
fn type_name_point() {
assert_eq!(sample_point().type_name(), "Point");
}
#[test]
fn type_name_linestring() {
assert_eq!(sample_linestring().type_name(), "LineString");
}
#[test]
fn type_name_polygon() {
assert_eq!(sample_polygon().type_name(), "Polygon");
}
#[test]
fn type_name_multi_and_collection() {
let mp = Geometry::MultiPoint(MultiPoint {
points: vec![Point {
coord: GeoCoord::from_lat_lon(0.0, 0.0),
}],
});
assert_eq!(mp.type_name(), "MultiPoint");
let mls = Geometry::MultiLineString(MultiLineString { lines: vec![] });
assert_eq!(mls.type_name(), "MultiLineString");
let mpoly = Geometry::MultiPolygon(MultiPolygon { polygons: vec![] });
assert_eq!(mpoly.type_name(), "MultiPolygon");
let gc = Geometry::GeometryCollection(vec![]);
assert_eq!(gc.type_name(), "GeometryCollection");
}
#[test]
fn point_is_never_empty() {
assert!(!sample_point().is_empty());
}
#[test]
fn empty_linestring_is_empty() {
let ls = Geometry::LineString(LineString { coords: vec![] });
assert!(ls.is_empty());
assert!(!sample_linestring().is_empty());
}
#[test]
fn empty_polygon_is_empty() {
let p = Geometry::Polygon(Polygon {
exterior: vec![],
interiors: vec![],
});
assert!(p.is_empty());
assert!(!sample_polygon().is_empty());
}
#[test]
fn empty_geometry_collection() {
let gc = Geometry::GeometryCollection(vec![]);
assert!(gc.is_empty());
let gc_with_empty =
Geometry::GeometryCollection(vec![Geometry::LineString(LineString { coords: vec![] })]);
assert!(gc_with_empty.is_empty());
let gc_with_content = Geometry::GeometryCollection(vec![sample_point()]);
assert!(!gc_with_content.is_empty());
}
#[test]
fn coord_count_point() {
assert_eq!(sample_point().coord_count(), 1);
}
#[test]
fn coord_count_linestring() {
assert_eq!(sample_linestring().coord_count(), 3);
}
#[test]
fn coord_count_polygon_with_hole() {
assert_eq!(sample_polygon().coord_count(), 10);
}
#[test]
fn coord_count_multi_polygon() {
let mp = Geometry::MultiPolygon(MultiPolygon {
polygons: vec![
Polygon {
exterior: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
],
interiors: vec![],
},
Polygon {
exterior: vec![
GeoCoord::from_lat_lon(2.0, 2.0),
GeoCoord::from_lat_lon(2.0, 3.0),
GeoCoord::from_lat_lon(3.0, 2.0),
GeoCoord::from_lat_lon(2.0, 2.0),
],
interiors: vec![],
},
],
});
assert_eq!(mp.coord_count(), 7);
}
#[test]
fn coord_count_geometry_collection() {
let gc = Geometry::GeometryCollection(vec![sample_point(), sample_linestring()]);
assert_eq!(gc.coord_count(), 4); }
#[test]
fn property_value_accessors() {
assert!(PropertyValue::Null.is_null());
assert!(!PropertyValue::Bool(true).is_null());
assert_eq!(PropertyValue::Bool(true).as_bool(), Some(true));
assert_eq!(PropertyValue::Number(42.0).as_bool(), None);
assert_eq!(PropertyValue::Number(3.125).as_f64(), Some(3.125));
assert_eq!(PropertyValue::String("hi".into()).as_f64(), None);
assert_eq!(
PropertyValue::String("hello".into()).as_str(),
Some("hello")
);
assert_eq!(PropertyValue::Null.as_str(), None);
}
#[test]
fn property_value_display() {
assert_eq!(format!("{}", PropertyValue::Null), "null");
assert_eq!(format!("{}", PropertyValue::Bool(false)), "false");
assert_eq!(format!("{}", PropertyValue::Number(1.5)), "1.5");
assert_eq!(format!("{}", PropertyValue::String("abc".into())), "abc");
}
#[test]
fn property_value_equality() {
assert_eq!(PropertyValue::Null, PropertyValue::Null);
assert_eq!(PropertyValue::Bool(true), PropertyValue::Bool(true));
assert_ne!(PropertyValue::Bool(true), PropertyValue::Bool(false));
assert_eq!(PropertyValue::Number(1.0), PropertyValue::Number(1.0));
assert_ne!(PropertyValue::Number(1.0), PropertyValue::Number(2.0));
assert_eq!(
PropertyValue::String("a".into()),
PropertyValue::String("a".into())
);
assert_ne!(PropertyValue::Null, PropertyValue::Bool(false));
}
#[test]
fn feature_property_lookup() {
let mut props = HashMap::new();
props.insert("name".into(), PropertyValue::String("test".into()));
let feature = Feature {
geometry: sample_point(),
properties: props,
};
assert_eq!(
feature.property("name").and_then(|v| v.as_str()),
Some("test")
);
assert!(feature.property("missing").is_none());
}
#[test]
fn feature_collection_len_and_empty() {
let fc = FeatureCollection::default();
assert!(fc.is_empty());
assert_eq!(fc.len(), 0);
}
#[test]
fn feature_collection_total_coords() {
let fc = FeatureCollection {
features: vec![
Feature {
geometry: sample_point(),
properties: HashMap::new(),
},
Feature {
geometry: sample_linestring(),
properties: HashMap::new(),
},
],
};
assert_eq!(fc.total_coords(), 4); assert_eq!(fc.len(), 2);
}
#[test]
fn feature_collection_iteration() {
let fc = FeatureCollection {
features: vec![
Feature {
geometry: sample_point(),
properties: HashMap::new(),
},
Feature {
geometry: sample_linestring(),
properties: HashMap::new(),
},
],
};
let names: Vec<_> = fc.iter().map(|f| f.geometry.type_name()).collect();
assert_eq!(names, vec!["Point", "LineString"]);
let count = (&fc).into_iter().count();
assert_eq!(count, 2);
}
}