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