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