use std::fmt;
#[cfg(feature = "geom-geojson")]
mod geojson;
#[cfg(feature = "geom-wkb")]
mod wkb;
#[cfg(feature = "geom-wkt")]
mod wkt;
pub type Coord = (f64, f64);
#[derive(Clone, Debug, PartialEq)]
pub struct Polygon {
pub exterior: Vec<Coord>,
pub interiors: Vec<Vec<Coord>>,
}
impl Polygon {
pub fn new(exterior: Vec<Coord>) -> Self {
Polygon {
exterior,
interiors: Vec::new(),
}
}
pub fn with_hole(mut self, hole: Vec<Coord>) -> Self {
self.interiors.push(hole);
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum Geometry {
Point(Coord),
MultiPoint(Vec<Coord>),
LineString(Vec<Coord>),
MultiLineString(Vec<Vec<Coord>>),
Polygon(Polygon),
MultiPolygon(Vec<Polygon>),
GeometryCollection(Vec<Geometry>),
#[default]
Empty,
}
impl Geometry {
pub fn is_empty(&self) -> bool {
match self {
Geometry::Empty => true,
Geometry::Point(_) => false,
Geometry::MultiPoint(pts) => pts.is_empty(),
Geometry::LineString(pts) => pts.is_empty(),
Geometry::MultiLineString(ls) => ls.iter().all(|l| l.is_empty()),
Geometry::Polygon(p) => p.exterior.is_empty(),
Geometry::MultiPolygon(ps) => ps.iter().all(|p| p.exterior.is_empty()),
Geometry::GeometryCollection(cs) => cs.iter().all(|c| c.is_empty()),
}
}
pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
let mut acc: Option<(f64, f64, f64, f64)> = None;
self.for_each_coord(&mut |(x, y)| {
if !x.is_finite() || !y.is_finite() {
return;
}
match &mut acc {
Some((xmin, ymin, xmax, ymax)) => {
if x < *xmin {
*xmin = x;
}
if x > *xmax {
*xmax = x;
}
if y < *ymin {
*ymin = y;
}
if y > *ymax {
*ymax = y;
}
}
None => acc = Some((x, y, x, y)),
}
});
acc
}
fn for_each_coord(&self, f: &mut impl FnMut(Coord)) {
match self {
Geometry::Empty => {}
Geometry::Point(c) => f(*c),
Geometry::MultiPoint(cs) | Geometry::LineString(cs) => {
for c in cs {
f(*c);
}
}
Geometry::MultiLineString(ls) => {
for line in ls {
for c in line {
f(*c);
}
}
}
Geometry::Polygon(p) => {
for c in &p.exterior {
f(*c);
}
for ring in &p.interiors {
for c in ring {
f(*c);
}
}
}
Geometry::MultiPolygon(ps) => {
for p in ps {
for c in &p.exterior {
f(*c);
}
for ring in &p.interiors {
for c in ring {
f(*c);
}
}
}
}
Geometry::GeometryCollection(cs) => {
for c in cs {
c.for_each_coord(f);
}
}
}
}
#[cfg(feature = "geom-wkt")]
pub fn from_wkt(s: &str) -> Result<Self, ParseError> {
wkt::parse(s)
}
#[cfg(feature = "geom-wkb")]
pub fn from_wkb(bytes: &[u8]) -> Result<Self, ParseError> {
wkb::parse(bytes)
}
#[cfg(feature = "geom-geojson")]
pub fn from_geojson(s: &str) -> Result<Self, ParseError> {
geojson::parse(s)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParseError {
Syntax(String),
UnknownType(String),
UnexpectedEnd,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::Syntax(msg) => write!(f, "geometry parse error: {msg}"),
ParseError::UnknownType(t) => write!(f, "geometry parse error: unknown type {t:?}"),
ParseError::UnexpectedEnd => write!(f, "geometry parse error: unexpected end of input"),
}
}
}
impl std::error::Error for ParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_empty() {
assert!(Geometry::Empty.is_empty());
assert!(Geometry::default().is_empty());
assert!(Geometry::MultiPoint(vec![]).is_empty());
assert!(Geometry::GeometryCollection(vec![Geometry::Empty]).is_empty());
}
#[test]
fn point_is_not_empty() {
assert!(!Geometry::Point((1.0, 2.0)).is_empty());
}
#[test]
fn bounds_of_empty_is_none() {
assert_eq!(Geometry::Empty.bounds(), None);
assert_eq!(Geometry::MultiPoint(vec![]).bounds(), None);
}
#[test]
fn bounds_of_point() {
assert_eq!(
Geometry::Point((1.5, 2.5)).bounds(),
Some((1.5, 2.5, 1.5, 2.5))
);
}
#[test]
fn bounds_of_polygon_with_hole() {
let poly = Polygon::new(vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
])
.with_hole(vec![
(2.0, 2.0),
(8.0, 2.0),
(8.0, 8.0),
(2.0, 8.0),
(2.0, 2.0),
]);
assert_eq!(
Geometry::Polygon(poly).bounds(),
Some((0.0, 0.0, 10.0, 10.0))
);
}
#[test]
fn bounds_of_collection() {
let g = Geometry::GeometryCollection(vec![
Geometry::Point((1.0, 2.0)),
Geometry::LineString(vec![(5.0, -1.0), (3.0, 4.0)]),
]);
assert_eq!(g.bounds(), Some((1.0, -1.0, 5.0, 4.0)));
}
#[test]
fn bounds_skips_nan() {
let g = Geometry::MultiPoint(vec![(f64::NAN, 0.0), (1.0, 2.0), (3.0, f64::INFINITY)]);
assert_eq!(g.bounds(), Some((1.0, 2.0, 1.0, 2.0)));
}
}