Skip to main content

proj_core/
coord.rs

1/// A 2D coordinate.
2///
3/// At the public API boundary, units match the CRS:
4/// - **Geographic CRS**: degrees (x = longitude, y = latitude)
5/// - **Projected CRS**: the CRS's native linear unit (x = easting, y = northing)
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Coord {
8    pub x: f64,
9    pub y: f64,
10}
11
12impl Coord {
13    pub fn new(x: f64, y: f64) -> Self {
14        Self { x, y }
15    }
16}
17
18/// A 3D coordinate.
19///
20/// At the public API boundary:
21/// - **Geographic CRS**: x/y are longitude/latitude in degrees
22/// - **Projected CRS**: x/y are easting/northing in the CRS's native linear unit
23/// - `z` is preserved only when source and target vertical semantics are unchanged
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Coord3D {
26    pub x: f64,
27    pub y: f64,
28    pub z: f64,
29}
30
31impl Coord3D {
32    pub fn new(x: f64, y: f64, z: f64) -> Self {
33        Self { x, y, z }
34    }
35}
36
37/// A 2D axis-aligned bounding box in CRS-native units.
38///
39/// At the public API boundary, units match the CRS:
40/// - **Geographic CRS**: degrees
41/// - **Projected CRS**: the CRS's native linear unit
42///
43/// Bounds transformation APIs accept at most
44/// [`MAX_BOUNDS_DENSIFY_POINTS`] intermediate samples per edge.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct Bounds {
47    pub min_x: f64,
48    pub min_y: f64,
49    pub max_x: f64,
50    pub max_y: f64,
51}
52
53/// Maximum intermediate densification points accepted per bounds edge.
54///
55/// This caps CPU work for APIs such as [`crate::Transform::transform_bounds`]
56/// and AOI bounds normalization in [`crate::SelectionOptions`].
57pub const MAX_BOUNDS_DENSIFY_POINTS: usize = 10_000;
58
59impl Bounds {
60    pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
61        Self {
62            min_x,
63            min_y,
64            max_x,
65            max_y,
66        }
67    }
68
69    pub fn width(&self) -> f64 {
70        self.max_x - self.min_x
71    }
72
73    pub fn height(&self) -> f64 {
74        self.max_y - self.min_y
75    }
76
77    /// Return true when all bounds are finite and both axes satisfy `min <= max`.
78    pub fn is_valid(&self) -> bool {
79        self.min_x.is_finite()
80            && self.min_y.is_finite()
81            && self.max_x.is_finite()
82            && self.max_y.is_finite()
83            && self.min_x <= self.max_x
84            && self.min_y <= self.max_y
85    }
86
87    pub(crate) fn expand_to_include(&mut self, coord: Coord) {
88        self.min_x = self.min_x.min(coord.x);
89        self.min_y = self.min_y.min(coord.y);
90        self.max_x = self.max_x.max(coord.x);
91        self.max_y = self.max_y.max(coord.y);
92    }
93}
94
95impl From<(f64, f64)> for Coord {
96    fn from((x, y): (f64, f64)) -> Self {
97        Self { x, y }
98    }
99}
100
101impl From<Coord> for (f64, f64) {
102    fn from(c: Coord) -> Self {
103        (c.x, c.y)
104    }
105}
106
107impl From<(f64, f64, f64)> for Coord3D {
108    fn from((x, y, z): (f64, f64, f64)) -> Self {
109        Self { x, y, z }
110    }
111}
112
113impl From<Coord3D> for (f64, f64, f64) {
114    fn from(c: Coord3D) -> Self {
115        (c.x, c.y, c.z)
116    }
117}
118
119#[cfg(feature = "geo-types")]
120impl From<geo_types::Coord<f64>> for Coord {
121    fn from(c: geo_types::Coord<f64>) -> Self {
122        Self { x: c.x, y: c.y }
123    }
124}
125
126#[cfg(feature = "geo-types")]
127impl From<Coord> for geo_types::Coord<f64> {
128    fn from(c: Coord) -> Self {
129        geo_types::Coord { x: c.x, y: c.y }
130    }
131}
132
133/// Trait for types that can be transformed through a [`Transform`](crate::Transform).
134///
135/// The transform returns the same type as the input, so `geo_types::Coord<f64>` in
136/// gives `geo_types::Coord<f64>` out, and `(f64, f64)` in gives `(f64, f64)` out.
137pub trait Transformable: Sized {
138    fn into_coord(self) -> Coord;
139    fn from_coord(c: Coord) -> Self;
140}
141
142/// Trait for types that can be transformed through a [`Transform`](crate::Transform)
143/// while preserving a height component when vertical CRS semantics are unchanged.
144///
145/// The transform returns the same type as the input, so `(f64, f64, f64)` in gives
146/// `(f64, f64, f64)` out and [`Coord3D`] in gives [`Coord3D`] out.
147pub trait Transformable3D: Sized {
148    fn into_coord3d(self) -> Coord3D;
149    fn from_coord3d(c: Coord3D) -> Self;
150}
151
152impl Transformable for Coord {
153    fn into_coord(self) -> Coord {
154        self
155    }
156    fn from_coord(c: Coord) -> Self {
157        c
158    }
159}
160
161impl Transformable for (f64, f64) {
162    fn into_coord(self) -> Coord {
163        Coord {
164            x: self.0,
165            y: self.1,
166        }
167    }
168    fn from_coord(c: Coord) -> Self {
169        (c.x, c.y)
170    }
171}
172
173impl Transformable3D for Coord3D {
174    fn into_coord3d(self) -> Coord3D {
175        self
176    }
177
178    fn from_coord3d(c: Coord3D) -> Self {
179        c
180    }
181}
182
183impl Transformable3D for (f64, f64, f64) {
184    fn into_coord3d(self) -> Coord3D {
185        Coord3D {
186            x: self.0,
187            y: self.1,
188            z: self.2,
189        }
190    }
191
192    fn from_coord3d(c: Coord3D) -> Self {
193        (c.x, c.y, c.z)
194    }
195}
196
197#[cfg(feature = "geo-types")]
198impl Transformable for geo_types::Coord<f64> {
199    fn into_coord(self) -> Coord {
200        Coord {
201            x: self.x,
202            y: self.y,
203        }
204    }
205    fn from_coord(c: Coord) -> Self {
206        geo_types::Coord { x: c.x, y: c.y }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn coord_from_tuple() {
216        let c: Coord = (1.0, 2.0).into();
217        assert_eq!(c.x, 1.0);
218        assert_eq!(c.y, 2.0);
219    }
220
221    #[test]
222    fn tuple_from_coord() {
223        let t: (f64, f64) = Coord::new(3.0, 4.0).into();
224        assert_eq!(t, (3.0, 4.0));
225    }
226
227    #[test]
228    fn coord3d_from_tuple() {
229        let c: Coord3D = (1.0, 2.0, 3.0).into();
230        assert_eq!(c.x, 1.0);
231        assert_eq!(c.y, 2.0);
232        assert_eq!(c.z, 3.0);
233    }
234
235    #[test]
236    fn tuple_from_coord3d() {
237        let t: (f64, f64, f64) = Coord3D::new(3.0, 4.0, 5.0).into();
238        assert_eq!(t, (3.0, 4.0, 5.0));
239    }
240
241    #[test]
242    fn transformable_roundtrip_tuple() {
243        let original = (10.0, 20.0);
244        let coord = original.into_coord();
245        let back = <(f64, f64)>::from_coord(coord);
246        assert_eq!(original, back);
247    }
248
249    #[test]
250    fn transformable3d_roundtrip_tuple() {
251        let original = (10.0, 20.0, 30.0);
252        let coord = original.into_coord3d();
253        let back = <(f64, f64, f64)>::from_coord3d(coord);
254        assert_eq!(original, back);
255    }
256
257    #[test]
258    fn bounds_basics() {
259        let bounds = Bounds::new(-10.0, 20.0, 30.0, 40.0);
260        assert_eq!(bounds.width(), 40.0);
261        assert_eq!(bounds.height(), 20.0);
262        assert!(bounds.is_valid());
263    }
264
265    #[test]
266    fn bounds_invalid_when_non_finite_or_reversed() {
267        assert!(!Bounds::new(f64::NAN, 20.0, 30.0, 40.0).is_valid());
268        assert!(!Bounds::new(-10.0, 20.0, f64::INFINITY, 40.0).is_valid());
269        assert!(!Bounds::new(30.0, 20.0, -10.0, 40.0).is_valid());
270        assert!(!Bounds::new(-10.0, 40.0, 30.0, 20.0).is_valid());
271    }
272}