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