cql2/
geometry.rs

1use std::cmp::Ordering;
2
3use crate::{Error, Expr};
4use geo::*;
5use geo_types::Geometry as GGeom;
6use geozero::{wkt::Wkt, CoordDimensions, ToGeo, ToWkt};
7use serde::{Deserialize, Serialize, Serializer};
8
9const DEFAULT_NDIM: usize = 2;
10
11/// Crate-specific geometry type to hold either WKT or GeoJSON.
12#[derive(Debug, Serialize, Deserialize, Clone)]
13#[serde(untagged)]
14pub enum Geometry {
15    /// A GeoJSON geometry.
16    GeoJSON(geojson::Geometry),
17
18    /// A WKT geometry.
19    #[serde(skip_deserializing, serialize_with = "to_geojson")]
20    Wkt(String),
21}
22
23impl Geometry {
24    /// Converts this geometry to Well-Known Text (WKT).
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use cql2::Geometry;
30    ///
31    /// let geometry: Geometry = serde_json::from_str(
32    ///      "{\"type\":\"Point\",\"coordinates\":[-105.1019,40.1672]}"
33    /// ).unwrap();
34    /// assert_eq!("POINT(-105.1019 40.1672)", geometry.to_wkt().unwrap());
35    /// ```
36    pub fn to_wkt(&self) -> Result<String, Error> {
37        match self {
38            Geometry::Wkt(wkt) => Ok(wkt.clone()),
39            Geometry::GeoJSON(geojson) => {
40                let dims = match geojson_ndims(geojson) {
41                    3 => CoordDimensions::xyz(),
42                    4 => CoordDimensions::xyzm(),
43                    _ => CoordDimensions::xy(),
44                };
45                let geometry: geo_types::Geometry<f64> = geojson.clone().try_into()?;
46                geometry.to_wkt_ndim(dims).map_err(Error::from)
47            }
48        }
49    }
50}
51
52impl PartialEq for Geometry {
53    fn eq(&self, other: &Self) -> bool {
54        let left = Expr::Geometry(self.clone());
55        let right = Expr::Geometry(other.clone());
56        let v = spatial_op(left, right, "s_equals").unwrap_or(Expr::Bool(false));
57        match v {
58            Expr::Bool(v) => v,
59            _ => false,
60        }
61    }
62}
63
64impl PartialOrd for Geometry {
65    fn partial_cmp(&self, _other: &Self) -> Option<Ordering> {
66        None
67    }
68}
69
70fn to_geojson<S>(wkt: &String, serializer: S) -> Result<S::Ok, S::Error>
71where
72    S: Serializer,
73{
74    use serde::ser::Error;
75
76    let geometry = Wkt(wkt).to_geo().map_err(Error::custom)?;
77    geojson::ser::serialize_geometry(&geometry, serializer)
78}
79
80fn geojson_ndims(geojson: &geojson::Geometry) -> usize {
81    use geojson::Value::*;
82    match &geojson.value {
83        Point(coords) => coords.len(),
84        MultiPoint(v) => v.first().map(|v| v.len()).unwrap_or(DEFAULT_NDIM),
85        LineString(v) => v.first().map(|v| v.len()).unwrap_or(DEFAULT_NDIM),
86        MultiLineString(v) => v
87            .first()
88            .and_then(|v| v.first())
89            .map(|v| v.len())
90            .unwrap_or(DEFAULT_NDIM),
91        Polygon(v) => v
92            .first()
93            .and_then(|v| v.first())
94            .map(|v| v.len())
95            .unwrap_or(DEFAULT_NDIM),
96        MultiPolygon(v) => v
97            .first()
98            .and_then(|v| v.first())
99            .and_then(|v| v.first())
100            .map(|v| v.len())
101            .unwrap_or(DEFAULT_NDIM),
102        GeometryCollection(v) => v.first().map(geojson_ndims).unwrap_or(DEFAULT_NDIM),
103    }
104}
105
106/// Run a spatial operation.
107pub fn spatial_op(left: Expr, right: Expr, op: &str) -> Result<Expr, Error> {
108    let left: GGeom = GGeom::try_from(left)?;
109    let right: GGeom = GGeom::try_from(right)?;
110    let rel = left.relate(&right);
111    let out = match op {
112        "s_equals" => rel.is_equal_topo(),
113        "s_intersects" | "intersects" => rel.is_intersects(),
114        "s_disjoint" => rel.is_disjoint(),
115        "s_touches" => rel.is_touches(),
116        "s_within" => rel.is_within(),
117        "s_overlaps" => rel.is_overlaps(),
118        "s_crosses" => rel.is_crosses(),
119        "s_contains" => rel.is_contains(),
120        &_ => todo!(),
121    };
122    Ok(Expr::Bool(out))
123}