egui_map_view/
projection.rs1use 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
8pub struct MapProjection {
10 pub zoom: u8,
12 pub center_lon: f64,
14 pub center_lat: f64,
16 pub widget_rect: Rect,
18}
19
20impl MapProjection {
21 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
67pub struct GeoPos {
68 pub lon: f64,
70
71 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)), 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)); 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); 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); assert!((screen_pos_in.y - screen_pos_out.y).abs() < 1e-3);
165 }
166}