1use crate::layers::Layer;
4use crate::projection::{GeoPos, MapProjection};
5use egui::{Align2, Color32, FontId, Painter, Pos2, Rect, Response};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8
9mod ser_color {
11 use egui::Color32;
12 use serde::{self, Deserialize, Deserializer, Serializer};
13
14 pub fn serialize<S>(color: &Color32, serializer: S) -> Result<S::Ok, S::Error>
15 where
16 S: Serializer,
17 {
18 let s = color.to_hex();
19 serializer.serialize_str(&s)
20 }
21
22 pub fn deserialize<'de, D>(deserializer: D) -> Result<Color32, D::Error>
23 where
24 D: Deserializer<'de>,
25 {
26 let s = String::deserialize(deserializer)?;
27 if !s.starts_with('#') {
28 return Err(serde::de::Error::custom("hex color must start with '#'"));
29 }
30 let s = &s[1..];
31 let (r, g, b, a) = match s.len() {
32 6 => {
33 let r = u8::from_str_radix(&s[0..2], 16).map_err(serde::de::Error::custom)?;
34 let g = u8::from_str_radix(&s[2..4], 16).map_err(serde::de::Error::custom)?;
35 let b = u8::from_str_radix(&s[4..6], 16).map_err(serde::de::Error::custom)?;
36 (r, g, b, 255)
37 }
38 8 => {
39 let r = u8::from_str_radix(&s[0..2], 16).map_err(serde::de::Error::custom)?;
40 let g = u8::from_str_radix(&s[2..4], 16).map_err(serde::de::Error::custom)?;
41 let b = u8::from_str_radix(&s[4..6], 16).map_err(serde::de::Error::custom)?;
42 let a = u8::from_str_radix(&s[6..8], 16).map_err(serde::de::Error::custom)?;
43 (r, g, b, a)
44 }
45 _ => {
46 return Err(serde::de::Error::custom("invalid hex color length"));
47 }
48 };
49 Ok(Color32::from_rgba_unmultiplied(r, g, b, a))
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
55pub enum TextSize {
56 Static(f32),
58
59 Relative(f32),
61}
62
63impl Default for TextSize {
64 fn default() -> Self {
65 Self::Static(12.0)
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72pub struct Text {
73 pub text: String,
75
76 pub pos: GeoPos,
78
79 pub size: TextSize,
81
82 #[serde(with = "ser_color")]
84 pub color: Color32,
85
86 #[serde(with = "ser_color")]
88 pub background: Color32,
89}
90
91impl Default for Text {
92 fn default() -> Self {
93 Self {
94 text: "New Text".to_string(),
95 pos: GeoPos { lon: 0.0, lat: 0.0 }, size: TextSize::default(),
97 color: Color32::BLACK,
98 background: Color32::from_rgba_unmultiplied(255, 255, 255, 180),
99 }
100 }
101}
102
103#[derive(Clone, Debug)]
105pub struct EditingText {
106 pub index: Option<usize>,
108 pub properties: Text,
110}
111
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub enum TextLayerMode {
115 #[default]
117 Disabled,
118 Modify,
120}
121
122#[derive(Clone, Serialize, Deserialize)]
124#[serde(default)]
125pub struct TextLayer {
126 texts: Vec<Text>,
127
128 #[serde(skip)]
130 pub mode: TextLayerMode,
131
132 #[serde(skip)]
134 pub new_text_properties: Text,
135
136 #[serde(skip)]
138 pub editing: Option<EditingText>,
139
140 #[serde(skip)]
141 dragged_text_index: Option<usize>,
142}
143
144impl Default for TextLayer {
145 fn default() -> Self {
146 Self {
147 texts: Vec::new(),
148 mode: TextLayerMode::default(),
149 new_text_properties: Text::default(),
150 editing: None,
151 dragged_text_index: None,
152 }
153 }
154}
155
156impl TextLayer {
157 pub fn start_editing(&mut self, index: usize) {
159 if let Some(text) = self.texts.get(index) {
160 self.editing = Some(EditingText {
161 index: Some(index),
162 properties: text.clone(),
163 });
164 }
165 }
166
167 pub fn delete(&mut self, index: usize) {
169 if index < self.texts.len() {
170 self.texts.remove(index);
171 }
172 }
173
174 pub fn commit_edit(&mut self) {
176 if let Some(editing) = self.editing.take() {
177 if let Some(index) = editing.index {
178 if let Some(text) = self.texts.get_mut(index) {
180 *text = editing.properties;
181 }
182 } else {
183 self.texts.push(editing.properties);
185 }
186 }
187 }
188
189 pub fn cancel_edit(&mut self) {
191 self.editing = None;
192 }
193
194 fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
195 if self.editing.is_some() {
196 return response.hovered();
199 }
200
201 if response.drag_started() {
202 if let Some(pointer_pos) = response.interact_pointer_pos() {
203 self.dragged_text_index = self.find_text_at(pointer_pos, projection, &response.ctx);
204 }
205 }
206
207 if response.dragged() {
208 if let Some(text_index) = self.dragged_text_index {
209 if let Some(text) = self.texts.get_mut(text_index) {
210 if let Some(pointer_pos) = response.interact_pointer_pos() {
211 text.pos = projection.unproject(pointer_pos);
212 }
213 }
214 }
215 }
216
217 if response.drag_stopped() {
218 self.dragged_text_index = None;
219 }
220
221 if self.dragged_text_index.is_some() {
223 response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
224 } else if let Some(hover_pos) = response.hover_pos() {
225 if self
226 .find_text_at(hover_pos, projection, &response.ctx)
227 .is_some()
228 {
229 response.ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
230 } else {
231 response.ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
232 }
233 }
234
235 if !response.dragged() && response.clicked() {
236 if let Some(pointer_pos) = response.interact_pointer_pos() {
238 if let Some(index) = self.find_text_at(pointer_pos, projection, &response.ctx) {
239 self.start_editing(index);
241 } else {
242 let geo_pos = projection.unproject(pointer_pos);
244 let mut properties = self.new_text_properties.clone();
245 properties.pos = geo_pos;
246 self.editing = Some(EditingText {
247 index: None,
248 properties,
249 });
250 }
251 }
252 }
253
254 response.hovered()
255 }
256
257 fn find_text_at(
259 &self,
260 screen_pos: Pos2,
261 projection: &MapProjection,
262 ctx: &egui::Context,
263 ) -> Option<usize> {
264 self.texts.iter().enumerate().rev().find_map(|(i, text)| {
265 let text_rect = self.get_text_rect(text, projection, ctx);
266 if text_rect.expand(5.0).contains(screen_pos) {
267 Some(i)
269 } else {
270 None
271 }
272 })
273 }
274}
275
276impl Layer for TextLayer {
277 fn as_any(&self) -> &dyn Any {
278 self
279 }
280
281 fn as_any_mut(&mut self) -> &mut dyn Any {
282 self
283 }
284
285 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
286 match self.mode {
287 TextLayerMode::Disabled => false,
288 TextLayerMode::Modify => self.handle_modify_input(response, projection),
289 }
290 }
291
292 fn draw(&self, painter: &Painter, projection: &MapProjection) {
293 for text in &self.texts {
294 let screen_pos = projection.project(text.pos);
295
296 let galley = painter.layout_no_wrap(
297 text.text.clone(),
299 FontId::proportional(self.get_font_size(text, projection)),
300 text.color,
301 );
302
303 let rect =
304 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()));
305
306 painter.rect_filled(rect.expand(2.0), 3.0, text.background);
307 painter.galley(rect.min, galley, Color32::TRANSPARENT);
308 }
309 }
310}
311
312impl TextLayer {
313 fn get_font_size(&self, text: &Text, projection: &MapProjection) -> f32 {
314 match text.size {
315 TextSize::Static(size) => size,
316 TextSize::Relative(size_in_meters) => {
317 let p2 = projection.project(GeoPos {
318 lon: text.pos.lon
319 + (size_in_meters as f64 / (111_320.0 * text.pos.lat.to_radians().cos())),
320 lat: text.pos.lat,
321 });
322 (p2.x - projection.project(text.pos).x).abs()
323 }
324 }
325 }
326
327 fn get_text_rect(&self, text: &Text, projection: &MapProjection, ctx: &egui::Context) -> Rect {
328 let font_size = self.get_font_size(text, projection);
329 let galley = ctx.fonts(|f| {
330 f.layout_no_wrap(
331 text.text.clone(),
332 FontId::proportional(font_size),
333 text.color,
334 )
335 });
336 let screen_pos = projection.project(text.pos);
337 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()))
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn text_layer_serde() {
347 let mut layer = TextLayer::default();
348 layer.mode = TextLayerMode::Modify; layer.texts.push(Text {
350 text: "Hello".to_string(),
351 pos: GeoPos { lon: 1.0, lat: 2.0 },
352 size: TextSize::Static(14.0),
353 color: Color32::from_rgb(0, 0, 255),
354 background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
355 });
356
357 let json = serde_json::to_string(&layer).unwrap();
358
359 assert!(json.contains(r##""texts":[{"text":"Hello","pos":{"lon":1.0,"lat":2.0},"size":{"Static":14.0},"color":"#0000ffff","background":"#ff000080""##));
361
362 assert!(!json.contains("mode"));
364 assert!(!json.contains("new_text_properties"));
365 assert!(!json.contains("editing"));
366 assert!(!json.contains("dragged_text_index"));
367
368 let deserialized: TextLayer = serde_json::from_str(&json).unwrap();
369
370 assert_eq!(deserialized.texts.len(), 1);
372 assert_eq!(deserialized.texts[0].text, "Hello");
373 assert_eq!(deserialized.texts[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
374 assert_eq!(deserialized.texts[0].size, TextSize::Static(14.0));
375 assert_eq!(deserialized.texts[0].color, Color32::from_rgb(0, 0, 255));
376 assert_eq!(
377 deserialized.texts[0].background,
378 Color32::from_rgba_unmultiplied(255, 0, 0, 128)
379 );
380
381 let default_layer = TextLayer::default();
383 assert_eq!(deserialized.mode, default_layer.mode);
384 assert_eq!(
385 deserialized.new_text_properties,
386 default_layer.new_text_properties
387 );
388 assert!(deserialized.editing.is_none());
389 assert!(deserialized.dragged_text_index.is_none());
390 }
391}