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**: meters (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 meters
23/// - `z` is preserved unchanged by the current horizontal-only transform pipeline
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**: meters
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Bounds {
44    pub min_x: f64,
45    pub min_y: f64,
46    pub max_x: f64,
47    pub max_y: f64,
48}
49
50impl Bounds {
51    pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
52        Self {
53            min_x,
54            min_y,
55            max_x,
56            max_y,
57        }
58    }
59
60    pub fn width(&self) -> f64 {
61        self.max_x - self.min_x
62    }
63
64    pub fn height(&self) -> f64 {
65        self.max_y - self.min_y
66    }
67
68    pub(crate) fn is_valid(&self) -> bool {
69        self.min_x.is_finite()
70            && self.min_y.is_finite()
71            && self.max_x.is_finite()
72            && self.max_y.is_finite()
73            && self.min_x <= self.max_x
74            && self.min_y <= self.max_y
75    }
76
77    pub(crate) fn expand_to_include(&mut self, coord: Coord) {
78        self.min_x = self.min_x.min(coord.x);
79        self.min_y = self.min_y.min(coord.y);
80        self.max_x = self.max_x.max(coord.x);
81        self.max_y = self.max_y.max(coord.y);
82    }
83}
84
85impl From<(f64, f64)> for Coord {
86    fn from((x, y): (f64, f64)) -> Self {
87        Self { x, y }
88    }
89}
90
91impl From<Coord> for (f64, f64) {
92    fn from(c: Coord) -> Self {
93        (c.x, c.y)
94    }
95}
96
97impl From<(f64, f64, f64)> for Coord3D {
98    fn from((x, y, z): (f64, f64, f64)) -> Self {
99        Self { x, y, z }
100    }
101}
102
103impl From<Coord3D> for (f64, f64, f64) {
104    fn from(c: Coord3D) -> Self {
105        (c.x, c.y, c.z)
106    }
107}
108
109#[cfg(feature = "geo-types")]
110impl From<geo_types::Coord<f64>> for Coord {
111    fn from(c: geo_types::Coord<f64>) -> Self {
112        Self { x: c.x, y: c.y }
113    }
114}
115
116#[cfg(feature = "geo-types")]
117impl From<Coord> for geo_types::Coord<f64> {
118    fn from(c: Coord) -> Self {
119        geo_types::Coord { x: c.x, y: c.y }
120    }
121}
122
123/// Trait for types that can be transformed through a [`Transform`](crate::Transform).
124///
125/// The transform returns the same type as the input, so `geo_types::Coord<f64>` in
126/// gives `geo_types::Coord<f64>` out, and `(f64, f64)` in gives `(f64, f64)` out.
127pub trait Transformable: Sized {
128    fn into_coord(self) -> Coord;
129    fn from_coord(c: Coord) -> Self;
130}
131
132/// Trait for types that can be transformed through a [`Transform`](crate::Transform)
133/// while preserving an ellipsoidal height component.
134///
135/// The transform returns the same type as the input, so `(f64, f64, f64)` in gives
136/// `(f64, f64, f64)` out and [`Coord3D`] in gives [`Coord3D`] out.
137pub trait Transformable3D: Sized {
138    fn into_coord3d(self) -> Coord3D;
139    fn from_coord3d(c: Coord3D) -> Self;
140}
141
142impl Transformable for Coord {
143    fn into_coord(self) -> Coord {
144        self
145    }
146    fn from_coord(c: Coord) -> Self {
147        c
148    }
149}
150
151impl Transformable for (f64, f64) {
152    fn into_coord(self) -> Coord {
153        Coord {
154            x: self.0,
155            y: self.1,
156        }
157    }
158    fn from_coord(c: Coord) -> Self {
159        (c.x, c.y)
160    }
161}
162
163impl Transformable3D for Coord3D {
164    fn into_coord3d(self) -> Coord3D {
165        self
166    }
167
168    fn from_coord3d(c: Coord3D) -> Self {
169        c
170    }
171}
172
173impl Transformable3D for (f64, f64, f64) {
174    fn into_coord3d(self) -> Coord3D {
175        Coord3D {
176            x: self.0,
177            y: self.1,
178            z: self.2,
179        }
180    }
181
182    fn from_coord3d(c: Coord3D) -> Self {
183        (c.x, c.y, c.z)
184    }
185}
186
187#[cfg(feature = "geo-types")]
188impl Transformable for geo_types::Coord<f64> {
189    fn into_coord(self) -> Coord {
190        Coord {
191            x: self.x,
192            y: self.y,
193        }
194    }
195    fn from_coord(c: Coord) -> Self {
196        geo_types::Coord { x: c.x, y: c.y }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn coord_from_tuple() {
206        let c: Coord = (1.0, 2.0).into();
207        assert_eq!(c.x, 1.0);
208        assert_eq!(c.y, 2.0);
209    }
210
211    #[test]
212    fn tuple_from_coord() {
213        let t: (f64, f64) = Coord::new(3.0, 4.0).into();
214        assert_eq!(t, (3.0, 4.0));
215    }
216
217    #[test]
218    fn coord3d_from_tuple() {
219        let c: Coord3D = (1.0, 2.0, 3.0).into();
220        assert_eq!(c.x, 1.0);
221        assert_eq!(c.y, 2.0);
222        assert_eq!(c.z, 3.0);
223    }
224
225    #[test]
226    fn tuple_from_coord3d() {
227        let t: (f64, f64, f64) = Coord3D::new(3.0, 4.0, 5.0).into();
228        assert_eq!(t, (3.0, 4.0, 5.0));
229    }
230
231    #[test]
232    fn transformable_roundtrip_tuple() {
233        let original = (10.0, 20.0);
234        let coord = original.into_coord();
235        let back = <(f64, f64)>::from_coord(coord);
236        assert_eq!(original, back);
237    }
238
239    #[test]
240    fn transformable3d_roundtrip_tuple() {
241        let original = (10.0, 20.0, 30.0);
242        let coord = original.into_coord3d();
243        let back = <(f64, f64, f64)>::from_coord3d(coord);
244        assert_eq!(original, back);
245    }
246
247    #[test]
248    fn bounds_basics() {
249        let bounds = Bounds::new(-10.0, 20.0, 30.0, 40.0);
250        assert_eq!(bounds.width(), 40.0);
251        assert_eq!(bounds.height(), 20.0);
252        assert!(bounds.is_valid());
253    }
254}