Skip to main content

ogc_cql2/geom/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Basic spatial type facades visible from this library.
6//!
7
8mod bbox;
9mod collection;
10mod line;
11mod lines;
12mod point;
13mod points;
14mod polygon;
15mod polygons;
16
17pub use bbox::*;
18pub use collection::*;
19pub use line::*;
20pub use lines::*;
21pub use point::*;
22pub use points::*;
23pub use polygon::*;
24pub use polygons::*;
25
26use crate::{MyError, config::config, crs::CRS, srid::SRID, text::cql2::wkt, wkb::*};
27use core::fmt;
28use geos::{ConstGeometry, Geom, Geometry, GeometryTypes};
29
30// type aliases to silence clippy + work nicely w/ macros...
31pub(crate) type XY1V = Vec<f64>;
32pub(crate) type XY2V = Vec<Vec<f64>>;
33pub(crate) type XY3V = Vec<Vec<Vec<f64>>>;
34pub(crate) type XY4V = Vec<Vec<Vec<Vec<f64>>>>;
35
36/// Ensure a float only has a fixed number of decimal digits in its fractional
37/// part.
38fn ensure_precision(x: &f64) -> f64 {
39    let d = 10.0_f64.powi(
40        config()
41            .default_precision()
42            .try_into()
43            .expect("Failed coercing DEFAULT_PRECISION"),
44    );
45    (x * d).round() / d
46}
47
48/// Geometry type variants handled by this library.
49#[derive(Debug, Clone, Default, PartialEq, PartialOrd)]
50pub enum G {
51    /// Undefined geometry.
52    #[default]
53    Null,
54
55    /// Point geometry.
56    Point(Point),
57    /// Line geometry.
58    Line(Line),
59    /// Polygon geometry.
60    Polygon(Polygon),
61    /// Point collection.
62    Points(Points),
63    /// Line collection.
64    Lines(Lines),
65    /// Polygon collection.
66    Polygons(Polygons),
67    /// Mixed collection excluding BBOX.
68    Vec(Geometries),
69    /// Bounding box geometry.
70    BBox(BBox),
71}
72
73/// Geometry Trait implemented by all [geometry][G] types in this library.
74pub trait GTrait {
75    /// Return TRUE if coordinates are 2D. Return FALSE otherwise.
76    fn is_2d(&self) -> bool;
77
78    /// Generate a WKT string representing this.
79    ///
80    /// This is a convenience method that calls the `to_wkt_fmt()` method w/ a
81    /// pre-configured default precision value.
82    ///
83    /// See the documentation in `.env.template` for `DEFAULT_PRECISION`.
84    fn to_wkt(&self) -> String {
85        self.to_wkt_fmt(config().default_precision())
86    }
87
88    /// Generate a WKT string similar to the `to_wkt()`alternative but w/ a
89    /// given `precision` paramter representing the number of digits to print
90    /// after the decimal point. Note though that if `precision` is `0` only
91    /// the integer part of the coordinate will be shown.
92    ///
93    /// Here are some examples...
94    /// ```rust
95    /// use ogc_cql2::prelude::*;
96    /// # use std::error::Error;
97    /// # fn test() -> Result<(), Box<dyn Error>> {
98    ///     let g = G::try_from("LINESTRING(-180 -45,0 -45)")?;
99    ///     assert_eq!(g.to_wkt_fmt(1), "LINESTRING (-180.0 -45.0, 0.0 -45.0)");
100    ///     // ...
101    ///     let g = G::try_from("POINT(-46.035560 -7.532500)")?;
102    ///     assert_eq!(g.to_wkt_fmt(0), "POINT (-46 -7)");
103    /// # Ok(())
104    /// # }
105    /// ```
106    fn to_wkt_fmt(&self, precision: usize) -> String;
107
108    /// Check if all geometry coordinates fall w/in a given CRS's Area-of-Use,
109    /// aka Extent-of-Validity.
110    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError>;
111
112    /// Return the name/type of this geometry.
113    fn type_(&self) -> &str;
114
115    /// Return the Spatial Reference IDentifier of this.
116    fn srid(&self) -> SRID;
117}
118
119impl GTrait for G {
120    fn is_2d(&self) -> bool {
121        match self {
122            G::Point(x) => x.is_2d(),
123            G::Line(x) => x.is_2d(),
124            G::Polygon(x) => x.is_2d(),
125            G::Points(x) => x.is_2d(),
126            G::Lines(x) => x.is_2d(),
127            G::Polygons(x) => x.is_2d(),
128            G::Vec(x) => x.is_2d(),
129            G::BBox(x) => x.is_2d(),
130            _ => unreachable!("N/A for this geometry type"),
131        }
132    }
133
134    fn to_wkt_fmt(&self, precision: usize) -> String {
135        match self {
136            G::Point(x) => x.to_wkt_fmt(precision),
137            G::Line(x) => x.to_wkt_fmt(precision),
138            G::Polygon(x) => x.to_wkt_fmt(precision),
139            G::Points(x) => x.to_wkt_fmt(precision),
140            G::Lines(x) => x.to_wkt_fmt(precision),
141            G::Polygons(x) => x.to_wkt_fmt(precision),
142            G::Vec(x) => x.to_wkt_fmt(precision),
143            G::BBox(x) => x.to_wkt_fmt(precision),
144            _ => unreachable!("N/A for this geometry type"),
145        }
146    }
147
148    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
149        match self {
150            G::Point(x) => x.check_coordinates(crs),
151            G::Line(x) => x.check_coordinates(crs),
152            G::Polygon(x) => x.check_coordinates(crs),
153            G::Points(x) => x.check_coordinates(crs),
154            G::Lines(x) => x.check_coordinates(crs),
155            G::Polygons(x) => x.check_coordinates(crs),
156            G::Vec(x) => x.check_coordinates(crs),
157            G::BBox(x) => x.check_coordinates(crs),
158            _ => unreachable!("N/A for this geometry type"),
159        }
160    }
161
162    fn type_(&self) -> &str {
163        match self {
164            G::Point(x) => x.type_(),
165            G::Line(x) => x.type_(),
166            G::Polygon(x) => x.type_(),
167            G::Points(x) => x.type_(),
168            G::Lines(x) => x.type_(),
169            G::Polygons(x) => x.type_(),
170            G::Vec(x) => x.type_(),
171            G::BBox(x) => x.type_(),
172            _ => unreachable!("N/A for this geometry type"),
173        }
174    }
175
176    fn srid(&self) -> SRID {
177        match self {
178            G::Point(x) => x.srid(),
179            G::Line(x) => x.srid(),
180            G::Polygon(x) => x.srid(),
181            G::Points(x) => x.srid(),
182            G::Lines(x) => x.srid(),
183            G::Polygons(x) => x.srid(),
184            G::Vec(x) => x.srid(),
185            G::BBox(x) => x.srid(),
186            _ => unreachable!("N/A for this geometry type"),
187        }
188    }
189}
190
191impl G {
192    /// Return this if it was indeed a Point, `None` otherwise.
193    pub fn as_point(&self) -> Option<&Point> {
194        match self {
195            G::Point(x) => Some(x),
196            _ => None,
197        }
198    }
199
200    /// Return this if it was indeed a Line, `None` otherwise.
201    pub fn as_line(&self) -> Option<&Line> {
202        match self {
203            G::Line(x) => Some(x),
204            _ => None,
205        }
206    }
207
208    /// Return this if it was indeed a Polygon, `None` otherwise.
209    pub fn as_polygon(&self) -> Option<&Polygon> {
210        match self {
211            G::Polygon(x) => Some(x),
212            _ => None,
213        }
214    }
215
216    /// Return this if it was indeed a Point collection, `None` otherwise.
217    pub fn as_points(&self) -> Option<&Points> {
218        match self {
219            G::Points(x) => Some(x),
220            _ => None,
221        }
222    }
223
224    /// Return this if it was indeed a Line collection, `None` otherwise.
225    pub fn as_lines(&self) -> Option<&Lines> {
226        match self {
227            G::Lines(x) => Some(x),
228            _ => None,
229        }
230    }
231
232    /// Return this if it was indeed a Polygon collection, `None` otherwise.
233    pub fn as_polygons(&self) -> Option<&Polygons> {
234        match self {
235            G::Polygons(x) => Some(x),
236            _ => None,
237        }
238    }
239
240    // ----- GEOS related methods...
241
242    pub(crate) fn to_geos(&self) -> Result<Geometry, MyError> {
243        match self {
244            G::Point(x) => x.to_geos(),
245            G::Line(x) => x.to_geos(),
246            G::Polygon(x) => x.to_geos(),
247            G::Points(x) => x.to_geos(),
248            G::Lines(x) => x.to_geos(),
249            G::Polygons(x) => x.to_geos(),
250            G::Vec(x) => x.to_geos(),
251            G::BBox(x) => x.to_geos(),
252            _ => unreachable!("N/A for this geometry type"),
253        }
254    }
255
256    pub(crate) fn intersects(&self, other: &G) -> Result<bool, MyError> {
257        let lhs = self.to_geos()?;
258        let rhs = other.to_geos()?;
259        let result = lhs.intersects(&rhs)?;
260        Ok(result)
261    }
262
263    pub(crate) fn equals(&self, other: &G) -> Result<bool, MyError> {
264        let lhs = self.to_geos()?;
265        let rhs = other.to_geos()?;
266        let result = lhs.equals(&rhs)?;
267        Ok(result)
268    }
269
270    pub(crate) fn disjoint(&self, other: &G) -> Result<bool, MyError> {
271        let lhs = self.to_geos()?;
272        let rhs = other.to_geos()?;
273        let result = lhs.disjoint(&rhs)?;
274        Ok(result)
275    }
276
277    pub(crate) fn touches(&self, other: &G) -> Result<bool, MyError> {
278        let lhs = self.to_geos()?;
279        let rhs = other.to_geos()?;
280        let result = lhs.touches(&rhs)?;
281        Ok(result)
282    }
283
284    pub(crate) fn within(&self, other: &G) -> Result<bool, MyError> {
285        let lhs = self.to_geos()?;
286        let rhs = other.to_geos()?;
287        let result = lhs.within(&rhs)?;
288        Ok(result)
289    }
290
291    pub(crate) fn overlaps(&self, other: &G) -> Result<bool, MyError> {
292        let lhs = self.to_geos()?;
293        let rhs = other.to_geos()?;
294        let result = lhs.overlaps(&rhs)?;
295        Ok(result)
296    }
297
298    pub(crate) fn crosses(&self, other: &G) -> Result<bool, MyError> {
299        let lhs = self.to_geos()?;
300        let rhs = other.to_geos()?;
301        let result = lhs.crosses(&rhs)?;
302        Ok(result)
303    }
304
305    pub(crate) fn contains(&self, other: &G) -> Result<bool, MyError> {
306        let lhs = self.to_geos()?;
307        let rhs = other.to_geos()?;
308        let result = lhs.contains(&rhs)?;
309        Ok(result)
310    }
311
312    // ----- methods exposed for use by Functions...
313
314    pub(crate) fn boundary(&self) -> Result<Self, MyError> {
315        let g1 = self.to_geos()?;
316        let g2 = g1.boundary()?;
317        let it = G::try_from(g2)?;
318        Ok(it)
319    }
320
321    pub(crate) fn buffer(&self, width: f64, quadsegs: i32) -> Result<Self, MyError> {
322        let g1 = self.to_geos()?;
323        let g2 = g1.buffer(width, quadsegs)?;
324        let it = G::try_from(g2)?;
325        Ok(it)
326    }
327
328    pub(crate) fn envelope(&self) -> Result<Self, MyError> {
329        let g1 = self.to_geos()?;
330        let g2 = g1.envelope()?;
331        let it = G::try_from(g2)?;
332        Ok(it)
333    }
334
335    pub(crate) fn centroid(&self) -> Result<Self, MyError> {
336        let g1 = self.to_geos()?;
337        let g2 = g1.get_centroid()?;
338        let it = G::try_from(g2)?;
339        Ok(it)
340    }
341
342    pub(crate) fn convex_hull(&self) -> Result<Self, MyError> {
343        let g1 = self.to_geos()?;
344        let g2 = g1.convex_hull()?;
345        let it = G::try_from(g2)?;
346        Ok(it)
347    }
348
349    pub(crate) fn get_x(&self) -> Result<f64, MyError> {
350        if let Some(pt) = self.as_point() {
351            Ok(pt.x())
352        } else {
353            Err(MyError::Runtime("This is NOT a Point".into()))
354        }
355    }
356
357    pub(crate) fn get_y(&self) -> Result<f64, MyError> {
358        if let Some(pt) = self.as_point() {
359            Ok(pt.y())
360        } else {
361            Err(MyError::Runtime("This is NOT a Point".into()))
362        }
363    }
364
365    pub(crate) fn get_z(&self) -> Result<f64, MyError> {
366        if let Some(pt) = self.as_point() {
367            if let Some(z) = pt.z() {
368                Ok(z)
369            } else {
370                Err(MyError::Runtime("This is NOT a 3D Point".into()))
371            }
372        } else {
373            Err(MyError::Runtime("This is NOT a Point".into()))
374        }
375    }
376
377    // ----- methods used to accommodate GeoPackage related ops...
378
379    #[cfg(any(feature = "gpkg_ds", feature = "pg_ds"))]
380    pub(crate) fn to_sql(&self) -> Result<String, MyError> {
381        match self {
382            G::BBox(x) => x.to_sql(),
383            x => {
384                let wkt = x.to_wkt();
385                let srid = self.srid().into_inner();
386                Ok(format!("ST_GeomFromText('{wkt}', {srid})"))
387            }
388        }
389    }
390
391    // ----- crate-private methods invisible to the outside...
392
393    pub(crate) fn set_srid_unchecked(&mut self, srid: &SRID) {
394        match self {
395            G::Point(x) => x.set_srid_unchecked(srid),
396            G::Line(x) => x.set_srid_unchecked(srid),
397            G::Polygon(x) => x.set_srid_unchecked(srid),
398            G::Points(x) => x.set_srid_unchecked(srid),
399            G::Lines(x) => x.set_srid_unchecked(srid),
400            G::Polygons(x) => x.set_srid_unchecked(srid),
401            G::Vec(x) => x.set_srid_unchecked(srid),
402            G::BBox(x) => x.set_srid_unchecked(srid),
403            _ => unreachable!("N/A for this geometry type"),
404        }
405    }
406}
407
408impl fmt::Display for G {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        match self {
411            G::Null => write!(f, ""),
412            G::Point(x) => write!(f, "{x}"),
413            G::Line(x) => write!(f, "{x}"),
414            G::Polygon(x) => write!(f, "{x}"),
415            G::Points(x) => write!(f, "{x}"),
416            G::Lines(x) => write!(f, "{x}"),
417            G::Polygons(x) => write!(f, "{x}"),
418            G::Vec(x) => write!(f, "{x}"),
419            G::BBox(x) => write!(f, "{x}"),
420        }
421    }
422}
423
424// Construct new instance from WKT string...
425impl TryFrom<&str> for G {
426    type Error = MyError;
427
428    fn try_from(value: &str) -> Result<Self, Self::Error> {
429        let mut g = wkt(value).map_err(MyError::Text)?;
430        // NOTE (rsn) 20251023 - WKT does not encode SRIDs.  assign configured
431        // global default set in .env...
432        g.set_srid_unchecked(config().default_srid());
433
434        Ok(g)
435    }
436}
437
438// Construct new instance from GeoPackage WKB byte array...
439impl TryFrom<&[u8]> for G {
440    type Error = MyError;
441
442    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
443        let wkb = GeoPackageBinary::try_from(value)?;
444        Ok(wkb.geom())
445    }
446}
447
448// Construct new instance from GEOS Geometry instance...
449impl TryFrom<Geometry> for G {
450    type Error = MyError;
451
452    fn try_from(value: Geometry) -> Result<Self, Self::Error> {
453        match value.geometry_type()? {
454            GeometryTypes::Point => {
455                let g = Point::try_from(value)?;
456                Ok(G::Point(g))
457            }
458            GeometryTypes::LineString | GeometryTypes::LinearRing => {
459                let g = Line::try_from(value)?;
460                Ok(G::Line(g))
461            }
462            GeometryTypes::Polygon => {
463                let g = Polygon::try_from(value)?;
464                Ok(G::Polygon(g))
465            }
466            GeometryTypes::MultiPoint => {
467                let g = Points::try_from(value)?;
468                Ok(G::Points(g))
469            }
470            GeometryTypes::MultiLineString => {
471                let g = Lines::try_from(value)?;
472                Ok(G::Lines(g))
473            }
474            GeometryTypes::MultiPolygon => {
475                let g = Polygons::try_from(value)?;
476                Ok(G::Polygons(g))
477            }
478            GeometryTypes::GeometryCollection => {
479                let g = Geometries::try_from(value)?;
480                Ok(G::Vec(g))
481            }
482            // IMPORTANT (rsn) 20260310 - not needed w/ GEOS 3.12
483            // x => {
484            //     let msg = format!("Unknown ({x:?}) geometry type");
485            //     error!("Failed: {msg}");
486            //     Err(MyError::Runtime(msg.into()))
487            // }
488        }
489    }
490}
491
492// Construct new instance from GEOS ConstGeometry instance...
493impl TryFrom<ConstGeometry<'_>> for G {
494    type Error = MyError;
495
496    fn try_from(value: ConstGeometry) -> Result<Self, Self::Error> {
497        match value.geometry_type()? {
498            GeometryTypes::Point => {
499                let g = Point::try_from(value)?;
500                Ok(G::Point(g))
501            }
502            GeometryTypes::LineString | GeometryTypes::LinearRing => {
503                let g = Line::try_from(value)?;
504                Ok(G::Line(g))
505            }
506            GeometryTypes::Polygon => {
507                let g = Polygon::try_from(value)?;
508                Ok(G::Polygon(g))
509            }
510            GeometryTypes::MultiPoint => {
511                let g = Points::try_from(value)?;
512                Ok(G::Points(g))
513            }
514            GeometryTypes::MultiLineString => {
515                let g = Lines::try_from(value)?;
516                Ok(G::Lines(g))
517            }
518            GeometryTypes::MultiPolygon => {
519                let g = Polygons::try_from(value)?;
520                Ok(G::Polygons(g))
521            }
522            GeometryTypes::GeometryCollection => {
523                let g = Geometries::try_from(value)?;
524                Ok(G::Vec(g))
525            }
526            // IMPORTANT (rsn) 20260310 - not needed w/ GEOS 3.12
527            // x => {
528            //     let msg = format!("Unknown ({x:?}) geometry type");
529            //     error!("Failed: {msg}");
530            //     Err(MyError::Runtime(msg.into()))
531            // }
532        }
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::{expr::E, text::cql2};
540    use geos::Geom;
541    use std::error::Error;
542
543    #[test]
544    #[tracing_test::traced_test]
545    fn test_to_wkt() {
546        const G: &str = r#"Polygon Z (
547        (
548            -49.88024    0.5      -75993.341684, 
549             -1.5       -0.99999 -100000.0, 
550              0.0        0.5          -0.333333, 
551            -49.88024    0.5      -75993.341684
552        ), (
553            -65.887123   2.00001 -100000.0,
554              0.333333 -53.017711 -79471.332949,
555            180.0        0.0        1852.616704,
556            -65.887123   2.00001 -100000.0
557        ))"#;
558        const WKT: &str = "POLYGON Z ((-49.880240 0.500000 -75993.341684, -1.500000 -0.999990 -100000.000000, 0.000000 0.500000 -0.333333, -49.880240 0.500000 -75993.341684), (-65.887123 2.000010 -100000.000000, 0.333333 -53.017711 -79471.332949, 180.000000 0.000000 1852.616704, -65.887123 2.000010 -100000.000000))";
559
560        let exp = cql2::geom_expression(G);
561        // tracing::debug!("exp = {:?}", exp);
562        let spa = exp.expect("Failed parsing Polygon WKT");
563        let g = match spa {
564            E::Spatial(G::Polygon(x)) => x,
565            _ => panic!("Not a Polygon..."),
566        };
567        // should be a 3D polygon...
568        assert_eq!(g.is_2d(), false);
569
570        let wkt = g.to_wkt_fmt(6);
571        assert_eq!(WKT, wkt);
572    }
573
574    #[test]
575    #[tracing_test::traced_test]
576    fn test_to_geos() -> Result<(), Box<dyn Error>> {
577        let g = G::try_from("POINT(17.03 45.87)")?;
578        // tracing::debug!("g = {g:?}");
579        assert!(matches!(g, G::Point(_)));
580        // tracing::debug!("g (wkt) = {}", g.to_wkt());
581        let gg = g.to_geos()?;
582        assert_eq!(gg.get_type()?, "Point");
583        assert_eq!(gg.get_x()?, 17.03);
584        assert_eq!(gg.get_y()?, 45.87);
585        assert!(!gg.has_z()?);
586
587        let g = G::try_from("LINESTRING(-49.85 0.5, -1.5 -0.999, 0.0 0.5, -49.88 0.5)")?;
588        // tracing::debug!("g = {g:?}");
589        assert!(matches!(g, G::Line(_)));
590        // tracing::debug!("g (wkt) = {}", g.to_wkt());
591        let gg = g.to_geos()?;
592        assert_eq!(gg.get_type()?, "LineString");
593        assert_eq!(gg.get_num_points()?, 4);
594        assert_eq!(gg.get_start_point()?.get_x()?, -49.85);
595        assert_eq!(gg.get_end_point()?.get_y()?, 0.5);
596
597        let g = G::try_from(
598            r#"PolyGon ((
599            -0.333333   89.0, 
600            -102.723546 -0.5, 
601            -179.0     -89.0, 
602            -1.9        89.0, 
603            -0.0        89.0, 
604            2.00001     -1.9, 
605            -0.333333   89.0))"#,
606        )?;
607        // tracing::debug!("g = {g:?}");
608        assert!(matches!(g, G::Polygon(_)));
609        // tracing::debug!("g (wkt) = {}", g.to_wkt());
610        let gg = g.to_geos()?;
611        assert_eq!(gg.get_type()?, "Polygon");
612        assert_eq!(gg.get_num_interior_rings()?, 0);
613        assert_eq!(gg.get_exterior_ring()?.get_num_coordinates()?, 7);
614
615        // multi-stuff
616
617        let g = G::try_from("MULTIPOINT(17.03 45.87, -0.33 89.02)")?;
618        // tracing::debug!("g = {g:?}");
619        assert!(matches!(g, G::Points(_)));
620        // tracing::debug!("g (wkt) = {}", g.to_wkt());
621        let gg = g.to_geos()?;
622        assert_eq!(gg.get_type()?, "MultiPoint");
623        assert_eq!(gg.get_num_geometries()?, 2);
624        assert_eq!(gg.get_geometry_n(0)?.get_x()?, 17.03);
625        assert_eq!(gg.get_geometry_n(1)?.get_y()?, 89.02);
626
627        let g = G::try_from(
628            r#"MULTILINESTRING(
629            (-49.85 0.5, -1.5 -0.999, 0.0 0.5), 
630            (34.3 3.2, 0.1 0.2))"#,
631        )?;
632        // tracing::debug!("g = {g:?}");
633        assert!(matches!(g, G::Lines(_)));
634        // tracing::debug!("g (wkt) = {}", g.to_wkt());
635        let gg = g.to_geos()?;
636        assert_eq!(gg.get_type()?, "MultiLineString");
637        assert_eq!(gg.get_num_geometries()?, 2);
638        assert_eq!(gg.get_geometry_n(0)?.get_start_point()?.get_x()?, -49.85);
639        assert_eq!(gg.get_geometry_n(1)?.get_end_point()?.get_y()?, 0.2);
640
641        let g = G::try_from(
642            r#"MULTIPOLYGON (
643            ((
644                180.0 -16.0671326636424,
645                180.0 -16.5552165666392,
646                179.364142661964 -16.8013540769469,
647                178.725059362997 -17.012041674368,
648                178.596838595117 -16.63915,
649                179.096609362997 -16.4339842775474,
650                179.413509362997 -16.3790542775474,
651                180.0 -16.0671326636424
652            )),((
653                178.12557 -17.50481,
654                178.3736 -17.33992,
655                178.71806 -17.62846,
656                178.55271 -18.15059,
657                177.93266 -18.28799,
658                177.38146 -18.16432,
659                177.28504 -17.72465,
660                177.67087 -17.38114,
661                178.12557 -17.50481
662            )),((
663                -179.793320109049 -16.0208822567412,
664                -179.917369384765 -16.5017831356494,
665                -180 -16.5552165666392,
666                -180 -16.0671326636424,
667                -179.793320109049 -16.0208822567412
668            ))
669        )"#,
670        )?;
671        // tracing::debug!("g = {g:?}");
672        assert!(matches!(g, G::Polygons(_)));
673        // tracing::debug!("g (wkt) = {}", g.to_wkt());
674        let gg = g.to_geos()?;
675        assert_eq!(gg.get_type()?, "MultiPolygon");
676        assert_eq!(gg.get_num_geometries()?, 3);
677        let p1 = gg.get_geometry_n(0)?;
678        assert_eq!(p1.get_type()?, "Polygon");
679        assert_eq!(p1.get_exterior_ring()?.get_num_coordinates()?, 8);
680        assert_eq!(p1.get_num_interior_rings()?, 0);
681
682        let g = G::try_from(
683            r#"GEOMETRYCOLLECTION(
684            POINT(17.03 45.87), 
685            LINESTRING(-49.85 0.5, -1.5 -0.999, 0.0 0.5, -49.88 0.5)
686        )"#,
687        )?;
688        // tracing::debug!("g = {g:?}");
689        assert!(matches!(g, G::Vec(_)));
690        // tracing::debug!("g (wkt) = {}", g.to_wkt());
691        let gg = g.to_geos()?;
692        assert_eq!(gg.get_type()?, "GeometryCollection");
693        assert_eq!(gg.get_num_geometries()?, 2);
694        Ok(())
695    }
696
697    #[test]
698    #[tracing_test::traced_test]
699    fn test_geos() -> Result<(), Box<dyn Error>> {
700        const G: &str = r#"MultiLineString(
701        (-49.85 0.5, -1.5   -0.999,  0.0 0.5, -49.88 0.5 ),
702        (-65.87 2.01, 0.33 -53.07, 180.0 0)
703        )"#;
704
705        let exp = cql2::geom_expression(G);
706        // tracing::debug!("exp = {:?}", exp);
707        let spa = exp.expect("Failed parsing Polygon WKT");
708        let g = match spa {
709            E::Spatial(G::Lines(x)) => x,
710            _ => panic!("Not a Lines..."),
711        };
712        assert_eq!(g.is_2d(), true);
713        assert_eq!(g.num_lines(), 2);
714
715        let geos = g.to_geos().expect("Failed converting to GEOS geometry");
716        assert_eq!(geos.get_num_geometries()?, g.num_lines());
717        let l1 = geos.get_geometry_n(0)?;
718        assert_eq!(l1.get_num_coordinates()?, 4);
719        let l2 = geos.get_geometry_n(1)?;
720        assert_eq!(l2.get_num_coordinates()?, 3);
721
722        Ok(())
723    }
724
725    #[test]
726    #[tracing_test::traced_test]
727    fn test_new_from_wkt() -> Result<(), Box<dyn Error>> {
728        const PT: &str = "POINT (-46.03556 -7.5325)";
729        const LS: &str = "LINESTRING (-180 -45, 0 -45)";
730        const P: &str = "POLYGON ((-180 -90, -90 -90, -90 90, -180 90, -180 -90), (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50))";
731        const MPT: &str = "MULTIPOINT ((7.02 49.92), (90 180))";
732        // const MPT2: &str = "MULTIPOINT (7.02 49.92, 90 180)";
733        const MLS: &str = "MULTILINESTRING ((-180 -45, 0 -45), (0 45, 180 45))";
734        const MP: &str = r#"MULTIPOLYGON(
735            ((-180 -90, -90 -90, -90 90, -180 90, -180 -90),
736             (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)),
737            ((0 0, 10 0, 10 10, 0 10, 0 0))
738        )"#;
739        const MG: &str = r#"GEOMETRYCOLLECTION(
740            POINT(7.02 49.92),
741            POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))
742        )"#;
743
744        let pt = Geometry::new_from_wkt(PT);
745        assert!(pt.is_ok());
746        assert_eq!(pt?.to_wkt()?, PT);
747
748        let ls = Geometry::new_from_wkt(LS);
749        assert!(ls.is_ok());
750        // tracing::debug!("ls = {}", ls?.to_wkt()?);
751        assert_eq!(ls?.to_wkt()?, LS);
752
753        let poly = Geometry::new_from_wkt(P);
754        assert!(poly.is_ok());
755        // tracing::debug!("poly = {}", poly?.to_wkt()?);
756        assert_eq!(poly?.to_wkt()?, P);
757
758        let points = Geometry::new_from_wkt(MPT);
759        assert!(points.is_ok());
760        // tracing::debug!("points = {}", points?.to_wkt()?);
761        assert_eq!(points?.to_wkt()?, MPT);
762
763        let lines = Geometry::new_from_wkt(MLS);
764        assert!(lines.is_ok());
765        // tracing::debug!("lines = {}", lines?.to_wkt()?);
766        assert_eq!(lines?.to_wkt()?, MLS);
767
768        let polys = Geometry::new_from_wkt(MP);
769        assert!(polys.is_ok());
770        // tracing::debug!("polys = {}", polys?.to_wkt()?);
771        assert_eq!(polys?.get_type()?, "MultiPolygon");
772
773        let geometries = Geometry::new_from_wkt(MG);
774        assert!(geometries.is_ok());
775        assert_eq!(geometries?.get_type()?, "GeometryCollection");
776
777        Ok(())
778    }
779
780    #[test]
781    fn test_point_in_polygon() -> Result<(), Box<dyn Error>> {
782        const WKT1: &str = "POINT(-46.03556 -7.5325)";
783        const WKT2: &str =
784            "POLYGON((-65.887123 2.00001, 0.333333 -53.017711, 180.0 0.0, -65.887123 2.00001))";
785
786        let pt = Geometry::new_from_wkt(WKT1).expect("Failed parsing point");
787        let polygon = Geometry::new_from_wkt(WKT2).expect("Failed parsing polygon");
788
789        pt.within(&polygon)?;
790        // so is the inverse...
791        polygon.contains(&pt)?;
792
793        Ok(())
794    }
795
796    #[test]
797    fn test_try_from_wkt() -> Result<(), Box<dyn Error>> {
798        // Test Vector triplet consisting of (a) a test vector input, (b) expected
799        // WKT output, and (c) number of decimal digits in fraction to use.
800        #[rustfmt::skip]
801        const TV: [(&str, &str, usize); 8] = [
802            (
803                "POINT(-46.035560 -7.532500)",
804                "POINT (-46.03556 -7.53250)",
805                5
806            ), (
807                "LINESTRING   (-180 -45,   0 -45)",
808                "LINESTRING (-180.0 -45.0, 0.0 -45.0)", 
809                1
810            ), (
811                r#"POLYGON (
812                    (-180 -90, -90 -90, -90 90, -180 90, -180 -90),
813                    (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)
814                )"#,
815                "POLYGON ((-180 -90, -90 -90, -90 90, -180 90, -180 -90), (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50))",
816                0
817            ), (
818                "MULTIPOINT ((7.02 49.92), (90 180))",
819                "MULTIPOINT (7.02 49.92, 90.00 180.00)",
820                2
821            ), (
822                "MULTILINESTRING ((-180 -45, 0 -45), (0 45, 180 45))",
823                "MULTILINESTRING ((-180.0 -45.0, 0.0 -45.0), (0.0 45.0, 180.0 45.0))",
824                1
825            ), (
826                r#"MULTIPOLYGON((
827                    (-180 -90, -90 -90, -90 90, -180 90, -180 -90),
828                    (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)
829                ), (
830                    (0 0, 10 0, 10 10, 0 10, 0 0)
831                ))"#,
832                "MULTIPOLYGON (((-180 -90, -90 -90, -90 90, -180 90, -180 -90), (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)), ((0 0, 10 0, 10 10, 0 10, 0 0)))",
833                0
834            ), (
835                "GEOMETRYCOLLECTION(POINT(7.02 49.92),POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)))",
836                "GEOMETRYCOLLECTION (POINT (7.0 49.9), POLYGON ((0.0 0.0, 10.0 0.0, 10.0 10.0, 0.0 10.0, 0.0 0.0)))",
837                1
838            ), (
839                "BBOX(51.43,2.54,55.77,6.40)",
840                "BBOX (51.43, 2.54, 55.77, 6.40)",
841                2
842            ),
843        ];
844
845        for (ndx, (wkt, expected, precision)) in TV.iter().enumerate() {
846            // if let Ok(g) = G::try_from_wkt(wkt) {
847            if let Ok(g) = G::try_from(*wkt) {
848                let actual = g.to_wkt_fmt(*precision);
849                assert_eq!(actual, *expected);
850            } else {
851                panic!("Failed parsing WKT at index #{ndx}")
852            }
853        }
854
855        Ok(())
856    }
857
858    #[test]
859    #[tracing_test::traced_test]
860    fn test_geos_envelope() -> Result<(), Box<dyn Error>> {
861        let mut geom = geos::Geometry::new_from_wkt("LINESTRING(0 0, 1 3)")?;
862        geom.set_srid(3587);
863
864        let envelope = geom.envelope()?;
865        let srid = envelope.get_srid()?;
866        tracing::debug!("envelope SRS id = {srid}");
867        assert_eq!(envelope.to_wkt()?, "POLYGON ((0 0, 1 0, 1 3, 0 3, 0 0))");
868
869        Ok(())
870    }
871
872    #[test]
873    #[ignore = "GEOS possible bug"]
874    fn test_geos_wkt() -> Result<(), Box<dyn Error>> {
875        let expected = "POINT (1.0 3.0)";
876
877        let geom = geos::Geometry::new_from_wkt("POINT(1 3)")?;
878        let actual = geom.to_wkt_precision(0)?;
879
880        assert_eq!(actual, expected);
881        Ok(())
882    }
883
884    #[test]
885    #[ignore = "GEOS possible bug"]
886    fn test_geos_wkt_writer() -> Result<(), Box<dyn Error>> {
887        let expected = "POINT (1.00 3.00)";
888
889        let geom = geos::Geometry::new_from_wkt("POINT(1 3)")?;
890        let mut writer = geos::WKTWriter::new()?;
891        writer.set_rounding_precision(2);
892        let actual = writer.write(&geom)?;
893
894        assert_eq!(actual, expected);
895        Ok(())
896    }
897}