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