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