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