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(crate) 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    pub fn project(&self, geo_pos: GeoPos) -> Pos2 {
33        let center_x = lon_to_x(self.center_lon, self.zoom);
34        let center_y = lat_to_y(self.center_lat, self.zoom);
35
36        let tile_x = lon_to_x(geo_pos.lon, self.zoom);
37        let tile_y = lat_to_y(geo_pos.lat, self.zoom);
38
39        let dx = (tile_x - center_x) * TILE_SIZE as f64;
40        let dy = (tile_y - center_y) * TILE_SIZE as f64;
41
42        let widget_center = self.widget_rect.center();
43        widget_center + vec2(dx as f32, dy as f32)
44    }
45
46    /// Un-projects a screen coordinate to a geographical coordinate.
47    pub fn unproject(&self, screen_pos: Pos2) -> GeoPos {
48        let rel_pos = screen_pos - self.widget_rect.min;
49        let widget_center_x = self.widget_rect.width() as f64 / 2.0;
50        let widget_center_y = self.widget_rect.height() as f64 / 2.0;
51
52        let center_x = lon_to_x(self.center_lon, self.zoom);
53        let center_y = lat_to_y(self.center_lat, self.zoom);
54
55        let target_x = center_x + (rel_pos.x as f64 - widget_center_x) / TILE_SIZE as f64;
56        let target_y = center_y + (rel_pos.y as f64 - widget_center_y) / TILE_SIZE as f64;
57
58        GeoPos {
59            lon: x_to_lon(target_x, self.zoom),
60            lat: y_to_lat(target_y, self.zoom),
61        }
62    }
63}
64
65/// A geographical position.
66#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
67pub struct GeoPos {
68    /// Longitude.
69    pub lon: f64,
70
71    /// Latitude.
72    pub lat: f64,
73}
74
75impl From<(f64, f64)> for GeoPos {
76    fn from((lon, lat): (f64, f64)) -> Self {
77        Self { lon, lat }
78    }
79}
80
81impl From<GeoPos> for (f64, f64) {
82    fn from(pos: GeoPos) -> Self {
83        (pos.lon, pos.lat)
84    }
85}
86
87impl From<&[f64; 2]> for GeoPos {
88    fn from([lon, lat]: &[f64; 2]) -> Self {
89        Self {
90            lon: *lon,
91            lat: *lat,
92        }
93    }
94}
95
96impl From<GeoPos> for Vec<f64> {
97    fn from(pos: GeoPos) -> Self {
98        vec![pos.lon, pos.lat]
99    }
100}
101
102impl From<Vec<f64>> for GeoPos {
103    fn from(pos: Vec<f64>) -> Self {
104        Self {
105            lon: pos[0],
106            lat: pos[1],
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use egui::{pos2, vec2};
115
116    const EPSILON: f64 = 1e-9;
117
118    fn create_projection() -> MapProjection {
119        MapProjection::new(
120            10,
121            GeoPos::from((24.93545, 60.16952)), // Helsinki
122            Rect::from_min_size(pos2(100.0, 200.0), vec2(800.0, 600.0)),
123        )
124    }
125
126    #[test]
127    fn project_center() {
128        let projection = create_projection();
129        let center_geo = GeoPos::from((projection.center_lon, projection.center_lat));
130        let projected_center = projection.project(center_geo);
131        assert_eq!(projected_center, projection.widget_rect.center());
132    }
133
134    #[test]
135    fn unproject_center() {
136        let projection = create_projection();
137        let center_screen = projection.widget_rect.center();
138        let (lon, lat) = projection.unproject(center_screen).into();
139        assert!((lon - projection.center_lon).abs() < EPSILON);
140        assert!((lat - projection.center_lat).abs() < EPSILON);
141    }
142
143    #[test]
144    fn project_unproject_roundtrip() {
145        let projection = create_projection();
146        let geo_pos_in = GeoPos::from((24.93545, 60.16952)); // Some point near Helsinki
147
148        let screen_pos = projection.project(geo_pos_in);
149        let geo_pos_out = projection.unproject(screen_pos);
150
151        assert!((geo_pos_in.lon - geo_pos_out.lon).abs() < EPSILON);
152        assert!((geo_pos_in.lat - geo_pos_out.lat).abs() < EPSILON);
153    }
154
155    #[test]
156    fn unproject_project_roundtrip() {
157        let projection = create_projection();
158        let screen_pos_in = pos2(150.0, 250.0); // Some point on the widget
159
160        let geo_pos = projection.unproject(screen_pos_in);
161        let screen_pos_out = projection.project(geo_pos);
162
163        assert!((screen_pos_in.x - screen_pos_out.x).abs() < 1e-3); // f32 precision
164        assert!((screen_pos_in.y - screen_pos_out.y).abs() < 1e-3);
165    }
166}