1use crate::layers::{Layer, 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)]
81#[derive(Default)]
82pub struct TextLayer {
83 texts: Vec<Text>,
84
85 #[serde(skip)]
87 pub mode: TextLayerMode,
88
89 #[serde(skip)]
91 pub new_text_properties: Text,
92
93 #[serde(skip)]
95 pub editing: Option<EditingText>,
96
97 #[serde(skip)]
98 dragged_text_index: Option<usize>,
99}
100
101impl TextLayer {
102 pub fn start_editing(&mut self, index: usize) {
104 if let Some(text) = self.texts.get(index) {
105 self.editing = Some(EditingText {
106 index: Some(index),
107 properties: text.clone(),
108 });
109 }
110 }
111
112 pub fn delete(&mut self, index: usize) {
114 if index < self.texts.len() {
115 self.texts.remove(index);
116 }
117 }
118
119 pub fn commit_edit(&mut self) {
121 if let Some(editing) = self.editing.take() {
122 if let Some(index) = editing.index {
123 if let Some(text) = self.texts.get_mut(index) {
125 *text = editing.properties;
126 }
127 } else {
128 self.texts.push(editing.properties);
130 }
131 }
132 }
133
134 pub fn cancel_edit(&mut self) {
136 self.editing = None;
137 }
138
139 #[cfg(feature = "geojson")]
141 pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
142 let features: Vec<geojson::Feature> = self
143 .texts
144 .clone()
145 .into_iter()
146 .map(geojson::Feature::from)
147 .collect();
148 let feature_collection = geojson::FeatureCollection {
149 bbox: None,
150 features,
151 foreign_members: None,
152 };
153 serde_json::to_string(&feature_collection)
154 }
155
156 #[cfg(feature = "geojson")]
158 pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
159 let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
160 let new_texts: Vec<Text> = feature_collection
161 .features
162 .into_iter()
163 .filter_map(|f| Text::try_from(f).ok())
164 .collect();
165 self.texts.extend(new_texts);
166 Ok(())
167 }
168
169 fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
170 if self.editing.is_some() {
171 return response.hovered();
174 }
175
176 if response.drag_started()
177 && let Some(pointer_pos) = response.interact_pointer_pos()
178 {
179 self.dragged_text_index = self.find_text_at(pointer_pos, projection, &response.ctx);
180 }
181
182 if response.dragged()
183 && let Some(text_index) = self.dragged_text_index
184 && let Some(text) = self.texts.get_mut(text_index)
185 && let Some(pointer_pos) = response.interact_pointer_pos()
186 {
187 text.pos = projection.unproject(pointer_pos);
188 }
189
190 if response.drag_stopped() {
191 self.dragged_text_index = None;
192 }
193
194 if self.dragged_text_index.is_some() {
196 response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
197 } else if let Some(hover_pos) = response.hover_pos() {
198 if self
199 .find_text_at(hover_pos, projection, &response.ctx)
200 .is_some()
201 {
202 response.ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
203 } else {
204 response.ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
205 }
206 }
207
208 if !response.dragged() && response.clicked() {
209 if let Some(pointer_pos) = response.interact_pointer_pos() {
211 if let Some(index) = self.find_text_at(pointer_pos, projection, &response.ctx) {
212 self.start_editing(index);
214 } else {
215 let geo_pos = projection.unproject(pointer_pos);
217 let mut properties = self.new_text_properties.clone();
218 properties.pos = geo_pos;
219 self.editing = Some(EditingText {
220 index: None,
221 properties,
222 });
223 }
224 }
225 }
226
227 response.hovered()
228 }
229
230 fn find_text_at(
232 &self,
233 screen_pos: Pos2,
234 projection: &MapProjection,
235 ctx: &egui::Context,
236 ) -> Option<usize> {
237 self.texts.iter().enumerate().rev().find_map(|(i, text)| {
238 let text_rect = self.get_text_rect(text, projection, ctx);
239 if text_rect.expand(5.0).contains(screen_pos) {
240 Some(i)
242 } else {
243 None
244 }
245 })
246 }
247}
248
249impl Layer for TextLayer {
250 fn as_any(&self) -> &dyn Any {
251 self
252 }
253
254 fn as_any_mut(&mut self) -> &mut dyn Any {
255 self
256 }
257
258 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
259 match self.mode {
260 TextLayerMode::Disabled => false,
261 TextLayerMode::Modify => self.handle_modify_input(response, projection),
262 }
263 }
264
265 fn draw(&self, painter: &Painter, projection: &MapProjection) {
266 for text in &self.texts {
267 let screen_pos = projection.project(text.pos);
268
269 let galley = painter.layout_no_wrap(
270 text.text.clone(),
272 FontId::proportional(self.get_font_size(text, projection)),
273 text.color,
274 );
275
276 let rect =
277 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()));
278
279 painter.rect_filled(rect.expand(2.0), 3.0, text.background);
280 painter.galley(rect.min, galley, Color32::TRANSPARENT);
281 }
282 }
283}
284
285impl TextLayer {
286 fn get_font_size(&self, text: &Text, projection: &MapProjection) -> f32 {
287 match text.size {
288 TextSize::Static(size) => size,
289 TextSize::Relative(size_in_meters) => {
290 let p2 = projection.project(GeoPos {
291 lon: text.pos.lon
292 + (f64::from(size_in_meters)
293 / (111_320.0 * text.pos.lat.to_radians().cos())),
294 lat: text.pos.lat,
295 });
296 (p2.x - projection.project(text.pos).x).abs()
297 }
298 }
299 }
300
301 fn get_text_rect(&self, text: &Text, projection: &MapProjection, ctx: &egui::Context) -> Rect {
302 let font_size = self.get_font_size(text, projection);
303 let galley = ctx
304 .debug_painter()
305 .layout_job(egui::text::LayoutJob::simple(
306 text.text.clone(),
307 FontId::proportional(font_size),
308 text.color,
309 f32::INFINITY,
310 ));
311 let screen_pos = projection.project(text.pos);
312 Align2::CENTER_CENTER.anchor_rect(Rect::from_min_size(screen_pos, galley.size()))
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn text_layer_serde() {
322 let mut layer = TextLayer::default();
323 layer.mode = TextLayerMode::Modify; layer.texts.push(Text {
325 text: "Hello".to_string(),
326 pos: GeoPos { lon: 1.0, lat: 2.0 },
327 size: TextSize::Static(14.0),
328 color: Color32::from_rgb(0, 0, 255),
329 background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
330 });
331
332 let json = serde_json::to_string(&layer).unwrap();
333
334 assert!(json.contains(r##""texts":[{"text":"Hello","pos":{"lon":1.0,"lat":2.0},"size":{"Static":14.0},"color":"#0000ffff","background":"#ff000080""##));
336
337 assert!(!json.contains("mode"));
339 assert!(!json.contains("new_text_properties"));
340 assert!(!json.contains("editing"));
341 assert!(!json.contains("dragged_text_index"));
342
343 let deserialized: TextLayer = serde_json::from_str(&json).unwrap();
344
345 assert_eq!(deserialized.texts.len(), 1);
347 assert_eq!(deserialized.texts[0].text, "Hello");
348 assert_eq!(deserialized.texts[0].pos, GeoPos { lon: 1.0, lat: 2.0 });
349 assert_eq!(deserialized.texts[0].size, TextSize::Static(14.0));
350 assert_eq!(deserialized.texts[0].color, Color32::from_rgb(0, 0, 255));
351 assert_eq!(
352 deserialized.texts[0].background,
353 Color32::from_rgba_unmultiplied(255, 0, 0, 128)
354 );
355
356 let default_layer = TextLayer::default();
358 assert_eq!(deserialized.mode, default_layer.mode);
359 assert_eq!(
360 deserialized.new_text_properties,
361 default_layer.new_text_properties
362 );
363 assert!(deserialized.editing.is_none());
364 assert!(deserialized.dragged_text_index.is_none());
365 }
366
367 #[cfg(feature = "geojson")]
368 mod geojson_tests {
369 use super::*;
370
371 #[test]
372 fn text_layer_geojson() {
373 let mut layer = TextLayer::default();
374 layer.texts.push(Text {
375 text: "Hello".to_string(),
376 pos: (10.0, 20.0).into(),
377 size: TextSize::Static(14.0),
378 color: Color32::from_rgb(0, 0, 255),
379 background: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
380 });
381
382 let geojson_str = layer.to_geojson_str().unwrap();
383 let mut new_layer = TextLayer::default();
384 new_layer.from_geojson_str(&geojson_str).unwrap();
385
386 assert_eq!(new_layer.texts.len(), 1);
387 assert_eq!(layer.texts[0], new_layer.texts[0]);
388 }
389 }
390}