1use crate::layers::{Layer, default_opacity, serde_color32};
4use crate::projection::{GeoPos, MapProjection};
5use egui::{Align2, Color32, FontId, Painter, Pos2, Rect, Response};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8
9#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
11pub enum TextSize {
12 Static(f32),
14
15 Relative(f32),
17}
18
19impl Default for TextSize {
20 fn default() -> Self {
21 Self::Static(12.0)
23 }
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct Text {
29 pub text: String,
31
32 pub pos: GeoPos,
34
35 pub size: TextSize,
37
38 #[serde(with = "serde_color32")]
40 pub color: Color32,
41
42 #[serde(with = "serde_color32")]
44 pub background: Color32,
45}
46
47impl Default for Text {
48 fn default() -> Self {
49 Self {
50 text: "New Text".to_string(),
51 pos: GeoPos { lon: 0.0, lat: 0.0 }, size: TextSize::default(),
53 color: Color32::BLACK,
54 background: Color32::from_rgba_unmultiplied(255, 255, 255, 180),
55 }
56 }
57}
58
59#[derive(Clone, Debug)]
61pub struct EditingText {
62 pub index: Option<usize>,
64 pub properties: Text,
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum TextLayerMode {
71 #[default]
73 Disabled,
74 Modify,
76}
77
78#[derive(Clone, Serialize, Deserialize)]
80#[serde(default)]
81pub struct TextLayer {
82 texts: Vec<Text>,
83
84 #[serde(skip)]
86 pub mode: TextLayerMode,
87
88 #[serde(skip)]
90 pub new_text_properties: Text,
91
92 #[serde(skip)]
94 pub editing: Option<EditingText>,
95
96 #[serde(skip)]
97 dragged_text_index: Option<usize>,
98
99 #[serde(default = "default_opacity")]
101 pub opacity: f32,
102}
103
104impl Default for TextLayer {
105 fn default() -> Self {
106 Self {
107 texts: Vec::new(),
108 mode: TextLayerMode::default(),
109 new_text_properties: Text::default(),
110 editing: None,
111 dragged_text_index: None,
112 opacity: 1.0,
113 }
114 }
115}
116
117impl TextLayer {
118 pub fn start_editing(&mut self, index: usize) {
120 if let Some(text) = self.texts.get(index) {
121 self.editing = Some(EditingText {
122 index: Some(index),
123 properties: text.clone(),
124 });
125 }
126 }
127
128 pub fn delete(&mut self, index: usize) {
130 if index < self.texts.len() {
131 self.texts.remove(index);
132 }
133 }
134
135 pub fn commit_edit(&mut self) {
137 if let Some(editing) = self.editing.take() {
138 if let Some(index) = editing.index {
139 if let Some(text) = self.texts.get_mut(index) {
141 *text = editing.properties;
142 }
143 } else {
144 self.texts.push(editing.properties);
146 }
147 }
148 }
149
150 pub fn cancel_edit(&mut self) {
152 self.editing = None;
153 }
154
155 #[cfg(feature = "geojson")]
157 pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
158 let features: Vec<geojson::Feature> = self
159 .texts
160 .clone()
161 .into_iter()
162 .map(geojson::Feature::from)
163 .collect();
164 let mut foreign_members = serde_json::Map::new();
165 foreign_members.insert(
166 "opacity".to_string(),
167 serde_json::Value::from(f64::from(self.opacity)),
168 );
169
170 let feature_collection = geojson::FeatureCollection {
171 bbox: None,
172 features,
173 foreign_members: Some(foreign_members),
174 };
175 serde_json::to_string(&feature_collection)
176 }
177
178 #[cfg(feature = "geojson")]
180 pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
181 let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
182 let new_texts: Vec<Text> = feature_collection
183 .features
184 .into_iter()
185 .filter_map(|f| Text::try_from(f).ok())
186 .collect();
187 self.texts.extend(new_texts);
188
189 if let Some(foreign_members) = feature_collection.foreign_members
190 && let Some(value) = foreign_members.get("opacity")
191 && let Some(opacity) = value.as_f64()
192 {
193 self.opacity = opacity as f32;
194 }
195 Ok(())
196 }
197
198 fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
199 if self.editing.is_some() {
200 return response.hovered();
203 }
204
205 if response.drag_started()
206 && let Some(pointer_pos) = response.interact_pointer_pos()
207 {
208 self.dragged_text_index = self.find_text_at(pointer_pos, projection, &response.ctx);
209 }
210
211 if response.dragged()
212 && let Some(text_index) = self.dragged_text_index
213 && let Some(text) = self.texts.get_mut(text_index)
214 && let Some(pointer_pos) = response.interact_pointer_pos()
215 {
216 text.pos = projection.unproject(pointer_pos);
217 }
218
219 if response.drag_stopped() {
220 self.dragged_text_index = None;
221 }
222
223 if self.dragged_text_index.is_some() {
225 response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
226 } else if let Some(hover_pos) = response.hover_pos() {
227 if self
228 .find_text_at(hover_pos, projection, &response.ctx)
229 .is_some()
230 {
231 response.ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
232 } else {
233 response.ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
234 }
235 }
236
237 if !response.dragged() && response.clicked() {
238 if let Some(pointer_pos) = response.interact_pointer_pos() {
240 if let Some(index) = self.find_text_at(pointer_pos, projection, &response.ctx) {
241 self.start_editing(index);
243 } else {
244 let geo_pos = projection.unproject(pointer_pos);
246 let mut properties = self.new_text_properties.clone();
247 properties.pos = geo_pos;
248 self.editing = Some(EditingText {
249 index: None,
250 properties,
251 });
252 }
253 }
254 }
255
256 response.hovered()
257 }
258
259 fn find_text_at(
261 &self,
262 screen_pos: Pos2,
263 projection: &MapProjection,
264 ctx: &egui::Context,
265 ) -> Option<usize> {
266 self.texts.iter().enumerate().rev().find_map(|(i, text)| {
267 let text_rect = self.get_text_rect(text, projection, ctx);
268 if text_rect.expand(5.0).contains(screen_pos) {
269 Some(i)
271 } else {
272 None
273 }
274 })
275 }
276}
277
278impl Layer for TextLayer {
279 fn as_any(&self) -> &dyn Any {
280 self
281 }
282
283 fn as_any_mut(&mut self) -> &mut dyn Any {
284 self
285 }
286
287 fn opacity(&self) -> f32 {
288 self.opacity
289 }
290
291 fn set_opacity(&mut self, opacity: f32) {
292 self.opacity = opacity;
293 }
294
295 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
296 match self.mode {
297 TextLayerMode::Disabled => false,
298 TextLayerMode::Modify => self.handle_modify_input(response, projection),
299 }
300 }
301
302 fn draw(&self, painter: &Painter, projection: &MapProjection) {
303 for text in &self.texts {
304 let screen_pos = projection.project(text.pos);
305
306 let galley = painter.layout_no_wrap(
307 text.text.clone(),
309 FontId::proportional(self.get_font_size(text, projection)),
310 text.color.gamma_multiply(self.opacity),
311 );
312
313 let rect =
314 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()));
315
316 painter.rect_filled(
317 rect.expand(2.0),
318 3.0,
319 text.background.gamma_multiply(self.opacity),
320 );
321 painter.galley(rect.min, galley, Color32::TRANSPARENT);
322 }
323 }
324}
325
326impl TextLayer {
327 fn get_font_size(&self, text: &Text, projection: &MapProjection) -> f32 {
328 match text.size {
329 TextSize::Static(size) => size,
330 TextSize::Relative(size_in_meters) => {
331 let p2 = projection.project(GeoPos {
332 lon: text.pos.lon
333 + (f64::from(size_in_meters)
334 / (111_320.0 * text.pos.lat.to_radians().cos())),
335 lat: text.pos.lat,
336 });
337 (p2.x - projection.project(text.pos).x).abs()
338 }
339 }
340 }
341
342 fn get_text_rect(&self, text: &Text, projection: &MapProjection, ctx: &egui::Context) -> Rect {
343 let font_size = self.get_font_size(text, projection);
344 let galley = ctx
345 .debug_painter()
346 .layout_job(egui::text::LayoutJob::simple(
347 text.text.clone(),
348 FontId::proportional(font_size),
349 text.color,
350 f32::INFINITY,
351 ));
352 let screen_pos = projection.project(text.pos);
353 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()))
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn text_layer_serde() {
363 let mut layer = TextLayer {
364 mode: TextLayerMode::Modify,
365 ..TextLayer::default()
366 };
367 layer.texts.push(Text {
368 text: "Hello".to_string(),
369 pos: GeoPos { lon: 1.0, lat: 2.0 },
370 size: TextSize::Static(14.0),
371 color: Color32::from_rgb(0, 0, 255),
372 background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
373 });
374
375 let json = serde_json::to_string(&layer).unwrap();
376
377 assert!(json.contains(r##""texts":[{"text":"Hello","pos":{"lon":1.0,"lat":2.0},"size":{"Static":14.0},"color":"#0000ffff","background":"#ff000080""##));
379
380 assert!(!json.contains("mode"));
382 assert!(!json.contains("new_text_properties"));
383 assert!(!json.contains("editing"));
384 assert!(!json.contains("dragged_text_index"));
385
386 let deserialized: TextLayer = serde_json::from_str(&json).unwrap();
387
388 assert_eq!(deserialized.texts.len(), 1);
390 assert_eq!(deserialized.texts[0].text, "Hello");
391 assert_eq!(deserialized.texts[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
392 assert_eq!(deserialized.texts[0].size, TextSize::Static(14.0));
393 assert_eq!(deserialized.texts[0].color, Color32::from_rgb(0, 0, 255));
394 assert_eq!(
395 deserialized.texts[0].background,
396 Color32::from_rgba_unmultiplied(255, 0, 0, 128)
397 );
398
399 let default_layer = TextLayer::default();
401 assert_eq!(deserialized.mode, default_layer.mode);
402 assert_eq!(
403 deserialized.new_text_properties,
404 default_layer.new_text_properties
405 );
406 assert!(deserialized.editing.is_none());
407 assert!(deserialized.dragged_text_index.is_none());
408 }
409
410 #[cfg(feature = "geojson")]
411 mod geojson_tests {
412 use super::*;
413
414 #[test]
415 fn text_layer_geojson() {
416 let mut layer = TextLayer::default();
417 layer.texts.push(Text {
418 text: "Hello".to_string(),
419 pos: (10.0, 20.0).into(),
420 size: TextSize::Static(14.0),
421 color: Color32::from_rgb(0, 0, 255),
422 background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
423 });
424
425 let geojson_str = layer.to_geojson_str().unwrap();
426 let mut new_layer = TextLayer::default();
427 new_layer.from_geojson_str(&geojson_str).unwrap();
428
429 assert_eq!(new_layer.texts.len(), 1);
430 assert_eq!(layer.texts[0], new_layer.texts[0]);
431 }
432 }
433}