1use crate::layers::Layer;
4use crate::projection::{GeoPos, MapProjection};
5use egui::{Color32, Painter, PointerButton, Pos2, Response};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::collections::hash_map::DefaultHasher;
9use std::hash::{Hash, Hasher};
10
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct SvgElement {
14 pub pos: GeoPos,
16
17 pub text: String,
19
20 pub metadata: String,
22
23 pub scalable: bool,
27
28 #[serde(default = "default_true")]
32 pub clickable: bool,
33
34 #[serde(default)]
37 pub draggable: bool,
38
39 #[serde(default = "default_anchor")]
44 pub anchor: Pos2,
45}
46
47fn default_anchor() -> Pos2 {
48 Pos2::new(0.5, 0.5)
49}
50
51fn default_true() -> bool {
52 true
53}
54
55impl SvgElement {
56 pub fn new(pos: GeoPos, text: impl Into<String>, metadata: impl Into<String>) -> Self {
58 Self {
59 pos,
60 text: text.into(),
61 metadata: metadata.into(),
62 scalable: false,
63 clickable: true,
64 draggable: false,
65 anchor: default_anchor(),
66 }
67 }
68
69 pub fn from_xy(
71 lon: f64,
72 lat: f64,
73 text: impl Into<String>,
74 metadata: impl Into<String>,
75 ) -> Self {
76 Self {
77 pos: GeoPos { lon, lat },
78 text: text.into(),
79 metadata: metadata.into(),
80 scalable: false,
81 clickable: true,
82 draggable: false,
83 anchor: default_anchor(),
84 }
85 }
86
87 #[must_use]
89 pub fn with_scalable(mut self, scalable: bool) -> Self {
90 self.scalable = scalable;
91 self
92 }
93
94 #[must_use]
96 pub fn with_clickable(mut self, clickable: bool) -> Self {
97 self.clickable = clickable;
98 self
99 }
100
101 #[must_use]
103 pub fn with_draggable(mut self, draggable: bool) -> Self {
104 self.draggable = draggable;
105 self
106 }
107
108 #[must_use]
110 pub fn with_anchor(mut self, anchor: Pos2) -> Self {
111 self.anchor = anchor;
112 self
113 }
114}
115
116#[derive(Clone, Debug)]
118pub struct SvgClickEvent {
119 pub button: PointerButton,
121 pub metadata: String,
123 pub world_pos: GeoPos,
125 pub screen_pos: Pos2,
127}
128
129#[derive(Clone, Default, Serialize, Deserialize)]
131pub struct SvgLayer {
132 pub elements: Vec<SvgElement>,
134
135 #[serde(skip)]
137 pub events: Vec<SvgClickEvent>,
138
139 #[serde(skip)]
141 pub dragging_index: Option<usize>,
142}
143
144impl SvgLayer {
145 pub fn add_element(&mut self, element: SvgElement) {
147 self.elements.push(element);
148 }
149
150 pub fn clear(&mut self) {
152 self.elements.clear();
153 }
154
155 pub fn take_events(&mut self) -> Vec<SvgClickEvent> {
157 std::mem::take(&mut self.events)
158 }
159}
160
161impl Layer for SvgLayer {
162 fn as_any(&self) -> &dyn Any {
163 self
164 }
165
166 fn as_any_mut(&mut self) -> &mut dyn Any {
167 self
168 }
169
170 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
171 egui_extras::install_image_loaders(&response.ctx);
173
174 for element in &self.elements {
175 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
176 response
178 .ctx
179 .include_bytes(uri, element.text.as_bytes().to_vec());
180 }
181
182 let mut handled = false;
183
184 if let Some(index) = self.dragging_index {
186 if response.dragged() {
187 if let Some(pointer_pos) = response.interact_pointer_pos()
188 && let Some(element) = self.elements.get_mut(index)
189 {
190 element.pos = projection.unproject(pointer_pos);
191 handled = true;
192 response.ctx.request_repaint();
193 }
194 } else {
195 self.dragging_index = None;
196 }
197 }
198
199 if let Some(pointer_pos) = response.interact_pointer_pos() {
201 for (index, element) in self.elements.iter_mut().enumerate() {
202 if !element.clickable && !element.draggable {
203 continue;
204 }
205
206 let screen_pos = projection.project(element.pos);
207 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
208
209 if let Ok(egui::load::TexturePoll::Ready { texture }) = response
210 .ctx
211 .try_load_texture(&uri, egui::TextureOptions::default(), Default::default())
212 {
213 let mut size = texture.size;
214
215 if element.scalable {
216 let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
218 size *= scale;
219 }
220
221 let rect = egui::Rect::from_min_size(
222 screen_pos - size * element.anchor.to_vec2(),
223 size,
224 );
225 if rect.contains(pointer_pos) {
226 if element.draggable && response.drag_started() {
228 self.dragging_index = Some(index);
229 handled = true;
230 }
231
232 if element.clickable && (response.clicked() || response.secondary_clicked())
234 {
235 let button = if response.secondary_clicked() {
236 PointerButton::Secondary
237 } else {
238 PointerButton::Primary
239 };
240
241 self.events.push(SvgClickEvent {
242 button,
243 metadata: element.metadata.clone(),
244 world_pos: projection.unproject(pointer_pos),
245 screen_pos: pointer_pos,
246 });
247 handled = true;
248 }
249 }
250 }
251 }
252 }
253
254 handled
255 }
256
257 fn draw(&self, painter: &Painter, projection: &MapProjection) {
258 for element in &self.elements {
259 let screen_pos = projection.project(element.pos);
260 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
261
262 match painter.ctx().try_load_texture(
263 &uri,
264 egui::TextureOptions::default(),
265 Default::default(),
266 ) {
267 Ok(egui::load::TexturePoll::Ready { texture }) => {
268 let mut size = texture.size;
269
270 if element.scalable {
271 let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
274 size *= scale;
275 }
276
277 let rect = egui::Rect::from_min_size(
278 screen_pos - size * element.anchor.to_vec2(),
279 size,
280 );
281 painter.image(
282 texture.id,
283 rect,
284 egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
285 Color32::WHITE,
286 );
287 }
288 _ => {
289 painter.ctx().request_repaint();
292 }
293 }
294 }
295 }
296}
297
298fn rust_hash(s: &str) -> u64 {
299 let mut hasher = DefaultHasher::new();
300 s.hash(&mut hasher);
301 hasher.finish()
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn svg_layer_serde() {
310 let mut layer = SvgLayer::default();
311 layer.add_element(SvgElement {
312 pos: GeoPos { lon: 1.0, lat: 2.0 },
313 text: "<svg></svg>".to_string(),
314 metadata: "test metadata".to_string(),
315 scalable: false,
316 clickable: true,
317 draggable: false,
318 anchor: Pos2::new(0.5, 0.5),
319 });
320
321 let json = serde_json::to_string(&layer).unwrap();
322 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
323
324 assert_eq!(deserialized.elements.len(), 1);
325 assert_eq!(deserialized.elements[0].text, "<svg></svg>");
326 assert_eq!(deserialized.elements[0].metadata, "test metadata");
327 assert_eq!(deserialized.elements[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
328 assert!(deserialized.elements[0].clickable);
329 assert!(!deserialized.elements[0].draggable);
330 }
331
332 #[test]
333 fn svg_layer_serde_backward_compatibility() {
334 let json = r#"{
335 "elements": [
336 {
337 "pos": {"lon": 1.0, "lat": 2.0},
338 "text": "<svg></svg>",
339 "metadata": "test metadata",
340 "scalable": false
341 }
342 ]
343 }"#;
344 let deserialized: SvgLayer = serde_json::from_str(json).unwrap();
345 assert!(deserialized.elements[0].clickable);
346 assert!(!deserialized.elements[0].draggable);
347 }
348
349 #[test]
350 fn svg_layer_clickable_false() {
351 let mut layer = SvgLayer::default();
352 layer.add_element(SvgElement {
353 pos: GeoPos { lon: 1.0, lat: 2.0 },
354 text: "<svg></svg>".to_string(),
355 metadata: "test metadata".to_string(),
356 scalable: false,
357 clickable: false,
358 draggable: false,
359 anchor: default_anchor(),
360 });
361
362 let json = serde_json::to_string(&layer).unwrap();
363 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
364 assert!(!deserialized.elements[0].clickable);
365 }
366
367 #[test]
368 fn svg_layer_draggable_true() {
369 let mut layer = SvgLayer::default();
370 layer.add_element(SvgElement {
371 pos: GeoPos { lon: 1.0, lat: 2.0 },
372 text: "<svg></svg>".to_string(),
373 metadata: "test metadata".to_string(),
374 scalable: false,
375 clickable: false,
376 draggable: true,
377 anchor: default_anchor(),
378 });
379
380 let json = serde_json::to_string(&layer).unwrap();
381 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
382 assert!(deserialized.elements[0].draggable);
383 }
384}