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 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 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
69pub struct GeoPos {
70 pub lon: f64,
72
73 pub lat: f64,
75}
76
77impl GeoPos {
78 #[must_use]
80 pub fn distance(&self, other: &Self) -> f64 {
81 let r = 6_371_000.0; 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)), 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)); 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); 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); assert!((screen_pos_in.y - screen_pos_out.y).abs() < 1e-3);
183 }
184}