Skip to main content

ogc_cql2/geom/
line.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Line geometry.
6//!
7
8use crate::{
9    CRS, GTrait, MyError, Point,
10    config::config,
11    geom::{XY1V, XY2V},
12    srid::SRID,
13};
14use core::fmt;
15use geos::{ConstGeometry, CoordDimensions, CoordSeq, Geom, Geometry};
16use std::slice::Iter;
17use tracing::{error, warn};
18
19/// 2D or 3D line-string geometry.
20#[derive(Debug, Clone, PartialEq, PartialOrd)]
21pub struct Line {
22    coord: XY2V,
23    srid: SRID,
24}
25
26impl GTrait for Line {
27    fn is_2d(&self) -> bool {
28        self.coord[0].len() == 2
29    }
30
31    fn to_wkt_fmt(&self, precision: usize) -> String {
32        if self.is_2d() {
33            format!(
34                "LINESTRING {}",
35                Self::coords_with_dp(&self.coord, precision)
36            )
37        } else {
38            format!(
39                "LINESTRING Z {}",
40                Self::coords_with_dp(&self.coord, precision)
41            )
42        }
43    }
44
45    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
46        crs.check_line(&self.coord)
47    }
48
49    fn type_(&self) -> &str {
50        "LineString"
51    }
52
53    fn srid(&self) -> SRID {
54        self.srid
55    }
56}
57
58impl Line {
59    /// Return the number of vertices/points in this.
60    pub fn num_points(&self) -> usize {
61        self.coord.len()
62    }
63
64    /// Return an iterator over the points' coordinates.
65    pub fn points(&self) -> Iter<'_, XY1V> {
66        self.coord.iter()
67    }
68
69    pub(crate) fn from_xy(coord: XY2V) -> Self {
70        Self::from_xy_and_srid(coord, *config().default_srid())
71    }
72
73    pub(crate) fn from_xy_and_srid(coord: XY2V, srid: SRID) -> Self {
74        let coord = Self::ensure_precision_xy(&coord);
75        Line { coord, srid }
76    }
77
78    pub(crate) fn coords_as_txt(coord: &[XY1V]) -> String {
79        Self::coords_with_dp(coord, config().default_precision())
80    }
81
82    pub(crate) fn ensure_precision_xy(coord: &[XY1V]) -> XY2V {
83        coord
84            .iter()
85            .map(|x| Point::ensure_precision_xy(x))
86            .collect()
87    }
88
89    pub(crate) fn coords_with_dp(coord: &[XY1V], precision: usize) -> String {
90        let points: Vec<String> = coord
91            .iter()
92            .map(|x| Point::coords_with_dp(x, precision))
93            .collect();
94        format!("({})", points.join(", "))
95    }
96
97    pub(crate) fn to_geos(&self) -> Result<Geometry, MyError> {
98        Self::to_geos_xy(&self.coord, &self.srid)
99    }
100
101    pub(crate) fn to_geos_xy(xy: &[XY1V], srid: &SRID) -> Result<Geometry, MyError> {
102        let vertices: Vec<&[f64]> = xy.iter().map(|x| x.as_slice()).collect();
103        let xy = CoordSeq::new_from_vec(&vertices)?;
104        let mut g = Geometry::create_line_string(xy)?;
105        let srs_id = srid.as_usize()?;
106        g.set_srid(srs_id);
107
108        Ok(g)
109    }
110
111    // Return TRUE if the first and last vertices coincide. FALSE otherwise.
112    pub(crate) fn is_closed(&self) -> bool {
113        self.coord.first() == self.coord.last()
114    }
115
116    pub(crate) fn from_geos_xy<T: Geom>(gg: T) -> Result<XY2V, MyError> {
117        let cs = gg.get_coord_seq()?;
118        let is_3d = match cs.dimensions()? {
119            CoordDimensions::OneD => {
120                let msg = "Don't know how to handle 1D points";
121                error!("Failed: {msg}");
122                return Err(MyError::Runtime(msg.into()));
123            }
124            CoordDimensions::TwoD => false,
125            CoordDimensions::ThreeD => true,
126        };
127        let num_vertices = cs.size()?;
128        let mut xy = Vec::with_capacity(num_vertices);
129        for ndx in 0..num_vertices {
130            let vertex = if is_3d {
131                vec![cs.get_x(ndx)?, cs.get_y(ndx)?, cs.get_z(ndx)?]
132            } else {
133                vec![cs.get_x(ndx)?, cs.get_y(ndx)?]
134            };
135            xy.push(vertex);
136        }
137        Ok(xy)
138    }
139
140    pub(crate) fn set_srid_unchecked(&mut self, srid: &SRID) {
141        if self.srid != *srid {
142            warn!("Replacing current SRID ({}) w/ {srid}", self.srid);
143            self.srid = srid.to_owned();
144        }
145    }
146
147    // Return TRUE if this cnsists of at least 4 points w/ the first and last
148    // ones coinciding. Return FALSE otherwise.
149    #[cfg(test)]
150    pub(crate) fn is_ring(&self) -> bool {
151        self.num_points() > 3 && self.is_closed()
152    }
153
154    #[cfg(test)]
155    pub(crate) fn first(&self) -> Option<&XY1V> {
156        self.coord.first()
157    }
158
159    #[cfg(test)]
160    pub(crate) fn last(&self) -> Option<&XY1V> {
161        self.coord.last()
162    }
163}
164
165impl fmt::Display for Line {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
167        if self.is_closed() {
168            write!(f, "Ring (...)")
169        } else {
170            write!(f, "Line (...)")
171        }
172    }
173}
174
175impl TryFrom<Geometry> for Line {
176    type Error = MyError;
177
178    fn try_from(value: Geometry) -> Result<Self, Self::Error> {
179        let srs_id = value.get_srid().unwrap_or_else(|x| {
180            error!("Failed get_srid for GEOS Line. Will use Undefined: {}", x);
181            Default::default()
182        });
183        let xy = Line::from_geos_xy(value)?;
184        let srid = SRID::try_from(srs_id)?;
185        Ok(Line::from_xy_and_srid(xy, srid))
186    }
187}
188
189impl TryFrom<ConstGeometry<'_>> for Line {
190    type Error = MyError;
191
192    fn try_from(value: ConstGeometry) -> Result<Self, Self::Error> {
193        let srs_id = value.get_srid().unwrap_or_else(|x| {
194            error!("Failed get_srid for GEOS Line. Will use Undefined: {}", x);
195            Default::default()
196        });
197        let xy = Line::from_geos_xy(value)?;
198        let srid = SRID::try_from(srs_id)?;
199        Ok(Line::from_xy_and_srid(xy, srid))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::{G, expr::E, text::cql2};
207    use approx::assert_relative_eq;
208    use std::error::Error;
209
210    const TOLERANCE: f64 = 1.0E-3;
211
212    #[test]
213    #[tracing_test::traced_test]
214    fn test() {
215        const G: &str = r#"LineString(43.72992 -79.2998, 43.73005 -79.2991, 43.73006 -79.2984,
216                   43.73140 -79.2956, 43.73259 -79.2950, 43.73266 -79.2945,
217                   43.73320 -79.2936, 43.73378 -79.2936, 43.73486 -79.2917)"#;
218
219        let exp = cql2::geom_expression(G);
220        assert!(exp.is_ok());
221        let spa = exp.unwrap();
222        let g = match spa {
223            E::Spatial(G::Line(x)) => x,
224            _ => panic!("Not a Line..."),
225        };
226        assert_eq!(g.is_2d(), true);
227        assert_eq!(g.num_points(), 9);
228
229        assert!(g.first().is_some());
230        let first = g.first().unwrap();
231        assert_relative_eq!(first[0], 43.729, epsilon = TOLERANCE);
232        assert_relative_eq!(first[1], -79.299, epsilon = TOLERANCE);
233        assert!(g.last().is_some());
234        let last = g.last().unwrap();
235        assert_relative_eq!(last[0], 43.734, epsilon = TOLERANCE);
236        assert_relative_eq!(last[1], -79.291, epsilon = TOLERANCE);
237    }
238
239    #[test]
240    #[should_panic]
241    fn test_invalid() {
242        let line = Line::from_xy(vec![vec![0.0, 45.0], vec![90.0, 180.0], vec![45.0, 45.0]]);
243        let crs = CRS::default();
244        line.check_coordinates(&crs).unwrap();
245    }
246
247    #[test]
248    fn test_precision() -> Result<(), Box<dyn Error>> {
249        const WKT: &str = "LINESTRING (82.400480 30.411477, 82.722734 30.365046)";
250
251        let line_xy = vec![
252            vec![82.400479770847, 30.4114773625851],
253            vec![82.7227340026191, 30.3650460881709],
254        ];
255
256        let line = Line::from_xy(line_xy);
257        let wkt = line.to_wkt_fmt(6);
258        assert_eq!(wkt, WKT);
259
260        Ok(())
261    }
262}