1use crate::layers::{Layer, default_opacity};
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, 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 #[serde(default = "default_opacity")]
145 pub opacity: f32,
146}
147
148impl Default for SvgLayer {
149 fn default() -> Self {
150 Self {
151 elements: Vec::new(),
152 events: Vec::new(),
153 dragging_index: None,
154 opacity: 1.0,
155 }
156 }
157}
158
159impl SvgLayer {
160 pub fn add_element(&mut self, element: SvgElement) {
162 self.elements.push(element);
163 }
164
165 pub fn clear(&mut self) {
167 self.elements.clear();
168 }
169
170 pub fn take_events(&mut self) -> Vec<SvgClickEvent> {
172 std::mem::take(&mut self.events)
173 }
174}
175
176impl Layer for SvgLayer {
177 fn as_any(&self) -> &dyn Any {
178 self
179 }
180
181 fn as_any_mut(&mut self) -> &mut dyn Any {
182 self
183 }
184
185 fn opacity(&self) -> f32 {
186 self.opacity
187 }
188
189 fn set_opacity(&mut self, opacity: f32) {
190 self.opacity = opacity;
191 }
192
193 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
194 egui_extras::install_image_loaders(&response.ctx);
196
197 for element in &self.elements {
198 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
199 response
201 .ctx
202 .include_bytes(uri, element.text.as_bytes().to_vec());
203 }
204
205 let mut handled = false;
206
207 if let Some(index) = self.dragging_index {
209 if response.dragged() {
210 if let Some(pointer_pos) = response.interact_pointer_pos()
211 && let Some(element) = self.elements.get_mut(index)
212 {
213 element.pos = projection.unproject(pointer_pos);
214 handled = true;
215 response.ctx.request_repaint();
216 }
217 } else {
218 self.dragging_index = None;
219 }
220 }
221
222 if let Some(pointer_pos) = response.interact_pointer_pos() {
224 for (index, element) in self.elements.iter_mut().enumerate() {
225 if !element.clickable && !element.draggable {
226 continue;
227 }
228
229 let screen_pos = projection.project(element.pos);
230 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
231
232 if let Ok(egui::load::TexturePoll::Ready { texture }) = response
233 .ctx
234 .try_load_texture(&uri, egui::TextureOptions::default(), Default::default())
235 {
236 let mut size = texture.size;
237
238 if element.scalable {
239 let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
241 size *= scale;
242 }
243
244 let rect = egui::Rect::from_min_size(
245 screen_pos - size * element.anchor.to_vec2(),
246 size,
247 );
248 if rect.contains(pointer_pos) {
249 if element.draggable && response.drag_started() {
251 self.dragging_index = Some(index);
252 handled = true;
253 }
254
255 if element.clickable && (response.clicked() || response.secondary_clicked())
257 {
258 let button = if response.secondary_clicked() {
259 PointerButton::Secondary
260 } else {
261 PointerButton::Primary
262 };
263
264 self.events.push(SvgClickEvent {
265 button,
266 metadata: element.metadata.clone(),
267 world_pos: projection.unproject(pointer_pos),
268 screen_pos: pointer_pos,
269 });
270 handled = true;
271 }
272 }
273 }
274 }
275 }
276
277 handled
278 }
279
280 fn draw(&self, painter: &Painter, projection: &MapProjection) {
281 for element in &self.elements {
282 let screen_pos = projection.project(element.pos);
283 let uri = format!("bytes://{}.svg", rust_hash(&element.text));
284
285 match painter.ctx().try_load_texture(
286 &uri,
287 egui::TextureOptions::default(),
288 Default::default(),
289 ) {
290 Ok(egui::load::TexturePoll::Ready { texture }) => {
291 let mut size = texture.size;
292
293 if element.scalable {
294 let scale = 2.0_f32.powi(i32::from(projection.zoom) - 10);
297 size *= scale;
298 }
299
300 let rect = egui::Rect::from_min_size(
301 screen_pos - size * element.anchor.to_vec2(),
302 size,
303 );
304 painter.image(
305 texture.id,
306 rect,
307 egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
308 Color32::WHITE.gamma_multiply(self.opacity),
309 );
310 }
311 _ => {
312 painter.ctx().request_repaint();
315 }
316 }
317 }
318 }
319}
320
321fn rust_hash(s: &str) -> u64 {
322 let mut hasher = DefaultHasher::new();
323 s.hash(&mut hasher);
324 hasher.finish()
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn svg_layer_serde() {
333 let mut layer = SvgLayer::default();
334 layer.add_element(SvgElement {
335 pos: GeoPos { lon: 1.0, lat: 2.0 },
336 text: "<svg></svg>".to_string(),
337 metadata: "test metadata".to_string(),
338 scalable: false,
339 clickable: true,
340 draggable: false,
341 anchor: Pos2::new(0.5, 0.5),
342 });
343
344 let json = serde_json::to_string(&layer).unwrap();
345 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
346
347 assert_eq!(deserialized.elements.len(), 1);
348 assert_eq!(deserialized.elements[0].text, "<svg></svg>");
349 assert_eq!(deserialized.elements[0].metadata, "test metadata");
350 assert_eq!(deserialized.elements[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
351 assert!(deserialized.elements[0].clickable);
352 assert!(!deserialized.elements[0].draggable);
353 }
354
355 #[test]
356 fn svg_layer_serde_backward_compatibility() {
357 let json = r#"{
358 "elements": [
359 {
360 "pos": {"lon": 1.0, "lat": 2.0},
361 "text": "<svg></svg>",
362 "metadata": "test metadata",
363 "scalable": false
364 }
365 ]
366 }"#;
367 let deserialized: SvgLayer = serde_json::from_str(json).unwrap();
368 assert!(deserialized.elements[0].clickable);
369 assert!(!deserialized.elements[0].draggable);
370 }
371
372 #[test]
373 fn svg_layer_clickable_false() {
374 let mut layer = SvgLayer::default();
375 layer.add_element(SvgElement {
376 pos: GeoPos { lon: 1.0, lat: 2.0 },
377 text: "<svg></svg>".to_string(),
378 metadata: "test metadata".to_string(),
379 scalable: false,
380 clickable: false,
381 draggable: false,
382 anchor: default_anchor(),
383 });
384
385 let json = serde_json::to_string(&layer).unwrap();
386 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
387 assert!(!deserialized.elements[0].clickable);
388 }
389
390 #[test]
391 fn svg_layer_draggable_true() {
392 let mut layer = SvgLayer::default();
393 layer.add_element(SvgElement {
394 pos: GeoPos { lon: 1.0, lat: 2.0 },
395 text: "<svg></svg>".to_string(),
396 metadata: "test metadata".to_string(),
397 scalable: false,
398 clickable: false,
399 draggable: true,
400 anchor: default_anchor(),
401 });
402
403 let json = serde_json::to_string(&layer).unwrap();
404 let deserialized: SvgLayer = serde_json::from_str(&json).unwrap();
405 assert!(deserialized.elements[0].draggable);
406 }
407}