Skip to main content

egui_map_view/
projection.rs

1//! Projections handle converting different coordinate systems between other coordinate systems.
2
3use egui::{Pos2, Rect, vec2};
4use serde::{Deserialize, Serialize};
5
6use crate::{TILE_SIZE, lat_to_y, lon_to_x, x_to_lon, y_to_lat};
7
8/// A helper for converting between geographical and screen coordinates.
9pub struct MapProjection {
10    /// The zoom level of the map.
11    pub zoom: u8,
12    /// The longitude of the center point of the map.
13    pub center_lon: f64,
14    /// The latitude of the center point of the map.
15    pub center_lat: f64,
16    /// The screen rectangle where the map is displayed.
17    pub widget_rect: Rect,
18}
19
20impl MapProjection {
21    /// Creates a new `MapProjection`.
22    pub fn new(zoom: u8, center: GeoPos, widget_rect: Rect) -> Self {
23        Self {
24            zoom,
25            center_lon: center.lon,
26            center_lat: center.lat,
27            widget_rect,
28        }
29    }
30
31    /// Projects a geographical coordinate to a screen coordinate.
32    #[must_use]
33    pub fn project(&self, geo_pos: GeoPos) -> Pos2 {
34        let center_x = lon_to_x(self.center_lon, self.zoom);
35        let center_y = lat_to_y(self.center_lat, self.zoom);
36
37        let tile_x = lon_to_x(geo_pos.lon, self.zoom);
38        let tile_y = lat_to_y(geo_pos.lat, self.zoom);
39
40        let dx = (tile_x - center_x) * f64::from(TILE_SIZE);
41        let dy = (tile_y - center_y) * f64::from(TILE_SIZE);
42
43        let widget_center = self.widget_rect.center();
44        widget_center + vec2(dx as f32, dy as f32)
45    }
46
47    /// Un-projects a screen coordinate to a geographical coordinate.
48    #[must_use]
49    pub fn unproject(&self, screen_pos: Pos2) -> GeoPos {
50        let rel_pos = screen_pos - self.widget_rect.min;
51        let widget_center_x = f64::from(self.widget_rect.width()) / 2.0;
52        let widget_center_y = f64::from(self.widget_rect.height()) / 2.0;
53
54        let center_x = lon_to_x(self.center_lon, self.zoom);
55        let center_y = lat_to_y(self.center_lat, self.zoom);
56
57        let target_x = center_x + (f64::from(rel_pos.x) - widget_center_x) / f64::from(TILE_SIZE);
58        let target_y = center_y + (f64::from(rel_pos.y) - widget_center_y) / f64::from(TILE_SIZE);
59
60        GeoPos {
61            lon: x_to_lon(target_x, self.zoom),
62            lat: y_to_lat(target_y, self.zoom),
63        }
64    }
65}
66
67/// A geographical position.
68#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
69pub struct GeoPos {
70    /// Longitude.
71    pub lon: f64,
72
73    /// Latitude.
74    pub lat: f64,
75}
76
77impl GeoPos {
78    /// Returns the distance between two geographical positions in meters.
79    #[must_use]
80    pub fn distance(&self, other: &Self) -> f64 {
81        let r = 6_371_000.0; // Earth radius in meters
82        let d_lat = (other.lat - self.lat).to_radians();
83        let d_lon = (other.lon - self.lon).to_radians();
84        let a = (d_lat / 2.0).sin().powi(2)
85            + self.lat.to_radians().cos()
86                * other.lat.to_radians().cos()
87                * (d_lon / 2.0).sin().powi(2);
88        let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
89        r * c
90    }
91}
92
93impl From<(f64, f64)> for GeoPos {
94    fn from((lon, lat): (f64, f64)) -> Self {
95        Self { lon, lat }
96    }
97}
98
99impl From<GeoPos> for (f64, f64) {
100    fn from(pos: GeoPos) -> Self {
101        (pos.lon, pos.lat)
102    }
103}
104
105impl From<&[f64; 2]> for GeoPos {
106    fn from([lon, lat]: &[f64; 2]) -> Self {
107        Self {
108            lon: *lon,
109            lat: *lat,
110        }
111    }
112}
113
114impl From<GeoPos> for Vec<f64> {
115    fn from(pos: GeoPos) -> Self {
116        vec![pos.lon, pos.lat]
117    }
118}
119
120impl From<Vec<f64>> for GeoPos {
121    fn from(pos: Vec<f64>) -> Self {
122        Self {
123            lon: pos[0],
124            lat: pos[1],
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use egui::{pos2, vec2};
133
134    const EPSILON: f64 = 1e-9;
135
136    fn create_projection() -> MapProjection {
137        MapProjection::new(
138            10,
139            GeoPos::from((24.93545, 60.16952)), // Helsinki
140            Rect::from_min_size(pos2(100.0, 200.0), vec2(800.0, 600.0)),
141        )
142    }
143
144    #[test]
145    fn project_center() {
146        let projection = create_projection();
147        let center_geo = GeoPos::from((projection.center_lon, projection.center_lat));
148        let projected_center = projection.project(center_geo);
149        assert_eq!(projected_center, projection.widget_rect.center());
150    }
151
152    #[test]
153    fn unproject_center() {
154        let projection = create_projection();
155        let center_screen = projection.widget_rect.center();
156        let (lon, lat) = projection.unproject(center_screen).into();
157        assert!((lon - projection.center_lon).abs() < EPSILON);
158        assert!((lat - projection.center_lat).abs() < EPSILON);
159    }
160
161    #[test]
162    fn project_unproject_roundtrip() {
163        let projection = create_projection();
164        let geo_pos_in = GeoPos::from((24.93545, 60.16952)); // Some point near Helsinki
165
166        let screen_pos = projection.project(geo_pos_in);
167        let geo_pos_out = projection.unproject(screen_pos);
168
169        assert!((geo_pos_in.lon - geo_pos_out.lon).abs() < EPSILON);
170        assert!((geo_pos_in.lat - geo_pos_out.lat).abs() < EPSILON);
171    }
172
173    #[test]
174    fn unproject_project_roundtrip() {
175        let projection = create_projection();
176        let screen_pos_in = pos2(150.0, 250.0); // Some point on the widget
177
178        let geo_pos = projection.unproject(screen_pos_in);
179        let screen_pos_out = projection.project(geo_pos);
180
181        assert!((screen_pos_in.x - screen_pos_out.x).abs() < 1e-3); // f32 precision
182        assert!((screen_pos_in.y - screen_pos_out.y).abs() < 1e-3);
183    }
184}