ogc_cql2/
geom.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4#![allow(dead_code)]
5
6//! Basic spatial type facades visible from this library.
7//!
8
9use crate::{MyError, config::config, crs::CRS, text::cql2::wkt};
10use core::fmt;
11use geos::{CoordSeq, Geometry};
12use tracing::error;
13
14/// Ensure a float only has a fixed number of decimal digits in its fractional
15/// part.
16fn ensure_precision(x: &f64) -> f64 {
17    let d = 10.0_f64.powi(
18        config()
19            .default_precision()
20            .try_into()
21            .expect("Failed coercing DEFAULT_PRECISION"),
22    );
23    (x * d).round() / d
24}
25
26/// Geometry type variants handled by this library.
27#[derive(Debug, Clone, PartialEq, PartialOrd)]
28pub enum G {
29    /// Point geometry.
30    Point(Point),
31    /// Line geometry.
32    Line(Line),
33    /// Polygon geometry.
34    Polygon(Polygon),
35    /// Point collection.
36    Points(Points),
37    /// Line collection.
38    Lines(Lines),
39    /// Polygon collection.
40    Polygons(Polygons),
41    /// Mixed collection excluding BBOX.
42    Vec(Geometries),
43    /// Bounding box geometry.
44    BBox(BBox),
45}
46
47/// Geometry Trait implemented by all geometry types in this library.
48pub trait GTrait {
49    /// Return TRUE if coordinates are 2D. Return FALSE otherwise.
50    fn is_2d(&self) -> bool;
51
52    /// Generate a WKT string representing this.
53    ///
54    /// This is a convenience method that calls the `to_wkt_fmt()` method w/ a
55    /// pre-configured default precision value.
56    ///
57    /// See the documentation in `.env.template` for `DEFAULT_PRECISION`.
58    fn to_wkt(&self) -> String {
59        self.to_wkt_fmt(config().default_precision())
60    }
61
62    /// Generate a WKT string similar to the `to_wkt()`alternative but w/ a
63    /// given `precision` paramter representing the number of digits to print
64    /// after the decimal point. Note though that if `precision` is `0` only
65    /// the integer part of the coordinate will be shown.
66    ///
67    /// Here are some examples...
68    /// ```rust
69    /// use ogc_cql2::prelude::*;
70    /// # use std::error::Error;
71    /// # fn test() -> Result<(), Box<dyn Error>> {
72    ///     let g = G::try_from_wkt("LINESTRING(-180 -45,0 -45)")?;
73    ///     assert_eq!(g.to_wkt_fmt(1), "LINESTRING (-180.0 -45.0, 0.0 -45.0)");
74    ///     // ...
75    ///     let g = G::try_from_wkt("POINT(-46.035560 -7.532500)")?;
76    ///     assert_eq!(g.to_wkt_fmt(0), "POINT (-46 -7)");
77    /// # Ok(())
78    /// # }
79    /// ```
80    fn to_wkt_fmt(&self, precision: usize) -> String;
81
82    /// Check if all geometry coordinates fall w/in a given CRS's Area-of-Use,
83    /// aka Extent-of-Validity.
84    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError>;
85
86    /// Try creating a `geos` [Geometry] instance from this.
87    fn to_geos(&self) -> Result<Geometry, MyError>;
88}
89
90impl GTrait for G {
91    fn is_2d(&self) -> bool {
92        match self {
93            G::Point(x) => x.is_2d(),
94            G::Line(x) => x.is_2d(),
95            G::Polygon(x) => x.is_2d(),
96            G::Points(x) => x.is_2d(),
97            G::Lines(x) => x.is_2d(),
98            G::Polygons(x) => x.is_2d(),
99            G::Vec(x) => x.is_2d(),
100            G::BBox(x) => x.is_2d(),
101        }
102    }
103
104    fn to_wkt_fmt(&self, precision: usize) -> String {
105        match self {
106            G::Point(x) => x.to_wkt_fmt(precision),
107            G::Line(x) => x.to_wkt_fmt(precision),
108            G::Polygon(x) => x.to_wkt_fmt(precision),
109            G::Points(x) => x.to_wkt_fmt(precision),
110            G::Lines(x) => x.to_wkt_fmt(precision),
111            G::Polygons(x) => x.to_wkt_fmt(precision),
112            G::Vec(x) => x.to_wkt_fmt(precision),
113            G::BBox(x) => x.to_wkt_fmt(precision),
114        }
115    }
116
117    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
118        match self {
119            G::Point(x) => x.check_coordinates(crs),
120            G::Line(x) => x.check_coordinates(crs),
121            G::Polygon(x) => x.check_coordinates(crs),
122            G::Points(x) => x.check_coordinates(crs),
123            G::Lines(x) => x.check_coordinates(crs),
124            G::Polygons(x) => x.check_coordinates(crs),
125            G::Vec(x) => x.check_coordinates(crs),
126            G::BBox(x) => x.check_coordinates(crs),
127        }
128    }
129
130    fn to_geos(&self) -> Result<Geometry, MyError> {
131        match self {
132            G::Point(x) => x.to_geos(),
133            G::Line(x) => x.to_geos(),
134            G::Polygon(x) => x.to_geos(),
135            G::Points(x) => x.to_geos(),
136            G::Lines(x) => x.to_geos(),
137            G::Polygons(x) => x.to_geos(),
138            G::Vec(x) => x.to_geos(),
139            G::BBox(x) => x.to_geos(),
140        }
141    }
142}
143
144impl G {
145    /// Parse the input string as a WKT and if valid return a geometry instance
146    /// representing it.
147    pub fn try_from_wkt(s: &str) -> Result<Self, MyError> {
148        let g = wkt(s).map_err(|x| MyError::Runtime(format!("Not WKT: {x}").into()))?;
149        Ok(g)
150    }
151}
152
153impl fmt::Display for G {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            G::Point(x) => write!(f, "{x}"),
157            G::Line(x) => write!(f, "{x}"),
158            G::Polygon(x) => write!(f, "{x}"),
159            G::Points(x) => write!(f, "{x}"),
160            G::Lines(x) => write!(f, "{x}"),
161            G::Polygons(x) => write!(f, "{x}"),
162            G::Vec(x) => write!(f, "{x}"),
163            G::BBox(x) => write!(f, "{x}"),
164        }
165    }
166}
167
168// ----- Point ----------------------------------------------------------------
169
170/// 2D or 3D point geometry.
171#[derive(Debug, Clone, PartialEq, PartialOrd)]
172pub struct Point {
173    coord: Vec<f64>,
174}
175
176impl GTrait for Point {
177    fn is_2d(&self) -> bool {
178        self.coord.len() == 2
179    }
180
181    fn to_wkt_fmt(&self, precision: usize) -> String {
182        if self.is_2d() {
183            format!("POINT ({})", Self::coords_with_dp(self.as_2d(), precision))
184        } else {
185            format!(
186                "POINT Z ({})",
187                Self::coords_with_dp(self.as_3d(), precision)
188            )
189        }
190    }
191
192    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
193        crs.check_point(&self.coord)
194    }
195
196    fn to_geos(&self) -> Result<Geometry, MyError> {
197        Self::to_geos_impl(&self.coord)
198    }
199}
200
201impl Point {
202    pub(crate) fn from(coord: Vec<f64>) -> Self {
203        // shape the input coordinates to a fixed precision; i.e. a fixed
204        // number of decimals so `geos` can reliably assert equality of
205        // coordinate values.
206        let coord = Self::ensure_precision_impl(&coord);
207        Point { coord }
208    }
209
210    fn to_geos_impl(xy: &[f64]) -> Result<Geometry, MyError> {
211        let xy = CoordSeq::new_from_vec(&[xy]).map_err(MyError::Geos)?;
212        Geometry::create_point(xy).map_err(MyError::Geos)
213    }
214
215    /// Outputs given coordinates sequentially seperated by a space.
216    pub(crate) fn coords_as_txt(coord: &[f64]) -> String {
217        Self::coords_with_dp(coord, config().default_precision())
218    }
219
220    fn coords_with_dp(coord: &[f64], precision: usize) -> String {
221        if coord.len() == 2 {
222            format!("{:.2$} {:.2$}", coord[0], coord[1], precision)
223        } else {
224            format!(
225                "{:.3$} {:.3$} {:.3$}",
226                coord[0], coord[1], coord[2], precision
227            )
228        }
229    }
230
231    /// Return the 2D coordinates of this point.
232    fn as_2d(&self) -> &[f64; 2] {
233        self.coord
234            .as_slice()
235            .try_into()
236            .expect("Failed coercing Point to 2D")
237    }
238
239    /// Return the 3D coordinates of this point.
240    fn as_3d(&self) -> &[f64; 3] {
241        self.coord
242            .as_slice()
243            .try_into()
244            .expect("Failed coercing Point to 3D")
245    }
246
247    fn ensure_precision_impl(coord: &[f64]) -> Vec<f64> {
248        coord.iter().map(ensure_precision).collect()
249    }
250
251    // Return the 1st coordinate of this.
252    #[cfg(test)]
253    fn x(&self) -> f64 {
254        self.coord[0]
255    }
256
257    // Return the 2nd coordinate of this.
258    #[cfg(test)]
259    fn y(&self) -> f64 {
260        self.coord[1]
261    }
262
263    // Return the 3rd coordinate of this if it's a 3D one. Return `None` otherwise.
264    #[cfg(test)]
265    fn z(&self) -> Option<f64> {
266        if self.coord.len() == 2 {
267            None
268        } else {
269            Some(self.coord[2])
270        }
271    }
272}
273
274impl fmt::Display for Point {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
276        write!(f, "Pt (...)")
277    }
278}
279
280// ----- Line -----------------------------------------------------------------
281
282/// 2D or 3D line-string geometry.
283#[derive(Debug, Clone, PartialEq, PartialOrd)]
284pub struct Line {
285    coord: Vec<Vec<f64>>,
286}
287
288impl GTrait for Line {
289    fn is_2d(&self) -> bool {
290        self.coord[0].len() == 2
291    }
292
293    fn to_wkt_fmt(&self, precision: usize) -> String {
294        if self.is_2d() {
295            format!(
296                "LINESTRING {}",
297                Self::coords_with_dp(&self.coord, precision)
298            )
299        } else {
300            format!(
301                "LINESTRING Z {}",
302                Self::coords_with_dp(&self.coord, precision)
303            )
304        }
305    }
306
307    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
308        crs.check_line(&self.coord)
309    }
310
311    fn to_geos(&self) -> Result<Geometry, MyError> {
312        Self::to_geos_impl(&self.coord)
313    }
314}
315
316impl Line {
317    pub(crate) fn from(coord: Vec<Vec<f64>>) -> Self {
318        let coord = Self::ensure_precision_impl(&coord);
319        Line { coord }
320    }
321
322    fn to_geos_impl(xy: &[Vec<f64>]) -> Result<Geometry, MyError> {
323        let vertices: Vec<&[f64]> = xy.iter().map(|x| x.as_slice()).collect();
324        let xy = CoordSeq::new_from_vec(&vertices).map_err(MyError::Geos)?;
325        Geometry::create_line_string(xy).map_err(MyError::Geos)
326    }
327
328    fn ensure_precision_impl(coord: &[Vec<f64>]) -> Vec<Vec<f64>> {
329        coord
330            .iter()
331            .map(|x| Point::ensure_precision_impl(x))
332            .collect()
333    }
334
335    pub(crate) fn coords_as_txt(coord: &[Vec<f64>]) -> String {
336        Self::coords_with_dp(coord, config().default_precision())
337    }
338
339    fn coords_with_dp(coord: &[Vec<f64>], precision: usize) -> String {
340        let points: Vec<String> = coord
341            .iter()
342            .map(|x| Point::coords_with_dp(x, precision))
343            .collect();
344        format!("({})", points.join(", "))
345    }
346
347    // Return the number of vertices.
348    fn size(&self) -> usize {
349        self.coord.len()
350    }
351
352    // Return TRUE if the first and last vertices coincide. Return FALSE
353    // otherwise.
354    fn is_closed(&self) -> bool {
355        self.coord.first() == self.coord.last()
356    }
357
358    // Return TRUE if this cnsists of at least 4 points w/ the first and last
359    // ones coinciding. Return FALSE otherwise.
360    fn is_ring(&self) -> bool {
361        self.size() > 3 && self.is_closed()
362    }
363
364    fn first(&self) -> Option<&Vec<f64>> {
365        self.coord.first()
366    }
367
368    fn last(&self) -> Option<&Vec<f64>> {
369        self.coord.last()
370    }
371}
372
373impl fmt::Display for Line {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
375        write!(f, "LINE (...)")
376    }
377}
378
379// ----- Polygon --------------------------------------------------------------
380
381/// 2D or 3D polygon geometry.
382#[derive(Debug, Clone, PartialEq, PartialOrd)]
383pub struct Polygon {
384    rings: Vec<Vec<Vec<f64>>>,
385}
386
387impl GTrait for Polygon {
388    fn is_2d(&self) -> bool {
389        self.rings[0][0].len() == 2
390    }
391
392    fn to_wkt_fmt(&self, precision: usize) -> String {
393        if self.is_2d() {
394            format!("POLYGON {}", Self::coords_with_dp(&self.rings, precision))
395        } else {
396            format!("POLYGON Z {}", Self::coords_with_dp(&self.rings, precision))
397        }
398    }
399
400    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
401        crs.check_polygon(&self.rings)
402    }
403
404    fn to_geos(&self) -> Result<Geometry, MyError> {
405        Self::to_geos_impl(&self.rings)
406    }
407}
408
409impl Polygon {
410    pub(crate) fn from(rings: Vec<Vec<Vec<f64>>>) -> Self {
411        let rings = Self::ensure_precision_impl(&rings);
412        Polygon { rings }
413    }
414
415    fn ensure_precision_impl(rings: &[Vec<Vec<f64>>]) -> Vec<Vec<Vec<f64>>> {
416        rings
417            .iter()
418            .map(|r| Line::ensure_precision_impl(r))
419            .collect()
420    }
421
422    pub(crate) fn coords_as_txt(rings: &[Vec<Vec<f64>>]) -> String {
423        Self::coords_with_dp(rings, config().default_precision())
424    }
425
426    fn coords_with_dp(rings: &[Vec<Vec<f64>>], precision: usize) -> String {
427        let rings: Vec<String> = rings
428            .iter()
429            .map(|x| Line::coords_with_dp(x, precision))
430            .collect();
431        format!("({})", rings.join(", "))
432    }
433
434    fn to_geos_impl(rings: &[Vec<Vec<f64>>]) -> Result<Geometry, MyError> {
435        let vertices: Vec<&[f64]> = rings[0].iter().map(|x| x.as_slice()).collect();
436        let xy = CoordSeq::new_from_vec(&vertices).map_err(MyError::Geos)?;
437        let exterior = Geometry::create_linear_ring(xy).map_err(MyError::Geos)?;
438
439        let mut interiors = vec![];
440        for hole in &rings[1..] {
441            let vertices: Vec<&[f64]> = hole.iter().map(|x| x.as_slice()).collect();
442            let xy = CoordSeq::new_from_vec(&vertices).map_err(MyError::Geos)?;
443            let hole = Geometry::create_linear_ring(xy).map_err(MyError::Geos)?;
444            interiors.push(hole);
445        }
446
447        Geometry::create_polygon(exterior, interiors).map_err(MyError::Geos)
448    }
449
450    // Return the outer (always the first) linear ring contour of this.
451    fn outer(&self) -> &Vec<Vec<f64>> {
452        self.rings[0].as_ref()
453    }
454
455    fn outer_as_ring(&self) -> Line {
456        Line::from(self.outer().to_vec())
457    }
458
459    // Return TRUE if this has holes; i.e. more than 1 linear ring. Return
460    // FALSE otherwise.
461    fn has_holes(&self) -> bool {
462        self.rings.len() > 1
463    }
464
465    // Return the array of inner (holes) linear rings of this.
466    fn inners(&self) -> &[Vec<Vec<f64>>] {
467        &self.rings.as_slice()[1..]
468    }
469}
470
471impl fmt::Display for Polygon {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
473        write!(f, "POLYGON (...)")
474    }
475}
476
477// ----- Points ---------------------------------------------------------------
478
479/// Collection of point geometries.
480#[derive(Debug, Clone, PartialEq, PartialOrd)]
481pub struct Points {
482    points: Vec<Vec<f64>>,
483}
484
485impl GTrait for Points {
486    fn is_2d(&self) -> bool {
487        self.points[0].len() == 2
488    }
489
490    fn to_wkt_fmt(&self, precision: usize) -> String {
491        if self.is_2d() {
492            format!(
493                "MULTIPOINT {}",
494                Self::coords_with_dp(&self.points, precision)
495            )
496        } else {
497            format!(
498                "MULTIPOINT Z {}",
499                Self::coords_with_dp(&self.points, precision)
500            )
501        }
502    }
503
504    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
505        if self.points.iter().all(|p| crs.check_point(p).is_ok()) {
506            Ok(())
507        } else {
508            Err(MyError::Runtime(
509                "At least one point has invalid coordinates".into(),
510            ))
511        }
512    }
513
514    fn to_geos(&self) -> Result<Geometry, MyError> {
515        let mut points: Vec<Geometry> = vec![];
516        for p in &self.points {
517            let g = Point::to_geos_impl(p)?;
518            points.push(g);
519        }
520        Geometry::create_multipoint(points).map_err(MyError::Geos)
521    }
522}
523
524impl Points {
525    pub(crate) fn from(points: Vec<Vec<f64>>) -> Self {
526        let points = points
527            .iter()
528            .map(|x| Point::ensure_precision_impl(x))
529            .collect();
530        Points { points }
531    }
532
533    pub(crate) fn coords_as_txt(points: &[Vec<f64>]) -> String {
534        Self::coords_with_dp(points, config().default_precision())
535    }
536
537    fn coords_with_dp(points: &[Vec<f64>], precision: usize) -> String {
538        let points: Vec<String> = points
539            .iter()
540            .map(|x| Point::coords_with_dp(x, precision))
541            .collect();
542        format!("({})", points.join(", "))
543    }
544
545    #[cfg(test)]
546    // Return the number of points in this instance.
547    pub(crate) fn size(&self) -> usize {
548        self.points.len()
549    }
550}
551
552impl fmt::Display for Points {
553    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
554        write!(f, "POINTS (...)")
555    }
556}
557
558// ----- Lines ----------------------------------------------------------------
559
560/// Collection of line-string geometries.
561#[derive(Debug, Clone, PartialEq, PartialOrd)]
562pub struct Lines {
563    lines: Vec<Vec<Vec<f64>>>,
564}
565
566impl GTrait for Lines {
567    fn is_2d(&self) -> bool {
568        self.lines[0][0].len() == 2
569    }
570
571    fn to_wkt_fmt(&self, precision: usize) -> String {
572        if self.is_2d() {
573            format!(
574                "MULTILINESTRING {}",
575                Self::coords_with_dp(self.lines.as_slice(), precision)
576            )
577        } else {
578            format!(
579                "MULTILINESTRING Z {}",
580                Self::coords_with_dp(self.lines.as_slice(), precision)
581            )
582        }
583    }
584
585    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
586        if self.lines.iter().all(|l| crs.check_line(l).is_ok()) {
587            Ok(())
588        } else {
589            Err(MyError::Runtime(
590                "At least one line has invalid coordinates".into(),
591            ))
592        }
593    }
594
595    fn to_geos(&self) -> Result<Geometry, MyError> {
596        let mut lines: Vec<Geometry> = vec![];
597        for l in &self.lines {
598            let g = Line::to_geos_impl(l)?;
599            lines.push(g);
600        }
601        Geometry::create_multiline_string(lines).map_err(MyError::Geos)
602    }
603}
604
605impl Lines {
606    pub(crate) fn from(lines: Vec<Vec<Vec<f64>>>) -> Self {
607        let lines = lines
608            .iter()
609            .map(|x| Line::ensure_precision_impl(x))
610            .collect();
611        Lines { lines }
612    }
613
614    pub(crate) fn coords_as_txt(lines: &[Vec<Vec<f64>>]) -> String {
615        Self::coords_with_dp(lines, config().default_precision())
616    }
617
618    fn coords_with_dp(lines: &[Vec<Vec<f64>>], precision: usize) -> String {
619        let lines: Vec<String> = lines
620            .iter()
621            .map(|x| Line::coords_with_dp(x.as_slice(), precision))
622            .collect();
623        format!("({})", lines.join(", "))
624    }
625
626    // Return the number of lines in this instance.
627    #[cfg(test)]
628    fn size(&self) -> usize {
629        self.lines.len()
630    }
631
632    // Return the lines in this as a slice.
633    #[cfg(test)]
634    fn lines(&self) -> &[Vec<Vec<f64>>] {
635        self.lines.as_slice()
636    }
637}
638
639impl fmt::Display for Lines {
640    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
641        write!(f, "LINES (...)")
642    }
643}
644
645// ----- Polygons -------------------------------------------------------------
646
647/// Collection of polygon geometries.
648#[derive(Debug, Clone, PartialEq, PartialOrd)]
649pub struct Polygons {
650    polygons: Vec<Vec<Vec<Vec<f64>>>>,
651}
652
653impl GTrait for Polygons {
654    fn is_2d(&self) -> bool {
655        self.polygons[0][0][0].len() == 2
656    }
657
658    fn to_wkt_fmt(&self, precision: usize) -> String {
659        if self.is_2d() {
660            format!(
661                "MULTIPOLYGON {}",
662                Self::coords_with_dp(self.polygons.as_slice(), precision)
663            )
664        } else {
665            format!(
666                "MULTIPOLYGON Z {}",
667                Self::coords_with_dp(self.polygons.as_slice(), precision)
668            )
669        }
670    }
671
672    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
673        if self
674            .polygons
675            .iter()
676            .all(|poly| crs.check_polygon(poly).is_ok())
677        {
678            Ok(())
679        } else {
680            Err(MyError::Runtime(
681                "At least one polygon has invalid coordinates".into(),
682            ))
683        }
684    }
685
686    fn to_geos(&self) -> Result<Geometry, MyError> {
687        let mut polygons: Vec<Geometry> = vec![];
688        for p in &self.polygons {
689            let g = Polygon::to_geos_impl(p)?;
690            polygons.push(g);
691        }
692        Geometry::create_multipolygon(polygons).map_err(MyError::Geos)
693    }
694}
695
696impl Polygons {
697    pub(crate) fn from(polygons: Vec<Vec<Vec<Vec<f64>>>>) -> Self {
698        let polygons = polygons
699            .iter()
700            .map(|x| Polygon::ensure_precision_impl(x))
701            .collect();
702        Polygons { polygons }
703    }
704
705    pub(crate) fn coords_as_txt(polygons: &[Vec<Vec<Vec<f64>>>]) -> String {
706        Self::coords_with_dp(polygons, config().default_precision())
707    }
708
709    fn coords_with_dp(polygons: &[Vec<Vec<Vec<f64>>>], precision: usize) -> String {
710        let polygons: Vec<String> = polygons
711            .iter()
712            .map(|x| Polygon::coords_with_dp(x.as_slice(), precision))
713            .collect();
714        format!("({})", polygons.join(", "))
715    }
716}
717
718impl fmt::Display for Polygons {
719    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
720        write!(f, "POLYGONS (...)")
721    }
722}
723
724// ----- Geometries -----------------------------------------------------------
725
726/// Collection of mixed geometries.
727#[derive(Debug, Clone, PartialEq, PartialOrd)]
728pub struct Geometries {
729    items: Vec<G>,
730}
731
732impl GTrait for Geometries {
733    fn is_2d(&self) -> bool {
734        match &self.items[0] {
735            G::Point(x) => x.is_2d(),
736            G::Line(x) => x.is_2d(),
737            G::Polygon(x) => x.is_2d(),
738            G::Points(x) => x.is_2d(),
739            G::Lines(x) => x.is_2d(),
740            G::Polygons(x) => x.is_2d(),
741            _ => {
742                error!("Unexpected geometries item");
743                false
744            }
745        }
746    }
747
748    fn to_wkt_fmt(&self, precision: usize) -> String {
749        let items: Vec<String> = self
750            .items
751            .iter()
752            .map(|x| match x {
753                G::Point(x) => x.to_wkt_fmt(precision),
754                G::Line(x) => x.to_wkt_fmt(precision),
755                G::Polygon(x) => x.to_wkt_fmt(precision),
756                G::Points(x) => x.to_wkt_fmt(precision),
757                G::Lines(x) => x.to_wkt_fmt(precision),
758                G::Polygons(x) => x.to_wkt_fmt(precision),
759                G::BBox(x) => x.to_wkt_fmt(precision),
760                _ => panic!("Unexpected geometries item"),
761            })
762            .collect();
763        if self.is_2d() {
764            format!("GEOMETRYCOLLECTION ({})", items.join(", "))
765        } else {
766            format!("GEOMETRYCOLLECTION Z ({})", items.join(", "))
767        }
768    }
769
770    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
771        if self.items.iter().all(|x| match x {
772            G::Point(x) => x.check_coordinates(crs).is_ok(),
773            G::Line(x) => x.check_coordinates(crs).is_ok(),
774            G::Polygon(x) => x.check_coordinates(crs).is_ok(),
775            G::Points(x) => x.check_coordinates(crs).is_ok(),
776            G::Lines(x) => x.check_coordinates(crs).is_ok(),
777            G::Polygons(x) => x.check_coordinates(crs).is_ok(),
778            _ => {
779                error!("Unexpected geometries item");
780                false
781            }
782        }) {
783            Ok(())
784        } else {
785            Err(MyError::Runtime(
786                "At least one geometry has invalid coordinates".into(),
787            ))
788        }
789    }
790
791    fn to_geos(&self) -> Result<Geometry, MyError> {
792        let items: Result<Vec<Geometry>, MyError> = self
793            .items
794            .iter()
795            .map(|x| match x {
796                G::Point(x) => x.to_geos(),
797                G::Line(x) => x.to_geos(),
798                G::Polygon(x) => x.to_geos(),
799                G::Points(x) => x.to_geos(),
800                G::Lines(x) => x.to_geos(),
801                G::Polygons(x) => x.to_geos(),
802                _ => panic!("Unexpected geometries item"),
803            })
804            .collect();
805        Geometry::create_geometry_collection(items?).map_err(MyError::Geos)
806    }
807}
808
809impl Geometries {
810    pub(crate) fn from(items: Vec<G>) -> Self {
811        Geometries { items }
812    }
813}
814
815impl fmt::Display for Geometries {
816    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
817        write!(f, "GEOMETRIES (...)")
818    }
819}
820
821// ----- BBox -----------------------------------------------------------------
822
823/// 2D or 3D bounding box.
824#[derive(Debug, Clone, PartialEq, PartialOrd)]
825pub struct BBox {
826    w: f64,             // west bound longitude
827    s: f64,             // south bound latitude
828    z_min: Option<f64>, // minimum elevation
829    e: f64,             // east bound longitude
830    n: f64,             // north bound latitude
831    z_max: Option<f64>, // maximum elevation
832}
833
834impl GTrait for BBox {
835    fn is_2d(&self) -> bool {
836        self.z_min.is_none()
837    }
838
839    fn to_wkt_fmt(&self, precision: usize) -> String {
840        if self.z_min.is_none() {
841            format!(
842                "BBOX ({:.4$}, {:.4$}, {:.4$}, {:.4$})",
843                self.w, self.s, self.e, self.n, precision
844            )
845        } else {
846            format!(
847                "BBOX ({:.6$}, {:.6$}, {:.6$}, {:.6$}, {:.6$}, {:.6$})",
848                self.w,
849                self.s,
850                self.z_min.unwrap(),
851                self.e,
852                self.n,
853                self.z_max.unwrap(),
854                precision
855            )
856        }
857    }
858
859    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
860        crs.check_point([self.w, self.s].as_ref())?;
861        crs.check_point([self.e, self.n].as_ref())?;
862        Ok(())
863    }
864
865    fn to_geos(&self) -> Result<Geometry, MyError> {
866        self.to_geos_impl()
867    }
868}
869
870impl BBox {
871    /// (from [1]) If the vertical axis is included, the third and the sixth
872    /// number are the bottom and the top of the 3-dimensional bounding box.
873    ///
874    /// [1]: https://docs.ogc.org/is/21-065r2/21-065r2.html#basic-spatial-data-types
875    pub(crate) fn from(xy: Vec<f64>) -> Self {
876        if xy.len() == 4 {
877            BBox {
878                w: ensure_precision(&xy[0]),
879                s: ensure_precision(&xy[1]),
880                z_min: None,
881                e: ensure_precision(&xy[2]),
882                n: ensure_precision(&xy[3]),
883                z_max: None,
884            }
885        } else {
886            // panics if not 6-element long...
887            BBox {
888                w: ensure_precision(&xy[0]),
889                s: ensure_precision(&xy[1]),
890                z_min: Some(ensure_precision(&xy[2])),
891                e: ensure_precision(&xy[3]),
892                n: ensure_precision(&xy[4]),
893                z_max: Some(ensure_precision(&xy[5])),
894            }
895        }
896    }
897
898    // Convert this to one 2D polygon, or in the case of a box that spans the
899    // antimeridian, a 2D multi-polygon.
900    fn to_geos_impl(&self) -> Result<Geometry, MyError> {
901        let x1 = self.w;
902        let y1 = self.s;
903        let x2 = self.e;
904        let y2 = self.n;
905
906        // if x_min is larger than x_max, then the box spans the antimeridian...
907        if x1 < x2 {
908            let cs =
909                CoordSeq::new_from_vec(&[&[x1, y1], &[x2, y1], &[x2, y2], &[x1, y2], &[x1, y1]])
910                    .map_err(|x| {
911                        error!("Failed creating BBOX outer ring coordinates: {x}");
912                        MyError::Geos(x)
913                    })?;
914
915            let outer = Geometry::create_linear_ring(cs).map_err(|x| {
916                error!("Failed creating BBOX outer ring: {x}");
917                MyError::Geos(x)
918            })?;
919
920            Geometry::create_polygon(outer, vec![]).map_err(|x| {
921                error!("Failed creating BBOX polygon: {x}");
922                MyError::Geos(x)
923            })
924        } else {
925            let cs1 = CoordSeq::new_from_vec(&[
926                &[x1, y1],
927                &[180.0, y1],
928                &[180.0, y2],
929                &[x1, y2],
930                &[x1, y1],
931            ])
932            .map_err(|x| {
933                error!("Failed creating BBOX 1st outer ring coordinates: {x}");
934                MyError::Geos(x)
935            })?;
936
937            let cs2 = CoordSeq::new_from_vec(&[
938                &[x2, y1],
939                &[x2, y2],
940                &[-180.0, y2],
941                &[-180.0, y1],
942                &[x2, y1],
943            ])
944            .map_err(|x| {
945                error!("Failed creating BBOX 2nd outer ring coordinates: {x}");
946                MyError::Geos(x)
947            })?;
948
949            let outer1 = Geometry::create_linear_ring(cs1).map_err(|x| {
950                error!("Failed creating BBOX 1st outer ring: {x}");
951                MyError::Geos(x)
952            })?;
953
954            let outer2 = Geometry::create_linear_ring(cs2).map_err(|x| {
955                error!("Failed creating BBOX 2nd outer ring: {x}");
956                MyError::Geos(x)
957            })?;
958
959            let p1 = Geometry::create_polygon(outer1, vec![]).map_err(|x| {
960                error!("Failed creating BBOX 1st polygon: {x}");
961                MyError::Geos(x)
962            })?;
963            let p2 = Geometry::create_polygon(outer2, vec![]).map_err(|x| {
964                error!("Failed creating BBOX 1st polygon: {x}");
965                MyError::Geos(x)
966            })?;
967
968            Geometry::create_multipolygon(vec![p1, p2]).map_err(|x| {
969                error!("Failed creating BBOX multi-polygon: {x}");
970                MyError::Geos(x)
971            })
972        }
973    }
974
975    // Return the west bound longitude coordinate of this.
976    #[cfg(test)]
977    fn west(&self) -> f64 {
978        self.w
979    }
980
981    // Return the east bound longitude coordinate of this.
982    #[cfg(test)]
983    fn east(&self) -> f64 {
984        self.e
985    }
986
987    // Return the south bound latitude coordinate of this.
988    #[cfg(test)]
989    fn south(&self) -> f64 {
990        self.s
991    }
992
993    // Return the north bound latitude coordinate of this.
994    #[cfg(test)]
995    fn north(&self) -> f64 {
996        self.n
997    }
998
999    // Return the lowest (minimum) elevation of this.
1000    #[cfg(test)]
1001    fn z_min(&self) -> Option<f64> {
1002        self.z_min
1003    }
1004
1005    // Return the highest (maximum) elevation of this.
1006    #[cfg(test)]
1007    fn z_max(&self) -> Option<f64> {
1008        self.z_max
1009    }
1010}
1011
1012impl fmt::Display for BBox {
1013    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014        write!(f, "BBox (...)")
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021    use crate::{expr::E, text::cql2};
1022    use approx::assert_relative_eq;
1023    use geos::Geom;
1024    use std::error::Error;
1025
1026    const TOLERANCE: f64 = 1.0E-3;
1027
1028    #[test]
1029    fn test_pt_equality() {
1030        let p1 = Point {
1031            coord: vec![1., 1.],
1032        };
1033        let p2 = Point {
1034            coord: vec![1.0, 1.0],
1035        };
1036        let p3 = Point {
1037            coord: vec![1.0, 1.1],
1038        };
1039        let p4 = Point {
1040            coord: vec![1.1, 1.0],
1041        };
1042
1043        assert_eq!(p1, p2);
1044        assert_ne!(p1, p3);
1045        assert_ne!(p1, p4);
1046        assert!(p1 == p2);
1047        assert!(p1 != p3);
1048        assert!(p2 != p4);
1049        assert!(p3 != p4);
1050    }
1051
1052    #[test]
1053    fn test_pt_comparison() {
1054        let p1 = Point {
1055            coord: vec![1.0, 1.0],
1056        };
1057        let p2 = Point {
1058            coord: vec![1.0, 1.1],
1059        };
1060        let p3 = Point {
1061            coord: vec![1.1, 1.0],
1062        };
1063
1064        assert!(p1 < p2);
1065        assert!(p1 < p3);
1066        assert!(p2 < p3);
1067    }
1068
1069    #[test]
1070    // #[tracing_test::traced_test]
1071    fn test_point() {
1072        const G: &str = r#"point (-3.508362 -1.754181)"#;
1073
1074        let exp = cql2::geom_expression(G);
1075        assert!(exp.is_ok());
1076        // tracing::debug!("exp = {:?}", exp);
1077        let spa = exp.unwrap();
1078        let g = match spa {
1079            E::Spatial(G::Point(x)) => x,
1080            _ => panic!("Not a Point..."),
1081        };
1082        assert_eq!(g.is_2d(), true);
1083        // or...
1084        assert!(g.z().is_none());
1085        assert_relative_eq!(g.x(), -3.508, epsilon = TOLERANCE);
1086        assert_relative_eq!(g.y(), -1.754, epsilon = TOLERANCE);
1087    }
1088
1089    #[test]
1090    // #[tracing_test::traced_test]
1091    fn test_line() {
1092        const G: &str = r#"LineString(43.72992 -79.2998, 43.73005 -79.2991, 43.73006 -79.2984,
1093                   43.73140 -79.2956, 43.73259 -79.2950, 43.73266 -79.2945,
1094                   43.73320 -79.2936, 43.73378 -79.2936, 43.73486 -79.2917)"#;
1095
1096        let exp = cql2::geom_expression(G);
1097        assert!(exp.is_ok());
1098        // tracing::debug!("exp = {:?}", exp);
1099        let spa = exp.unwrap();
1100        let g = match spa {
1101            E::Spatial(G::Line(x)) => x,
1102            _ => panic!("Not a Line..."),
1103        };
1104        assert_eq!(g.is_2d(), true);
1105        assert_eq!(g.size(), 9);
1106
1107        assert!(g.first().is_some());
1108        let first = g.first().unwrap();
1109        assert_relative_eq!(first[0], 43.729, epsilon = TOLERANCE);
1110        assert_relative_eq!(first[1], -79.299, epsilon = TOLERANCE);
1111        assert!(g.last().is_some());
1112        let last = g.last().unwrap();
1113        assert_relative_eq!(last[0], 43.734, epsilon = TOLERANCE);
1114        assert_relative_eq!(last[1], -79.291, epsilon = TOLERANCE);
1115    }
1116
1117    #[test]
1118    // #[tracing_test::traced_test]
1119    fn test_polygon2d() {
1120        const G: &str = r#"PolyGon ((-0.333333 89.0, -102.723546 -0.5, -179.0 -89.0, -1.9 89.0, -0.0 89.0, 2.00001 -1.9, -0.333333 89.0))"#;
1121
1122        let exp = cql2::geom_expression(G);
1123        assert!(exp.is_ok());
1124        // tracing::debug!("exp = {:?}", exp);
1125        let spa = exp.unwrap();
1126        let g = match spa {
1127            E::Spatial(G::Polygon(x)) => x,
1128            _ => panic!("Not a Polygon..."),
1129        };
1130        assert_eq!(g.is_2d(), true);
1131
1132        let outer_ring = g.outer_as_ring();
1133        assert!(outer_ring.is_ring());
1134        assert!(outer_ring.is_closed());
1135        assert_eq!(outer_ring.size(), 7);
1136
1137        // has no holes...
1138        assert!(!g.has_holes());
1139        assert!(g.inners().is_empty());
1140    }
1141
1142    #[test]
1143    // #[tracing_test::traced_test]
1144    fn test_polygon3d() {
1145        const G: &str = r#"POLYGON Z ((-49.88024 0.5 -75993.341684, -1.5 -0.99999 -100000.0, 0.0 0.5 -0.333333, -49.88024 0.5 -75993.341684), (-65.887123 2.00001 -100000.0, 0.333333 -53.017711 -79471.332949, 180.0 0.0 1852.616704, -65.887123 2.00001 -100000.0))"#;
1146
1147        let exp = cql2::geom_expression(G);
1148        assert!(exp.is_ok());
1149        // tracing::debug!("exp = {:?}", exp);
1150        let spa = exp.unwrap();
1151        let g = match spa {
1152            E::Spatial(G::Polygon(x)) => x,
1153            _ => panic!("Not a Polygon..."),
1154        };
1155        assert_eq!(g.is_2d(), false);
1156
1157        let outer_ring = g.outer_as_ring();
1158        assert!(outer_ring.is_ring());
1159        assert!(outer_ring.is_closed());
1160        assert_eq!(outer_ring.size(), 4);
1161
1162        // has 1 hole...
1163        assert!(g.has_holes());
1164        assert_eq!(g.inners().len(), 1);
1165    }
1166
1167    #[test]
1168    // #[tracing_test::traced_test]
1169    fn test_multiline() {
1170        const G: &str = r#"MultiLineString(
1171        ( -1.9 -0.99999, 75.292574 1.5,     -0.5 -4.016458, -31.708594 -74.743801, 179.0 -90.0 ),
1172        (-1.9 -1.1,      1.5      8.547371))"#;
1173
1174        let exp = cql2::geom_expression(G);
1175        // tracing::debug!("exp: {:?}", exp);
1176        assert!(exp.is_ok());
1177        let spa = exp.unwrap();
1178        let g = match spa {
1179            E::Spatial(G::Lines(x)) => x,
1180            _ => panic!("Not a Polygon..."),
1181        };
1182        assert_eq!(g.is_2d(), true);
1183        let lines = g.lines();
1184        assert_eq!(lines.len(), 2);
1185        // or...
1186        assert_eq!(lines.len(), g.size());
1187    }
1188
1189    #[test]
1190    #[tracing_test::traced_test]
1191    fn test_bbox() {
1192        const G1: &str = "bbox(-128.098193, -1.1, -99999.0, 180.0, 90.0, 100000.0)";
1193        const G2: &str = "bbox(-128.098193,-1.1, -99999.0,180.0 , \t90.0, \n 100000.0)";
1194
1195        let x = cql2::geom_expression(G1);
1196        // tracing::debug!("x = {:?}", x);
1197        assert!(x.is_ok());
1198        let g = x.unwrap();
1199        assert!(matches!(g, E::Spatial(G::BBox(_))));
1200        let bbox1 = match g {
1201            E::Spatial(G::BBox(x)) => x,
1202            _ => panic!("Not a BBox"),
1203        };
1204        assert!(!bbox1.is_2d());
1205
1206        // should also succeed when coordinate sequence contains no, or other
1207        // sorts of whitespaces wherever they're placed...
1208        let x = cql2::geom_expression(G2);
1209        // tracing::debug!("x = {:?}", x);
1210        assert!(x.is_ok());
1211        let g = x.unwrap();
1212        assert!(matches!(g, E::Spatial(G::BBox(_))));
1213        let bbox2 = match g {
1214            E::Spatial(G::BBox(x)) => x,
1215            _ => panic!("Not a BBox"),
1216        };
1217        assert!(!bbox2.is_2d());
1218
1219        assert_eq!(bbox1.west(), bbox2.west());
1220        assert_eq!(bbox1.east(), bbox2.east());
1221        assert_eq!(bbox1.south(), bbox2.south());
1222        assert_eq!(bbox1.north(), bbox2.north());
1223        assert_eq!(bbox1.z_min(), bbox2.z_min());
1224        assert_eq!(bbox1.z_max(), bbox2.z_max());
1225    }
1226
1227    #[test]
1228    // #[tracing_test::traced_test]
1229    fn test_bbox_to_polygon() {
1230        const G1: &str = "BBOX(-180,-90,180,90)";
1231        const WKT: &str = "POLYGON ((-180 -90, 180 -90, 180 90, -180 90, -180 -90))";
1232        const G2: &str = "bbox(-180.0,-90.,-99999.0,180.0,90.0,100000.0)";
1233
1234        let x1 = cql2::geom_expression(G1);
1235        // tracing::debug!("x1 = {:?}", x1);
1236        assert!(x1.is_ok());
1237        let g1 = x1.unwrap();
1238        assert!(matches!(g1, E::Spatial(G::BBox(_))));
1239        let bbox1 = match g1 {
1240            E::Spatial(G::BBox(x)) => x,
1241            _ => panic!("Not a BBox"),
1242        };
1243        assert!(bbox1.is_2d());
1244        let g1 = bbox1.to_geos();
1245        assert!(g1.is_ok());
1246        let g1 = g1.unwrap();
1247        // tracing::debug!("g1 = {:?}", g1.to_wkt());
1248        let wkt1 = g1.to_wkt().unwrap();
1249        assert_eq!(wkt1, WKT);
1250
1251        let x2 = cql2::geom_expression(G2);
1252        // tracing::debug!("x2 = {:?}", x2);
1253        assert!(x2.is_ok());
1254        let g2 = x2.unwrap();
1255        assert!(matches!(g2, E::Spatial(G::BBox(_))));
1256        let bbox2 = match g2 {
1257            E::Spatial(G::BBox(x)) => x,
1258            _ => panic!("Not a BBox"),
1259        };
1260        assert!(!bbox2.is_2d());
1261        let g2 = bbox2.to_geos();
1262        assert!(g2.is_ok());
1263        let g2 = g2.unwrap();
1264        // tracing::debug!("g2 = {:?}", g2.to_wkt());
1265        let wkt2 = g2.to_wkt().unwrap();
1266        assert_eq!(wkt2, WKT);
1267    }
1268
1269    #[test]
1270    // #[tracing_test::traced_test]
1271    fn test_to_wkt() {
1272        const G: &str = r#"Polygon Z (
1273        (
1274            -49.88024    0.5      -75993.341684, 
1275             -1.5       -0.99999 -100000.0, 
1276              0.0        0.5          -0.333333, 
1277            -49.88024    0.5      -75993.341684
1278        ), (
1279            -65.887123   2.00001 -100000.0,
1280              0.333333 -53.017711 -79471.332949,
1281            180.0        0.0        1852.616704,
1282            -65.887123   2.00001 -100000.0
1283        ))"#;
1284        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))";
1285
1286        let exp = cql2::geom_expression(G);
1287        // tracing::debug!("exp = {:?}", exp);
1288        let spa = exp.expect("Failed parsing Polygon WKT");
1289        let g = match spa {
1290            E::Spatial(G::Polygon(x)) => x,
1291            _ => panic!("Not a Polygon..."),
1292        };
1293        // should be a 3D polygon...
1294        assert_eq!(g.is_2d(), false);
1295
1296        let wkt = g.to_wkt();
1297        // tracing::debug!("wkt = {}", wkt);
1298        assert_eq!(WKT, wkt);
1299    }
1300
1301    #[test]
1302    // #[tracing_test::traced_test]
1303    fn test_to_geos() -> Result<(), Box<dyn Error>> {
1304        let g = G::try_from_wkt("POINT(17.03 45.87)")?;
1305        // tracing::debug!("g = {g:?}");
1306        assert!(matches!(g, G::Point(_)));
1307        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1308        let gg = g.to_geos()?;
1309        assert_eq!(gg.get_type()?, "Point");
1310        assert_eq!(gg.get_x()?, 17.03);
1311        assert_eq!(gg.get_y()?, 45.87);
1312        assert!(!gg.has_z()?);
1313
1314        let g = G::try_from_wkt("LINESTRING(-49.85 0.5, -1.5 -0.999, 0.0 0.5, -49.88 0.5)")?;
1315        // tracing::debug!("g = {g:?}");
1316        assert!(matches!(g, G::Line(_)));
1317        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1318        let gg = g.to_geos()?;
1319        assert_eq!(gg.get_type()?, "LineString");
1320        assert_eq!(gg.get_num_points()?, 4);
1321        assert_eq!(gg.get_start_point()?.get_x()?, -49.85);
1322        assert_eq!(gg.get_end_point()?.get_y()?, 0.5);
1323
1324        let g = G::try_from_wkt(
1325            r#"PolyGon ((
1326            -0.333333   89.0, 
1327            -102.723546 -0.5, 
1328            -179.0     -89.0, 
1329            -1.9        89.0, 
1330            -0.0        89.0, 
1331            2.00001     -1.9, 
1332            -0.333333   89.0))"#,
1333        )?;
1334        // tracing::debug!("g = {g:?}");
1335        assert!(matches!(g, G::Polygon(_)));
1336        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1337        let gg = g.to_geos()?;
1338        assert_eq!(gg.get_type()?, "Polygon");
1339        assert_eq!(gg.get_num_interior_rings()?, 0);
1340        assert_eq!(gg.get_exterior_ring()?.get_num_coordinates()?, 7);
1341
1342        // multi-stuff
1343
1344        let g = G::try_from_wkt("MULTIPOINT(17.03 45.87, -0.33 89.02)")?;
1345        // tracing::debug!("g = {g:?}");
1346        assert!(matches!(g, G::Points(_)));
1347        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1348        let gg = g.to_geos()?;
1349        assert_eq!(gg.get_type()?, "MultiPoint");
1350        assert_eq!(gg.get_num_geometries()?, 2);
1351        assert_eq!(gg.get_geometry_n(0)?.get_x()?, 17.03);
1352        assert_eq!(gg.get_geometry_n(1)?.get_y()?, 89.02);
1353
1354        let g = G::try_from_wkt(
1355            r#"MULTILINESTRING(
1356            (-49.85 0.5, -1.5 -0.999, 0.0 0.5), 
1357            (34.3 3.2, 0.1 0.2))"#,
1358        )?;
1359        // tracing::debug!("g = {g:?}");
1360        assert!(matches!(g, G::Lines(_)));
1361        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1362        let gg = g.to_geos()?;
1363        assert_eq!(gg.get_type()?, "MultiLineString");
1364        assert_eq!(gg.get_num_geometries()?, 2);
1365        assert_eq!(gg.get_geometry_n(0)?.get_start_point()?.get_x()?, -49.85);
1366        assert_eq!(gg.get_geometry_n(1)?.get_end_point()?.get_y()?, 0.2);
1367
1368        let g = G::try_from_wkt(
1369            r#"MULTIPOLYGON (
1370            ((
1371                180.0 -16.0671326636424,
1372                180.0 -16.5552165666392,
1373                179.364142661964 -16.8013540769469,
1374                178.725059362997 -17.012041674368,
1375                178.596838595117 -16.63915,
1376                179.096609362997 -16.4339842775474,
1377                179.413509362997 -16.3790542775474,
1378                180.0 -16.0671326636424
1379            )),((
1380                178.12557 -17.50481,
1381                178.3736 -17.33992,
1382                178.71806 -17.62846,
1383                178.55271 -18.15059,
1384                177.93266 -18.28799,
1385                177.38146 -18.16432,
1386                177.28504 -17.72465,
1387                177.67087 -17.38114,
1388                178.12557 -17.50481
1389            )),((
1390                -179.793320109049 -16.0208822567412,
1391                -179.917369384765 -16.5017831356494,
1392                -180 -16.5552165666392,
1393                -180 -16.0671326636424,
1394                -179.793320109049 -16.0208822567412
1395            ))
1396        )"#,
1397        )?;
1398        // tracing::debug!("g = {g:?}");
1399        assert!(matches!(g, G::Polygons(_)));
1400        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1401        let gg = g.to_geos()?;
1402        assert_eq!(gg.get_type()?, "MultiPolygon");
1403        assert_eq!(gg.get_num_geometries()?, 3);
1404        let p1 = gg.get_geometry_n(0)?;
1405        assert_eq!(p1.get_type()?, "Polygon");
1406        assert_eq!(p1.get_exterior_ring()?.get_num_coordinates()?, 8);
1407        assert_eq!(p1.get_num_interior_rings()?, 0);
1408
1409        let g = G::try_from_wkt(
1410            r#"GEOMETRYCOLLECTION(
1411            POINT(17.03 45.87), 
1412            LINESTRING(-49.85 0.5, -1.5 -0.999, 0.0 0.5, -49.88 0.5)
1413        )"#,
1414        )?;
1415        // tracing::debug!("g = {g:?}");
1416        assert!(matches!(g, G::Vec(_)));
1417        // tracing::debug!("g (wkt) = {}", g.to_wkt());
1418        let gg = g.to_geos()?;
1419        assert_eq!(gg.get_type()?, "GeometryCollection");
1420        assert_eq!(gg.get_num_geometries()?, 2);
1421        Ok(())
1422    }
1423
1424    #[test]
1425    // #[tracing_test::traced_test]
1426    fn test_geos() -> Result<(), Box<dyn Error>> {
1427        const G: &str = r#"MultiLineString(
1428        (-49.85 0.5, -1.5   -0.999,  0.0 0.5, -49.88 0.5 ),
1429        (-65.87 2.01, 0.33 -53.07, 180.0 0)
1430        )"#;
1431
1432        let exp = cql2::geom_expression(G);
1433        // tracing::debug!("exp = {:?}", exp);
1434        let spa = exp.expect("Failed parsing Polygon WKT");
1435        let g = match spa {
1436            E::Spatial(G::Lines(x)) => x,
1437            _ => panic!("Not a Lines..."),
1438        };
1439        assert_eq!(g.is_2d(), true);
1440        assert_eq!(g.size(), 2);
1441
1442        let geos = g.to_geos().expect("Failed converting to GEOS geometry");
1443        assert_eq!(geos.get_num_geometries()?, g.size());
1444        let l1 = geos.get_geometry_n(0)?;
1445        assert_eq!(l1.get_num_coordinates()?, 4);
1446        let l2 = geos.get_geometry_n(1)?;
1447        assert_eq!(l2.get_num_coordinates()?, 3);
1448
1449        Ok(())
1450    }
1451
1452    #[test]
1453    // #[tracing_test::traced_test]
1454    fn test_new_from_wkt() -> Result<(), Box<dyn Error>> {
1455        const PT: &str = "POINT (-46.03556 -7.5325)";
1456        const LS: &str = "LINESTRING (-180 -45, 0 -45)";
1457        const P: &str = "POLYGON ((-180 -90, -90 -90, -90 90, -180 90, -180 -90), (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50))";
1458        const MPT: &str = "MULTIPOINT ((7.02 49.92), (90 180))";
1459        const MLS: &str = "MULTILINESTRING ((-180 -45, 0 -45), (0 45, 180 45))";
1460        const MP: &str = r#"MULTIPOLYGON(
1461            ((-180 -90, -90 -90, -90 90, -180 90, -180 -90),
1462             (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)),
1463            ((0 0, 10 0, 10 10, 0 10, 0 0))
1464        )"#;
1465        const MG: &str = r#"GEOMETRYCOLLECTION(
1466            POINT(7.02 49.92),
1467            POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))
1468        )"#;
1469
1470        let pt = Geometry::new_from_wkt(PT);
1471        assert!(pt.is_ok());
1472        assert_eq!(pt?.to_wkt()?, PT);
1473
1474        let ls = Geometry::new_from_wkt(LS);
1475        assert!(ls.is_ok());
1476        // tracing::debug!("ls = {}", ls?.to_wkt()?);
1477        assert_eq!(ls?.to_wkt()?, LS);
1478
1479        let poly = Geometry::new_from_wkt(P);
1480        assert!(poly.is_ok());
1481        // tracing::debug!("poly = {}", poly?.to_wkt()?);
1482        assert_eq!(poly?.to_wkt()?, P);
1483
1484        let points = Geometry::new_from_wkt(MPT);
1485        assert!(points.is_ok());
1486        // tracing::debug!("points = {}", points?.to_wkt()?);
1487        assert_eq!(points?.to_wkt()?, MPT);
1488
1489        let lines = Geometry::new_from_wkt(MLS);
1490        assert!(lines.is_ok());
1491        // tracing::debug!("lines = {}", lines?.to_wkt()?);
1492        assert_eq!(lines?.to_wkt()?, MLS);
1493
1494        let polys = Geometry::new_from_wkt(MP);
1495        assert!(polys.is_ok());
1496        // tracing::debug!("polys = {}", polys?.to_wkt()?);
1497        // tracing::debug!("polys#type = {}", polys?.get_type()?);
1498        assert_eq!(polys?.get_type()?, "MultiPolygon");
1499
1500        let geometries = Geometry::new_from_wkt(MG);
1501        assert!(geometries.is_ok());
1502        // tracing::debug!("geometries = {}", geometries?.to_wkt()?);
1503        // tracing::debug!("geometries#type = {}", geometries?.get_type()?);
1504        assert_eq!(geometries?.get_type()?, "GeometryCollection");
1505
1506        Ok(())
1507    }
1508
1509    #[test]
1510    // #[tracing_test::traced_test]
1511    fn test_point_in_polygon() -> Result<(), Box<dyn Error>> {
1512        const WKT1: &str = "POINT(-46.03556 -7.5325)";
1513        const WKT2: &str =
1514            "POLYGON((-65.887123 2.00001, 0.333333 -53.017711, 180.0 0.0, -65.887123 2.00001))";
1515
1516        let pt = Geometry::new_from_wkt(WKT1).expect("Failed parsing point");
1517        let polygon = Geometry::new_from_wkt(WKT2).expect("Failed parsing polygon");
1518
1519        pt.within(&polygon)?;
1520        // so is the inverse...
1521        polygon.contains(&pt)?;
1522
1523        Ok(())
1524    }
1525
1526    #[test]
1527    // #[tracing_test::traced_test]
1528    fn test_bbox_antimeridian() -> Result<(), Box<dyn Error>> {
1529        const WKT: &str = "MULTIPOLYGON (((150 -90, 180 -90, 180 90, 150 90, 150 -90)), ((-150 -90, -150 90, -180 90, -180 -90, -150 -90)))";
1530
1531        let bbox = BBox::from(vec![150.0, -90.0, -150.0, 90.0]);
1532        let mp = bbox.to_geos()?;
1533        assert_eq!(mp.get_type()?, "MultiPolygon");
1534
1535        let wkt = mp.to_wkt()?;
1536        assert_eq!(wkt, WKT);
1537
1538        let pt = Geometry::new_from_wkt("POINT(152 10)")?;
1539
1540        pt.within(&mp)?;
1541        mp.contains(&pt)?;
1542
1543        Ok(())
1544    }
1545
1546    #[test]
1547    #[should_panic]
1548    fn test_invalid_pt_xy() {
1549        let pt = Point::from(vec![90.0, 180.0]);
1550        let crs = CRS::default();
1551        pt.check_coordinates(&crs).unwrap();
1552    }
1553
1554    #[test]
1555    #[should_panic]
1556    fn test_invalid_line_xy() {
1557        let line = Line::from(vec![vec![0.0, 45.0], vec![90.0, 180.0], vec![45.0, 45.0]]);
1558        let crs = CRS::default();
1559        line.check_coordinates(&crs).unwrap();
1560    }
1561
1562    #[test]
1563    // #[tracing_test::traced_test]
1564    fn test_touches() -> Result<(), Box<dyn Error>> {
1565        const WKT1: &str = "POLYGON ((0 -90, 0 0, 180 0, 180 -90, 0 -90))";
1566        const WKT2: &str = "POLYGON ((-180 -90, -180 90, 180 90, 180 -90, -180 -90))";
1567
1568        let p1 = Geometry::new_from_wkt(WKT1)?;
1569        let p2 = Geometry::new_from_wkt(WKT2)?;
1570
1571        // although p1 and p2 share a segment of their bottom side, their
1572        // interiors are NOT disjoint and as such they are considered to
1573        // "touch" each other.
1574        assert!(!p1.touches(&p2)?);
1575
1576        Ok(())
1577    }
1578
1579    #[test]
1580    // #[tracing_test::traced_test]
1581    fn test_point_precision() -> Result<(), Box<dyn Error>> {
1582        const XYZ: [f64; 3] = [-16.0671326636424, -17.012041674368, 179.096609362997];
1583        const WKT: &str = "POINT Z (-16.067133 -17.012042 179.096609)";
1584
1585        let pt = Point::from(XYZ.to_vec());
1586        // tracing::debug!("pt = {pt:?}");
1587        let wkt = pt.to_wkt();
1588        // tracing::debug!("wkt = {wkt}");
1589        assert_eq!(wkt, WKT);
1590
1591        Ok(())
1592    }
1593
1594    #[test]
1595    // #[tracing_test::traced_test]
1596    fn test_line_precision() -> Result<(), Box<dyn Error>> {
1597        const WKT: &str = "LINESTRING (82.400480 30.411477, 82.722734 30.365046)";
1598
1599        let line_xy = vec![
1600            vec![82.400479770847, 30.4114773625851],
1601            vec![82.7227340026191, 30.3650460881709],
1602        ];
1603
1604        let line = Line::from(line_xy);
1605        // tracing::debug!("line = {line:?}");
1606        let wkt = line.to_wkt();
1607        // tracing::debug!("wkt = {wkt}");
1608        assert_eq!(wkt, WKT);
1609
1610        Ok(())
1611    }
1612
1613    #[test]
1614    // #[tracing_test::traced_test]
1615    fn test_bbox_precision() -> Result<(), Box<dyn Error>> {
1616        // const WKT: &str = "POLYGON ((6.043073 50.128052, 6.242751 50.128052, 6.242751 49.902226, 6.043073 49.902226, 6.043073 50.128052))";
1617        const WKT: &str = "BBOX (6.043073, 50.128052, 6.242751, 49.902226)";
1618
1619        let bbox_xy = vec![
1620            6.043073357781111,
1621            50.128051662794235,
1622            6.242751092156993,
1623            49.90222565367873,
1624        ];
1625
1626        let bbox = BBox::from(bbox_xy);
1627        // tracing::debug!("bbox = {bbox:?}");
1628        let wkt = bbox.to_wkt();
1629        // tracing::debug!("wkt = {wkt}");
1630        assert_eq!(wkt, WKT);
1631
1632        Ok(())
1633    }
1634
1635    #[test]
1636    fn test_try_from_wkt() -> Result<(), Box<dyn Error>> {
1637        // Test Vector triplet consisting of (a) a test vector input, (b) expected
1638        // WKT output, and (c) number of decimal digits in fraction to use.
1639        #[rustfmt::skip]
1640        const TV: [(&str, &str, usize); 8] = [
1641            (
1642                "POINT(-46.035560 -7.532500)",
1643                "POINT (-46.03556 -7.53250)",
1644                5
1645            ), (
1646                "LINESTRING   (-180 -45,   0 -45)",
1647                "LINESTRING (-180.0 -45.0, 0.0 -45.0)", 
1648                1
1649            ), (
1650                r#"POLYGON (
1651                    (-180 -90, -90 -90, -90 90, -180 90, -180 -90),
1652                    (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)
1653                )"#,
1654                "POLYGON ((-180 -90, -90 -90, -90 90, -180 90, -180 -90), (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50))",
1655                0
1656            ), (
1657                "MULTIPOINT ((7.02 49.92), (90 180))",
1658                "MULTIPOINT (7.02 49.92, 90.00 180.00)",
1659                2
1660            ), (
1661                "MULTILINESTRING ((-180 -45, 0 -45), (0 45, 180 45))",
1662                "MULTILINESTRING ((-180.0 -45.0, 0.0 -45.0), (0.0 45.0, 180.0 45.0))",
1663                1
1664            ), (
1665                r#"MULTIPOLYGON((
1666                    (-180 -90, -90 -90, -90 90, -180 90, -180 -90),
1667                    (-120 -50, -100 -50, -100 -40, -120 -40, -120 -50)
1668                ), (
1669                    (0 0, 10 0, 10 10, 0 10, 0 0)
1670                ))"#,
1671                "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)))",
1672                0
1673            ), (
1674                "GEOMETRYCOLLECTION(POINT(7.02 49.92),POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)))",
1675                "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)))",
1676                1
1677            ), (
1678                "BBOX(51.43,2.54,55.77,6.40)",
1679                "BBOX (51.43, 2.54, 55.77, 6.40)",
1680                2
1681            ),
1682        ];
1683
1684        for (ndx, (wkt, expected, precision)) in TV.iter().enumerate() {
1685            if let Ok(g) = G::try_from_wkt(wkt) {
1686                let actual = g.to_wkt_fmt(*precision);
1687                assert_eq!(actual, *expected);
1688            } else {
1689                panic!("Failed parsing WKT at index #{ndx}")
1690            }
1691        }
1692
1693        Ok(())
1694    }
1695}