1use crate::layers::{
47 Layer, default_opacity, dist_sq_to_segment, projection_factor, segments_intersect,
48 serde_color32, serde_stroke,
49};
50use crate::projection::{GeoPos, MapProjection};
51use egui::{Color32, Mesh, Painter, Pos2, Response, Shape, Stroke};
52use log::warn;
53use serde::{Deserialize, Serialize};
54use std::any::Any;
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum AreaMode {
59 #[default]
61 Disabled,
62 Modify,
64 ModifySelected,
66}
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub enum AreaShape {
71 Polygon(Vec<GeoPos>),
73 Circle {
75 center: GeoPos,
77 radius: f64,
79 points: Option<i64>,
81 },
82}
83
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
86pub enum FillType {
87 None,
89 #[default]
91 Solid,
92 Hatching,
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98pub struct Area {
99 pub shape: AreaShape,
101
102 #[serde(with = "serde_stroke")]
104 pub stroke: Stroke,
105
106 #[serde(with = "serde_color32")]
108 pub fill: Color32,
109
110 #[serde(default)]
112 pub fill_type: FillType,
113}
114
115#[derive(Clone, Debug)]
117enum DraggedObject {
118 PolygonNode {
119 area_index: usize,
120 node_index: usize,
121 },
122 CircleCenter {
123 area_index: usize,
124 },
125 CircleRadius {
126 area_index: usize,
127 },
128}
129
130#[derive(Clone, Serialize, Deserialize)]
132#[serde(default)]
133pub struct AreaLayer {
134 areas: Vec<Area>,
135
136 #[serde(skip)]
137 pub node_radius: f32,
139
140 #[serde(skip)]
141 pub node_fill: Color32,
143
144 #[serde(skip)]
145 pub mode: AreaMode,
147
148 #[serde(skip)]
149 dragged_object: Option<DraggedObject>,
150
151 #[serde(skip)]
152 hovered_object: Option<DraggedObject>,
153
154 #[serde(default = "default_opacity")]
156 pub opacity: f32,
157
158 #[serde(skip)]
159 pub selected_area: Option<usize>,
161}
162
163impl Default for AreaLayer {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl AreaLayer {
170 #[must_use]
172 pub fn new() -> Self {
173 Self {
174 areas: Vec::new(),
175 node_radius: 5.0,
176 node_fill: Color32::from_rgb(0, 128, 0),
177 mode: AreaMode::default(),
178 dragged_object: None,
179 hovered_object: None,
180 opacity: 1.0,
181 selected_area: None,
182 }
183 }
184
185 pub fn add_area(&mut self, area: Area) {
187 self.areas.push(area);
188 }
189
190 #[must_use]
192 pub fn areas(&self) -> &Vec<Area> {
193 &self.areas
194 }
195
196 pub fn areas_mut(&mut self) -> &mut Vec<Area> {
198 &mut self.areas
199 }
200
201 #[cfg(feature = "geojson")]
203 pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
204 let features: Vec<geojson::Feature> = self
205 .areas
206 .clone()
207 .into_iter()
208 .map(geojson::Feature::from)
209 .collect();
210 let mut foreign_members = serde_json::Map::new();
211 foreign_members.insert(
212 "opacity".to_string(),
213 serde_json::Value::from(f64::from(self.opacity)),
214 );
215
216 let feature_collection = geojson::FeatureCollection {
217 bbox: None,
218 features,
219 foreign_members: Some(foreign_members),
220 };
221 serde_json::to_string(&feature_collection)
222 }
223
224 #[cfg(feature = "geojson")]
226 pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
227 let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
228 let new_areas: Vec<Area> = feature_collection
229 .features
230 .into_iter()
231 .filter_map(|f| Area::try_from(f).ok())
232 .collect();
233 self.areas.extend(new_areas);
234
235 if let Some(foreign_members) = feature_collection.foreign_members
236 && let Some(value) = foreign_members.get("opacity")
237 && let Some(opacity) = value.as_f64()
238 {
239 self.opacity = opacity as f32;
240 }
241 Ok(())
242 }
243
244 fn handle_modify_input(
245 &mut self,
246 response: &Response,
247 projection: &MapProjection,
248 limit_to_area: Option<usize>,
249 ) -> bool {
250 self.hovered_object = response
251 .hover_pos()
252 .and_then(|pos| self.find_object_at(pos, projection, limit_to_area));
253
254 if response.double_clicked()
255 && let Some(pointer_pos) = response.interact_pointer_pos()
256 {
257 if self
259 .find_node_at(pointer_pos, projection, limit_to_area)
260 .is_none()
261 && let Some((area_idx, node_idx)) =
262 self.find_line_segment_at(pointer_pos, projection, limit_to_area)
263 && let Some(area) = self.areas.get_mut(area_idx)
264 && let AreaShape::Polygon(points) = &mut area.shape
265 {
266 let p1_screen = projection.project(points[node_idx]);
267 let p2_screen = projection.project(points[(node_idx + 1) % points.len()]);
268
269 let t = projection_factor(pointer_pos, p1_screen, p2_screen);
270
271 let new_pos_screen = p1_screen.lerp(p2_screen, t);
273 let new_pos_geo = projection.unproject(new_pos_screen);
274
275 points.insert(node_idx + 1, new_pos_geo);
276
277 return response.hovered();
279 }
280 }
281
282 if response.drag_started()
283 && let Some(pointer_pos) = response.interact_pointer_pos()
284 {
285 self.dragged_object = self.find_object_at(pointer_pos, projection, limit_to_area);
286 }
287
288 if response.dragged()
289 && let Some(dragged_object) = self.dragged_object.clone()
290 && let Some(pointer_pos) = response.interact_pointer_pos()
291 {
292 match dragged_object {
293 DraggedObject::PolygonNode {
294 area_index,
295 node_index,
296 } => {
297 if self.is_move_valid(area_index, node_index, pointer_pos, projection)
298 && let Some(area) = self.areas.get_mut(area_index)
299 {
300 let mut revert_info = None;
301 if let AreaShape::Polygon(points) = &mut area.shape
302 && let Some(node) = points.get_mut(node_index)
303 {
304 let old_pos = *node;
305 *node = projection.unproject(pointer_pos);
306 revert_info = Some(old_pos);
307 }
308
309 if let Some(old_pos) = revert_info
310 && !area.can_triangulate(projection)
311 {
312 warn!("Triangulation failed, cancelling drag");
313 self.dragged_object = None;
314 if let AreaShape::Polygon(points) = &mut area.shape {
315 points[node_index] = old_pos;
316 }
317 }
318 }
319 }
320 DraggedObject::CircleCenter { area_index } => {
321 if let Some(area) = self.areas.get_mut(area_index) {
322 let mut revert_center = None;
323 if let AreaShape::Circle { center, .. } = &mut area.shape {
324 revert_center = Some(*center);
325 *center = projection.unproject(pointer_pos);
326 }
327
328 if let Some(old_center) = revert_center
329 && !area.can_triangulate(projection)
330 {
331 warn!("Triangulation failed, cancelling drag");
332 self.dragged_object = None;
333 if let AreaShape::Circle { center, .. } = &mut area.shape {
334 *center = old_center;
335 }
336 }
337 }
338 }
339 DraggedObject::CircleRadius { area_index } => {
340 if let Some(area) = self.areas.get_mut(area_index) {
341 let mut revert_radius = None;
342 if let AreaShape::Circle {
343 center,
344 radius,
345 points: _,
346 } = &mut area.shape
347 {
348 revert_radius = Some(*radius);
349 let center_screen = projection.project(*center);
351 let new_radius_pixels = pointer_pos.distance(center_screen);
352 let new_edge_screen =
353 center_screen + egui::vec2(new_radius_pixels, 0.0);
354 let new_edge_geo = projection.unproject(new_edge_screen);
355
356 let distance_lon = (new_edge_geo.lon - center.lon).abs()
358 * (111_320.0 * center.lat.to_radians().cos());
359 let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
360 *radius = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
361 }
362
363 if let Some(old_radius) = revert_radius
364 && !area.can_triangulate(projection)
365 {
366 warn!("Triangulation failed, cancelling drag");
367 self.dragged_object = None;
368 if let AreaShape::Circle { radius, .. } = &mut area.shape {
369 *radius = old_radius;
370 }
371 }
372 }
373 }
374 }
375 }
376
377 if response.drag_stopped() {
378 self.dragged_object = None;
379 }
380
381 let is_dragging = self.dragged_object.is_some();
382
383 if is_dragging {
384 response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
385 } else if self.hovered_object.is_some() {
386 response.ctx.set_cursor_icon(egui::CursorIcon::Grab);
387 }
388
389 is_dragging || (response.hovered() && self.hovered_object.is_some())
390 }
391
392 fn find_object_at(
393 &self,
394 screen_pos: Pos2,
395 projection: &MapProjection,
396 limit_to_area: Option<usize>,
397 ) -> Option<DraggedObject> {
398 let click_tolerance_sq = (self.node_radius * 3.0).powi(2);
399
400 for (area_idx, area) in self.areas.iter().enumerate().rev() {
401 if let Some(limit_idx) = limit_to_area
402 && area_idx != limit_idx
403 {
404 continue;
405 }
406 match &area.shape {
407 AreaShape::Polygon(points) => {
408 for (node_idx, node) in points.iter().enumerate() {
409 let node_screen_pos = projection.project(*node);
410 if node_screen_pos.distance_sq(screen_pos) < click_tolerance_sq {
411 return Some(DraggedObject::PolygonNode {
412 area_index: area_idx,
413 node_index: node_idx,
414 });
415 }
416 }
417 }
418 AreaShape::Circle {
419 center,
420 radius,
421 points: _,
422 } => {
423 let center_screen = projection.project(*center);
424
425 let point_on_circle_geo = GeoPos {
427 lon: center.lon + (radius / (111_320.0 * center.lat.to_radians().cos())),
428 lat: center.lat,
429 };
430 let point_on_circle_screen = projection.project(point_on_circle_geo);
431 let radius_pixels = center_screen.distance(point_on_circle_screen);
432
433 let distance_to_edge =
435 (center_screen.distance(screen_pos) - radius_pixels).abs();
436 if distance_to_edge < self.node_radius * 2.0 {
437 return Some(DraggedObject::CircleRadius {
438 area_index: area_idx,
439 });
440 }
441
442 if center_screen.distance_sq(screen_pos) < click_tolerance_sq {
444 return Some(DraggedObject::CircleCenter {
445 area_index: area_idx,
446 });
447 }
448 }
449 }
450 }
451
452 None
453 }
454
455 fn find_node_at(
456 &self,
457 screen_pos: Pos2,
458 projection: &MapProjection,
459 limit_to_area: Option<usize>,
460 ) -> Option<(usize, usize)> {
461 match self.find_object_at(screen_pos, projection, limit_to_area) {
462 Some(DraggedObject::PolygonNode {
463 area_index,
464 node_index,
465 }) => Some((area_index, node_index)),
466 _ => None,
467 }
468 }
469
470 fn find_line_segment_at(
471 &self,
472 screen_pos: Pos2,
473 projection: &MapProjection,
474 limit_to_area: Option<usize>,
475 ) -> Option<(usize, usize)> {
476 let click_tolerance = (self.node_radius * 2.0).powi(2);
477
478 for (area_idx, area) in self.areas.iter().enumerate().rev() {
479 if let Some(limit_idx) = limit_to_area
480 && area_idx != limit_idx
481 {
482 continue;
483 }
484 if let AreaShape::Polygon(points) = &area.shape {
485 if points.len() < 2 {
486 continue;
487 }
488 for i in 0..points.len() {
489 let p1 = projection.project(points[i]);
490 let p2 = projection.project(points[(i + 1) % points.len()]);
491
492 if dist_sq_to_segment(screen_pos, p1, p2) < click_tolerance {
493 return Some((area_idx, i));
494 }
495 }
496 }
497 }
498 None
499 }
500
501 fn is_move_valid(
503 &self,
504 area_idx: usize,
505 node_idx: usize,
506 new_screen_pos: Pos2,
507 projection: &MapProjection,
508 ) -> bool {
509 let area = if let Some(area) = self.areas.get(area_idx) {
510 area
511 } else {
512 return false; };
514
515 let points = match &area.shape {
516 AreaShape::Polygon(points) => points,
517 _ => return true, };
519
520 if points.len() < 3 {
521 return true;
522 }
523 let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
524
525 let n = screen_points.len();
526 let prev_node_idx = (node_idx + n - 1) % n;
527 let next_node_idx = (node_idx + 1) % n;
528
529 let new_edge1 = (screen_points[prev_node_idx], new_screen_pos);
531 let new_edge2 = (new_screen_pos, screen_points[next_node_idx]);
532
533 for i in 0..n {
534 let p1_idx = i;
535 let p2_idx = (i + 1) % n;
536
537 if p1_idx == node_idx || p2_idx == node_idx {
539 continue;
540 }
541
542 let edge_to_check = (screen_points[p1_idx], screen_points[p2_idx]);
543
544 if p1_idx != prev_node_idx
546 && p2_idx != prev_node_idx
547 && segments_intersect(new_edge1.0, new_edge1.1, edge_to_check.0, edge_to_check.1)
548 {
549 return false;
550 }
551
552 if p1_idx != next_node_idx
554 && p2_idx != next_node_idx
555 && segments_intersect(new_edge2.0, new_edge2.1, edge_to_check.0, edge_to_check.1)
556 {
557 return false;
558 }
559 }
560
561 true
562 }
563}
564
565impl Area {
566 fn can_triangulate(&self, projection: &MapProjection) -> bool {
568 let points = self.get_points(projection);
569 let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
570
571 if screen_points.len() < 3 {
572 return true;
573 }
574
575 let flat_points: Vec<f64> = screen_points
576 .iter()
577 .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
578 .collect();
579 earcutr::earcut(&flat_points, &[], 2).is_ok()
580 }
581
582 fn get_points(&self, projection: &MapProjection) -> Vec<GeoPos> {
584 match &self.shape {
585 AreaShape::Polygon(points) => points.clone(),
586 AreaShape::Circle {
587 center,
588 radius,
589 points,
590 } => {
591 let center_geo = *center;
593 let point_on_circle_geo = GeoPos {
594 lon: center_geo.lon
595 + (radius / (111_320.0 * center_geo.lat.to_radians().cos())),
596 lat: center_geo.lat,
597 };
598 let center_screen = projection.project(center_geo);
599 let point_on_circle_screen = projection.project(point_on_circle_geo);
600 let radius_pixels = center_screen.distance(point_on_circle_screen);
601
602 let num_points = if let Some(points) = points {
603 *points
604 } else {
605 (f64::from(radius_pixels) * 2.0 * std::f64::consts::PI / 10.0).ceil() as i64
608 };
609 let mut circle_points = Vec::with_capacity(num_points as usize);
610
611 for i in 0..num_points {
612 let angle = (i as f64 / num_points as f64) * 2.0 * std::f64::consts::PI;
613 let point_screen = center_screen
614 + egui::vec2(
615 radius_pixels * angle.cos() as f32,
616 radius_pixels * angle.sin() as f32,
617 );
618 circle_points.push(projection.unproject(point_screen));
619 }
620 circle_points
621 }
622 }
623 }
624
625 pub fn contains(&self, pos: Pos2, projection: &MapProjection) -> bool {
627 match &self.shape {
628 AreaShape::Circle { center, radius, .. } => {
629 let center_screen = projection.project(*center);
630 let point_on_circle_geo = GeoPos {
631 lon: center.lon + (radius / (111_320.0 * center.lat.to_radians().cos())),
632 lat: center.lat,
633 };
634 let point_on_circle_screen = projection.project(point_on_circle_geo);
635 let radius_pixels = center_screen.distance(point_on_circle_screen);
636 center_screen.distance_sq(pos) <= radius_pixels.powi(2)
637 }
638 AreaShape::Polygon(_) => {
639 let points = self.get_points(projection);
640 let screen_points: Vec<Pos2> =
641 points.iter().map(|p| projection.project(*p)).collect();
642 if screen_points.len() < 3 {
643 return false;
644 }
645 let flat_points: Vec<f64> = screen_points
646 .iter()
647 .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
648 .collect();
649 if let Ok(indices) = earcutr::earcut(&flat_points, &[], 2) {
650 for chunk in indices.chunks_exact(3) {
651 let p1 = screen_points[chunk[0]];
652 let p2 = screen_points[chunk[1]];
653 let p3 = screen_points[chunk[2]];
654 if point_in_triangle(pos, p1, p2, p3) {
655 return true;
656 }
657 }
658 }
659 false
660 }
661 }
662 }
663}
664
665fn point_in_triangle(p: Pos2, a: Pos2, b: Pos2, c: Pos2) -> bool {
666 let d1 = sign(p, a, b);
667 let d2 = sign(p, b, c);
668 let d3 = sign(p, c, a);
669
670 let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
671 let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
672
673 !(has_neg && has_pos)
674}
675
676fn sign(p1: Pos2, p2: Pos2, p3: Pos2) -> f32 {
677 (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
678}
679
680fn generate_hatching_lines(screen_points: &[Pos2], spacing: f32, angle: f32) -> Vec<(Pos2, Pos2)> {
688 if screen_points.len() < 3 || spacing <= 0.0 {
689 return Vec::new();
690 }
691
692 let dir = egui::vec2(angle.cos(), angle.sin());
694 let perp = egui::vec2(-angle.sin(), angle.cos());
695
696 let mut min_perp = f32::MAX;
698 let mut max_perp = f32::MIN;
699 for p in screen_points {
700 let d = p.to_vec2().dot(perp);
701 min_perp = min_perp.min(d);
702 max_perp = max_perp.max(d);
703 }
704
705 let n = screen_points.len();
706 let mut segments = Vec::new();
707
708 let mut offset = min_perp + spacing;
710 while offset < max_perp {
711 let line_origin = Pos2::ZERO + perp * offset;
713
714 let mut t_values: Vec<f32> = Vec::new();
716 for i in 0..n {
717 let a = screen_points[i];
718 let b = screen_points[(i + 1) % n];
719 let edge = b - a;
720
721 let denom = edge.x * dir.y - edge.y * dir.x;
724 if denom.abs() < 1e-9 {
725 continue; }
727
728 let diff = a - line_origin;
729 let t_edge = -(diff.x * dir.y - diff.y * dir.x) / denom;
730
731 if (0.0..=1.0).contains(&t_edge) {
732 let t_line = if dir.x.abs() > dir.y.abs() {
734 (a.x - line_origin.x + t_edge * edge.x) / dir.x
735 } else {
736 (a.y - line_origin.y + t_edge * edge.y) / dir.y
737 };
738 t_values.push(t_line);
739 }
740 }
741
742 t_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
744
745 for pair in t_values.chunks_exact(2) {
747 let p1 = line_origin + dir * pair[0];
748 let p2 = line_origin + dir * pair[1];
749 segments.push((p1, p2));
750 }
751
752 offset += spacing;
753 }
754
755 segments
756}
757
758impl Layer for AreaLayer {
759 fn as_any(&self) -> &dyn Any {
760 self
761 }
762
763 fn as_any_mut(&mut self) -> &mut dyn Any {
764 self
765 }
766
767 fn opacity(&self) -> f32 {
768 self.opacity
769 }
770
771 fn set_opacity(&mut self, opacity: f32) {
772 self.opacity = opacity;
773 }
774
775 fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
776 match self.mode {
777 AreaMode::Disabled => {
778 self.hovered_object = None;
779 false
780 }
781 AreaMode::Modify => self.handle_modify_input(response, projection, None),
782 AreaMode::ModifySelected => {
783 if response.clicked()
784 && let Some(pointer_pos) = response.interact_pointer_pos()
785 {
786 let clicked_area_idx =
788 self.areas.iter().enumerate().rev().find_map(|(idx, area)| {
789 if area.contains(pointer_pos, projection) {
790 Some(idx)
791 } else {
792 None
793 }
794 });
795
796 if clicked_area_idx != self.selected_area {
797 self.selected_area = clicked_area_idx;
798 return true;
799 }
800 }
801
802 if let Some(selected_idx) = self.selected_area {
803 self.handle_modify_input(response, projection, Some(selected_idx))
804 } else {
805 false
806 }
807 }
808 }
809 }
810
811 fn draw(&self, painter: &Painter, projection: &MapProjection) {
812 for (area_idx, area) in self.areas.iter().enumerate() {
813 let points = area.get_points(projection);
814 let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
815
816 if screen_points.len() >= 3 {
818 let is_selected =
819 self.mode == AreaMode::ModifySelected && self.selected_area == Some(area_idx);
820 let stroke = if is_selected {
821 Stroke {
822 width: area.stroke.width * 2.0,
823 color: area.stroke.color.gamma_multiply(self.opacity),
824 }
825 } else {
826 Stroke {
827 color: area.stroke.color.gamma_multiply(self.opacity),
828 ..area.stroke
829 }
830 };
831
832 let path_shape = Shape::Path(egui::epaint::PathShape {
834 points: screen_points.clone(),
835 closed: true,
836 fill: Color32::TRANSPARENT,
837 stroke: stroke.into(),
838 });
839 painter.add(path_shape);
840
841 match area.fill_type {
842 FillType::None => { }
843 FillType::Solid => {
844 let flat_points: Vec<f64> = screen_points
846 .iter()
847 .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
848 .collect();
849 match earcutr::earcut(&flat_points, &[], 2) {
850 Ok(indices) => {
851 let mesh = Mesh {
852 vertices: screen_points
853 .iter()
854 .map(|p| egui::epaint::Vertex {
855 pos: *p,
856 uv: Default::default(),
857 color: area.fill.gamma_multiply(self.opacity),
858 })
859 .collect(),
860 indices: indices.into_iter().map(|i| i as u32).collect(),
861 ..Default::default()
862 };
863 painter.add(Shape::Mesh(mesh.into()));
864 }
865 Err(e) => {
866 warn!("Failed to triangulate area: {e:?}");
867 }
868 }
869 }
870 FillType::Hatching => {
871 let segments = generate_hatching_lines(
872 &screen_points,
873 8.0,
874 std::f32::consts::FRAC_PI_4,
875 );
876 for (a, b) in segments {
877 painter.line_segment(
878 [a, b],
879 Stroke {
880 color: area.stroke.color.gamma_multiply(self.opacity),
881 ..area.stroke
882 },
883 );
884 }
885 }
886 }
887 } else {
888 warn!("Invalid amount of points in area. {area:?}");
889 }
890
891 let show_nodes = self.mode == AreaMode::Modify
893 || (self.mode == AreaMode::ModifySelected && self.selected_area == Some(area_idx));
894 if show_nodes {
895 match &area.shape {
896 AreaShape::Polygon(_) => {
897 for (node_idx, point) in screen_points.iter().enumerate() {
898 painter.circle_filled(
899 *point,
900 self.node_radius,
901 self.node_fill.gamma_multiply(self.opacity),
902 );
903
904 if let Some(DraggedObject::PolygonNode {
905 area_index,
906 node_index,
907 }) = self.hovered_object
908 && area_index == area_idx && node_index == node_idx {
909 painter.circle_stroke(
910 *point,
911 self.node_radius * 3.0,
912 Stroke::new(
913 1.0,
914 self.node_fill.gamma_multiply(self.opacity),
915 ),
916 );
917 }
918 }
919 }
920 AreaShape::Circle {
921 center,
922 radius,
923 points: _,
924 } => {
925 let center_screen = projection.project(*center);
926
927 let point_on_circle_geo = GeoPos {
929 lon: center.lon
930 + (radius / (111_320.0 * center.lat.to_radians().cos())),
931 lat: center.lat,
932 };
933 let point_on_circle_screen = projection.project(point_on_circle_geo);
934 let radius_pixels = center_screen.distance(point_on_circle_screen);
935
936 painter.circle_filled(
937 center_screen,
938 self.node_radius,
939 self.node_fill.gamma_multiply(self.opacity),
940 );
941
942 if let Some(DraggedObject::CircleCenter { area_index }) =
943 self.hovered_object
944 && area_index == area_idx {
945 painter.circle_stroke(
946 center_screen,
947 self.node_radius * 3.0,
948 Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
949 );
950 }
951
952 let radius_handle_pos = center_screen + egui::vec2(radius_pixels, 0.0);
953 painter.circle_filled(
954 radius_handle_pos,
955 self.node_radius,
956 self.node_fill.gamma_multiply(self.opacity),
957 );
958
959 if let Some(DraggedObject::CircleRadius { area_index }) =
960 self.hovered_object
961 && area_index == area_idx {
962 painter.circle_stroke(
963 radius_handle_pos,
964 self.node_radius * 2.0,
965 Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
966 );
967 }
968 }
969 }
970 }
971 }
972 }
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978 use crate::projection::MapProjection;
979 use egui::{Rect, pos2, vec2};
980
981 fn dummy_projection() -> MapProjection {
983 MapProjection::new(
984 10, (0.0, 0.0).into(), Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 1000.0)),
987 )
988 }
989
990 #[test]
991 fn area_layer_new() {
992 let layer = AreaLayer::default();
993 assert_eq!(layer.mode, AreaMode::Disabled);
994 assert!(layer.areas.is_empty());
995 assert_eq!(layer.node_radius, 5.0);
996 }
997
998 #[test]
999 fn area_layer_add_area() {
1000 let mut layer = AreaLayer::default();
1001 assert_eq!(layer.areas.len(), 0);
1002
1003 layer.add_area(Area {
1004 shape: AreaShape::Polygon(vec![
1005 (0.0, 0.0).into(),
1006 (1.0, 0.0).into(),
1007 (0.0, 1.0).into(),
1008 ]),
1009 stroke: Default::default(),
1010 fill: Default::default(),
1011 fill_type: Default::default(),
1012 });
1013
1014 assert_eq!(layer.areas.len(), 1);
1015 }
1016
1017 #[test]
1018 fn circle_get_points_with_fixed_number() {
1019 let projection = dummy_projection();
1020 let area = Area {
1021 shape: AreaShape::Circle {
1022 center: (0.0, 0.0).into(),
1023 radius: 1000.0,
1024 points: Some(16),
1025 },
1026 stroke: Default::default(),
1027 fill: Default::default(),
1028 fill_type: Default::default(),
1029 };
1030
1031 let points = area.get_points(&projection);
1032 assert_eq!(points.len(), 16);
1033 }
1034
1035 #[test]
1036 fn find_object_at_empty() {
1037 let layer = AreaLayer::default();
1038 let projection = dummy_projection();
1039 let position = pos2(100.0, 100.0);
1040
1041 assert!(layer.find_object_at(position, &projection, None).is_none());
1042 }
1043
1044 #[test]
1045 fn find_object_at_polygon_node() {
1046 let projection = dummy_projection();
1047 let mut layer = AreaLayer::default();
1048 let geo_pos = projection.unproject(pos2(100.0, 100.0));
1049
1050 layer.add_area(Area {
1051 shape: AreaShape::Polygon(vec![geo_pos]),
1052 stroke: Default::default(),
1053 fill: Default::default(),
1054 fill_type: Default::default(),
1055 });
1056
1057 let found = layer.find_object_at(pos2(100.0, 100.0), &projection, None);
1059 assert!(matches!(
1060 found,
1061 Some(DraggedObject::PolygonNode {
1062 area_index: 0,
1063 node_index: 0
1064 })
1065 ));
1066
1067 let found_nearby = layer.find_object_at(pos2(101.0, 101.0), &projection, None);
1069 assert!(matches!(
1070 found_nearby,
1071 Some(DraggedObject::PolygonNode {
1072 area_index: 0,
1073 node_index: 0
1074 })
1075 ));
1076
1077 let not_found = layer.find_object_at(pos2(200.0, 200.0), &projection, None);
1079 assert!(not_found.is_none());
1080 }
1081
1082 #[test]
1083 fn area_layer_serde() {
1084 let mut layer = AreaLayer::default();
1085 layer.add_area(Area {
1086 shape: AreaShape::Polygon(vec![(0.0, 0.0).into()]),
1087 stroke: Stroke::new(1.0, Color32::RED),
1088 fill: Color32::BLUE,
1089 fill_type: Default::default(),
1090 });
1091
1092 let json = serde_json::to_string(&layer).unwrap();
1093 let deserialized: AreaLayer = serde_json::from_str(&json).unwrap();
1094
1095 assert_eq!(deserialized.areas.len(), 1);
1096 assert_eq!(deserialized.mode, AreaMode::Disabled); }
1098
1099 #[test]
1100 fn test_can_triangulate_valid() {
1101 let projection = dummy_projection();
1102 let area = Area {
1103 shape: AreaShape::Polygon(vec![
1104 (0.0, 0.0).into(),
1105 (10.0, 0.0).into(),
1106 (0.0, 10.0).into(),
1107 ]),
1108 stroke: Default::default(),
1109 fill: Default::default(),
1110 fill_type: Default::default(),
1111 };
1112
1113 assert!(area.can_triangulate(&projection));
1114 }
1115
1116 #[test]
1117 fn test_can_triangulate_insufficient_points() {
1118 let projection = dummy_projection();
1119 let area = Area {
1120 shape: AreaShape::Polygon(vec![(0.0, 0.0).into(), (10.0, 0.0).into()]),
1121 stroke: Default::default(),
1122 fill: Default::default(),
1123 fill_type: Default::default(),
1124 };
1125
1126 assert!(area.can_triangulate(&projection));
1129 }
1130
1131 #[cfg(feature = "geojson")]
1132 mod geojson_tests {
1133 use super::*;
1134
1135 #[test]
1136 fn area_layer_geojson_polygon() {
1137 let mut layer = AreaLayer::default();
1138 layer.add_area(Area {
1139 shape: AreaShape::Polygon(vec![
1140 (10.0, 20.0).into(),
1141 (30.0, 40.0).into(),
1142 (50.0, 60.0).into(),
1143 ]),
1144 stroke: Stroke::new(2.0, Color32::from_rgb(0, 0, 255)),
1145 fill: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
1146 fill_type: Default::default(),
1147 });
1148
1149 let geojson_str = layer.to_geojson_str().unwrap();
1150
1151 let mut new_layer = AreaLayer::default();
1152 new_layer.from_geojson_str(&geojson_str).unwrap();
1153
1154 assert_eq!(new_layer.areas.len(), 1);
1155 assert_eq!(layer.areas[0], new_layer.areas[0]);
1156 }
1157
1158 #[test]
1159 fn area_layer_geojson_circle() {
1160 let mut layer = AreaLayer::default();
1161 layer.add_area(Area {
1162 shape: AreaShape::Circle {
1163 center: (10.0, 20.0).into(),
1164 radius: 1000.0,
1165 points: Some(32),
1166 },
1167 stroke: Default::default(),
1168 fill: Default::default(),
1169 fill_type: Default::default(),
1170 });
1171
1172 let geojson_str = layer.to_geojson_str().unwrap();
1173 let mut new_layer = AreaLayer::default();
1174 new_layer.from_geojson_str(&geojson_str).unwrap();
1175
1176 assert_eq!(new_layer.areas.len(), 1);
1177 assert_eq!(layer.areas[0].shape, new_layer.areas[0].shape);
1178 }
1179 }
1180
1181 #[test]
1182 fn find_node_at_on_segment() {
1183 let projection = dummy_projection();
1184 let mut layer = AreaLayer::default();
1185
1186 let p1 = projection.unproject(pos2(100.0, 100.0));
1187 let p2 = projection.unproject(pos2(200.0, 100.0));
1188
1189 layer.add_area(Area {
1190 shape: AreaShape::Polygon(vec![p1, p2, projection.unproject(pos2(150.0, 200.0))]), stroke: Default::default(),
1192 fill: Default::default(),
1193 fill_type: Default::default(),
1194 });
1195
1196 let click_pos = pos2(150.0, 100.0);
1198
1199 assert!(layer.find_node_at(click_pos, &projection, None).is_none());
1201
1202 let segment = layer.find_line_segment_at(click_pos, &projection, None);
1204 assert!(segment.is_some());
1205 assert_eq!(segment.unwrap().0, 0); assert_eq!(segment.unwrap().1, 0);
1207 }
1208}