use crate::layers::{
Layer, dist_sq_to_segment, projection_factor, segments_intersect, serde_color32, serde_stroke,
};
use crate::projection::{GeoPos, MapProjection};
use egui::{Color32, Mesh, Painter, Pos2, Response, Shape, Stroke};
use log::warn;
use serde::{Deserialize, Serialize};
use std::any::Any;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AreaMode {
#[default]
Disabled,
Modify,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum AreaShape {
Polygon(Vec<GeoPos>),
Circle {
center: GeoPos,
radius: f64,
points: Option<i64>,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum FillType {
None,
#[default]
Solid,
Hatching,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Area {
pub shape: AreaShape,
#[serde(with = "serde_stroke")]
pub stroke: Stroke,
#[serde(with = "serde_color32")]
pub fill: Color32,
#[serde(default)]
pub fill_type: FillType,
}
#[derive(Clone, Debug)]
enum DraggedObject {
PolygonNode {
area_index: usize,
node_index: usize,
},
CircleCenter {
area_index: usize,
},
CircleRadius {
area_index: usize,
},
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AreaLayer {
areas: Vec<Area>,
#[serde(skip)]
pub node_radius: f32,
#[serde(skip)]
pub node_fill: Color32,
#[serde(skip)]
pub mode: AreaMode,
#[serde(skip)]
dragged_object: Option<DraggedObject>,
}
impl Default for AreaLayer {
fn default() -> Self {
Self::new()
}
}
impl AreaLayer {
#[must_use]
pub fn new() -> Self {
Self {
areas: Vec::new(),
node_radius: 5.0,
node_fill: Color32::from_rgb(0, 128, 0),
mode: AreaMode::default(),
dragged_object: None,
}
}
pub fn add_area(&mut self, area: Area) {
self.areas.push(area);
}
#[must_use]
pub fn areas(&self) -> &Vec<Area> {
&self.areas
}
pub fn areas_mut(&mut self) -> &mut Vec<Area> {
&mut self.areas
}
#[cfg(feature = "geojson")]
pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
let features: Vec<geojson::Feature> = self
.areas
.clone()
.into_iter()
.map(geojson::Feature::from)
.collect();
let feature_collection = geojson::FeatureCollection {
bbox: None,
features,
foreign_members: None,
};
serde_json::to_string(&feature_collection)
}
#[cfg(feature = "geojson")]
pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
let new_areas: Vec<Area> = feature_collection
.features
.into_iter()
.filter_map(|f| Area::try_from(f).ok())
.collect();
self.areas.extend(new_areas);
Ok(())
}
fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
if response.double_clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
if self.find_node_at(pointer_pos, projection).is_none()
&& let Some((area_idx, node_idx)) =
self.find_line_segment_at(pointer_pos, projection)
&& let Some(area) = self.areas.get_mut(area_idx)
&& let AreaShape::Polygon(points) = &mut area.shape
{
let p1_screen = projection.project(points[node_idx]);
let p2_screen = projection.project(points[(node_idx + 1) % points.len()]);
let t = projection_factor(pointer_pos, p1_screen, p2_screen);
let new_pos_screen = p1_screen.lerp(p2_screen, t);
let new_pos_geo = projection.unproject(new_pos_screen);
points.insert(node_idx + 1, new_pos_geo);
return response.hovered();
}
}
if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
self.dragged_object = self.find_object_at(pointer_pos, projection);
}
if response.dragged()
&& let Some(dragged_object) = self.dragged_object.clone()
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
match dragged_object {
DraggedObject::PolygonNode {
area_index,
node_index,
} => {
if self.is_move_valid(area_index, node_index, pointer_pos, projection)
&& let Some(area) = self.areas.get_mut(area_index)
{
let mut revert_info = None;
if let AreaShape::Polygon(points) = &mut area.shape
&& let Some(node) = points.get_mut(node_index)
{
let old_pos = *node;
*node = projection.unproject(pointer_pos);
revert_info = Some(old_pos);
}
if let Some(old_pos) = revert_info
&& !area.can_triangulate(projection)
{
warn!("Triangulation failed, cancelling drag");
self.dragged_object = None;
if let AreaShape::Polygon(points) = &mut area.shape {
points[node_index] = old_pos;
}
}
}
}
DraggedObject::CircleCenter { area_index } => {
if let Some(area) = self.areas.get_mut(area_index) {
let mut revert_center = None;
if let AreaShape::Circle { center, .. } = &mut area.shape {
revert_center = Some(*center);
*center = projection.unproject(pointer_pos);
}
if let Some(old_center) = revert_center
&& !area.can_triangulate(projection)
{
warn!("Triangulation failed, cancelling drag");
self.dragged_object = None;
if let AreaShape::Circle { center, .. } = &mut area.shape {
*center = old_center;
}
}
}
}
DraggedObject::CircleRadius { area_index } => {
if let Some(area) = self.areas.get_mut(area_index) {
let mut revert_radius = None;
if let AreaShape::Circle {
center,
radius,
points: _,
} = &mut area.shape
{
revert_radius = Some(*radius);
let center_screen = projection.project(*center);
let new_radius_pixels = pointer_pos.distance(center_screen);
let new_edge_screen =
center_screen + egui::vec2(new_radius_pixels, 0.0);
let new_edge_geo = projection.unproject(new_edge_screen);
let distance_lon = (new_edge_geo.lon - center.lon).abs()
* (111_320.0 * center.lat.to_radians().cos());
let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
*radius = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
}
if let Some(old_radius) = revert_radius
&& !area.can_triangulate(projection)
{
warn!("Triangulation failed, cancelling drag");
self.dragged_object = None;
if let AreaShape::Circle { radius, .. } = &mut area.shape {
*radius = old_radius;
}
}
}
}
}
}
if response.drag_stopped() {
self.dragged_object = None;
}
let is_dragging = self.dragged_object.is_some();
if is_dragging {
response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
} else if let Some(pointer_pos) = response.hover_pos()
&& self.find_object_at(pointer_pos, projection).is_some()
{
response.ctx.set_cursor_icon(egui::CursorIcon::Grab);
}
is_dragging
|| (response.hovered()
&& self
.find_object_at(response.hover_pos().unwrap_or_default(), projection)
.is_some())
}
fn find_object_at(
&self,
screen_pos: Pos2,
projection: &MapProjection,
) -> Option<DraggedObject> {
let click_tolerance_sq = (self.node_radius * 3.0).powi(2);
for (area_idx, area) in self.areas.iter().enumerate().rev() {
match &area.shape {
AreaShape::Polygon(points) => {
for (node_idx, node) in points.iter().enumerate() {
let node_screen_pos = projection.project(*node);
if node_screen_pos.distance_sq(screen_pos) < click_tolerance_sq {
return Some(DraggedObject::PolygonNode {
area_index: area_idx,
node_index: node_idx,
});
}
}
}
AreaShape::Circle {
center,
radius,
points: _,
} => {
let center_screen = projection.project(*center);
let point_on_circle_geo = GeoPos {
lon: center.lon + (radius / (111_320.0 * center.lat.to_radians().cos())),
lat: center.lat,
};
let point_on_circle_screen = projection.project(point_on_circle_geo);
let radius_pixels = center_screen.distance(point_on_circle_screen);
let distance_to_edge =
(center_screen.distance(screen_pos) - radius_pixels).abs();
if distance_to_edge < self.node_radius * 2.0 {
return Some(DraggedObject::CircleRadius {
area_index: area_idx,
});
}
if center_screen.distance_sq(screen_pos) < click_tolerance_sq {
return Some(DraggedObject::CircleCenter {
area_index: area_idx,
});
}
}
}
}
None
}
fn find_node_at(&self, screen_pos: Pos2, projection: &MapProjection) -> Option<(usize, usize)> {
match self.find_object_at(screen_pos, projection) {
Some(DraggedObject::PolygonNode {
area_index,
node_index,
}) => Some((area_index, node_index)),
_ => None,
}
}
fn find_line_segment_at(
&self,
screen_pos: Pos2,
projection: &MapProjection,
) -> Option<(usize, usize)> {
let click_tolerance = (self.node_radius * 2.0).powi(2);
for (area_idx, area) in self.areas.iter().enumerate().rev() {
if let AreaShape::Polygon(points) = &area.shape {
if points.len() < 2 {
continue;
}
for i in 0..points.len() {
let p1 = projection.project(points[i]);
let p2 = projection.project(points[(i + 1) % points.len()]);
if dist_sq_to_segment(screen_pos, p1, p2) < click_tolerance {
return Some((area_idx, i));
}
}
}
}
None
}
fn is_move_valid(
&self,
area_idx: usize,
node_idx: usize,
new_screen_pos: Pos2,
projection: &MapProjection,
) -> bool {
let area = if let Some(area) = self.areas.get(area_idx) {
area
} else {
return false; };
let points = match &area.shape {
AreaShape::Polygon(points) => points,
_ => return true, };
if points.len() < 3 {
return true;
}
let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
let n = screen_points.len();
let prev_node_idx = (node_idx + n - 1) % n;
let next_node_idx = (node_idx + 1) % n;
let new_edge1 = (screen_points[prev_node_idx], new_screen_pos);
let new_edge2 = (new_screen_pos, screen_points[next_node_idx]);
for i in 0..n {
let p1_idx = i;
let p2_idx = (i + 1) % n;
if p1_idx == node_idx || p2_idx == node_idx {
continue;
}
let edge_to_check = (screen_points[p1_idx], screen_points[p2_idx]);
if p1_idx != prev_node_idx
&& p2_idx != prev_node_idx
&& segments_intersect(new_edge1.0, new_edge1.1, edge_to_check.0, edge_to_check.1)
{
return false;
}
if p1_idx != next_node_idx
&& p2_idx != next_node_idx
&& segments_intersect(new_edge2.0, new_edge2.1, edge_to_check.0, edge_to_check.1)
{
return false;
}
}
true
}
}
impl Area {
fn can_triangulate(&self, projection: &MapProjection) -> bool {
let points = self.get_points(projection);
let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
if screen_points.len() < 3 {
return true;
}
let flat_points: Vec<f64> = screen_points
.iter()
.flat_map(|p| [f64::from(p.x), f64::from(p.y)])
.collect();
earcutr::earcut(&flat_points, &[], 2).is_ok()
}
fn get_points(&self, projection: &MapProjection) -> Vec<GeoPos> {
match &self.shape {
AreaShape::Polygon(points) => points.clone(),
AreaShape::Circle {
center,
radius,
points,
} => {
let center_geo = *center;
let point_on_circle_geo = GeoPos {
lon: center_geo.lon
+ (radius / (111_320.0 * center_geo.lat.to_radians().cos())),
lat: center_geo.lat,
};
let center_screen = projection.project(center_geo);
let point_on_circle_screen = projection.project(point_on_circle_geo);
let radius_pixels = center_screen.distance(point_on_circle_screen);
let num_points = if let Some(points) = points {
*points
} else {
(f64::from(radius_pixels) * 2.0 * std::f64::consts::PI / 10.0).ceil() as i64
};
let mut circle_points = Vec::with_capacity(num_points as usize);
for i in 0..num_points {
let angle = (i as f64 / num_points as f64) * 2.0 * std::f64::consts::PI;
let point_screen = center_screen
+ egui::vec2(
radius_pixels * angle.cos() as f32,
radius_pixels * angle.sin() as f32,
);
circle_points.push(projection.unproject(point_screen));
}
circle_points
}
}
}
}
fn generate_hatching_lines(screen_points: &[Pos2], spacing: f32, angle: f32) -> Vec<(Pos2, Pos2)> {
if screen_points.len() < 3 || spacing <= 0.0 {
return Vec::new();
}
let dir = egui::vec2(angle.cos(), angle.sin());
let perp = egui::vec2(-angle.sin(), angle.cos());
let mut min_perp = f32::MAX;
let mut max_perp = f32::MIN;
for p in screen_points {
let d = p.to_vec2().dot(perp);
min_perp = min_perp.min(d);
max_perp = max_perp.max(d);
}
let n = screen_points.len();
let mut segments = Vec::new();
let mut offset = min_perp + spacing;
while offset < max_perp {
let line_origin = Pos2::ZERO + perp * offset;
let mut t_values: Vec<f32> = Vec::new();
for i in 0..n {
let a = screen_points[i];
let b = screen_points[(i + 1) % n];
let edge = b - a;
let denom = edge.x * dir.y - edge.y * dir.x;
if denom.abs() < 1e-9 {
continue; }
let diff = a - line_origin;
let t_edge = -(diff.x * dir.y - diff.y * dir.x) / denom;
if (0.0..=1.0).contains(&t_edge) {
let t_line = if dir.x.abs() > dir.y.abs() {
(a.x - line_origin.x + t_edge * edge.x) / dir.x
} else {
(a.y - line_origin.y + t_edge * edge.y) / dir.y
};
t_values.push(t_line);
}
}
t_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for pair in t_values.chunks_exact(2) {
let p1 = line_origin + dir * pair[0];
let p2 = line_origin + dir * pair[1];
segments.push((p1, p2));
}
offset += spacing;
}
segments
}
impl Layer for AreaLayer {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
match self.mode {
AreaMode::Disabled => false,
AreaMode::Modify => self.handle_modify_input(response, projection),
}
}
fn draw(&self, painter: &Painter, projection: &MapProjection) {
for area in &self.areas {
let points = area.get_points(projection);
let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
if screen_points.len() >= 3 {
let path_shape = Shape::Path(egui::epaint::PathShape {
points: screen_points.clone(),
closed: true,
fill: Color32::TRANSPARENT,
stroke: area.stroke.into(),
});
painter.add(path_shape);
match area.fill_type {
FillType::None => { }
FillType::Solid => {
let flat_points: Vec<f64> = screen_points
.iter()
.flat_map(|p| [f64::from(p.x), f64::from(p.y)])
.collect();
match earcutr::earcut(&flat_points, &[], 2) {
Ok(indices) => {
let mesh = Mesh {
vertices: screen_points
.iter()
.map(|p| egui::epaint::Vertex {
pos: *p,
uv: Default::default(),
color: area.fill,
})
.collect(),
indices: indices.into_iter().map(|i| i as u32).collect(),
..Default::default()
};
painter.add(Shape::Mesh(mesh.into()));
}
Err(e) => {
warn!("Failed to triangulate area: {e:?}");
}
}
}
FillType::Hatching => {
let segments = generate_hatching_lines(
&screen_points,
8.0,
std::f32::consts::FRAC_PI_4,
);
for (a, b) in segments {
painter.line_segment([a, b], area.stroke);
}
}
}
} else {
warn!("Invalid amount of points in area. {area:?}");
}
if self.mode == AreaMode::Modify {
match &area.shape {
AreaShape::Polygon(_) => {
for point in &screen_points {
painter.circle_filled(*point, self.node_radius, self.node_fill);
}
}
AreaShape::Circle {
center,
radius,
points: _,
} => {
let center_screen = projection.project(*center);
let point_on_circle_geo = GeoPos {
lon: center.lon
+ (radius / (111_320.0 * center.lat.to_radians().cos())),
lat: center.lat,
};
let point_on_circle_screen = projection.project(point_on_circle_geo);
let radius_pixels = center_screen.distance(point_on_circle_screen);
painter.circle_filled(center_screen, self.node_radius, self.node_fill);
let radius_handle_pos = center_screen + egui::vec2(radius_pixels, 0.0);
painter.circle_filled(radius_handle_pos, self.node_radius, self.node_fill);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::projection::MapProjection;
use egui::{Rect, pos2, vec2};
fn dummy_projection() -> MapProjection {
MapProjection::new(
10, (0.0, 0.0).into(), Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 1000.0)),
)
}
#[test]
fn area_layer_new() {
let layer = AreaLayer::default();
assert_eq!(layer.mode, AreaMode::Disabled);
assert!(layer.areas.is_empty());
assert_eq!(layer.node_radius, 5.0);
}
#[test]
fn area_layer_add_area() {
let mut layer = AreaLayer::default();
assert_eq!(layer.areas.len(), 0);
layer.add_area(Area {
shape: AreaShape::Polygon(vec![
(0.0, 0.0).into(),
(1.0, 0.0).into(),
(0.0, 1.0).into(),
]),
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
});
assert_eq!(layer.areas.len(), 1);
}
#[test]
fn circle_get_points_with_fixed_number() {
let projection = dummy_projection();
let area = Area {
shape: AreaShape::Circle {
center: (0.0, 0.0).into(),
radius: 1000.0,
points: Some(16),
},
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
};
let points = area.get_points(&projection);
assert_eq!(points.len(), 16);
}
#[test]
fn find_object_at_empty() {
let layer = AreaLayer::default();
let projection = dummy_projection();
let position = pos2(100.0, 100.0);
assert!(layer.find_object_at(position, &projection).is_none());
}
#[test]
fn find_object_at_polygon_node() {
let projection = dummy_projection();
let mut layer = AreaLayer::default();
let geo_pos = projection.unproject(pos2(100.0, 100.0));
layer.add_area(Area {
shape: AreaShape::Polygon(vec![geo_pos]),
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
});
let found = layer.find_object_at(pos2(100.0, 100.0), &projection);
assert!(matches!(
found,
Some(DraggedObject::PolygonNode {
area_index: 0,
node_index: 0
})
));
let found_nearby = layer.find_object_at(pos2(101.0, 101.0), &projection);
assert!(matches!(
found_nearby,
Some(DraggedObject::PolygonNode {
area_index: 0,
node_index: 0
})
));
let not_found = layer.find_object_at(pos2(200.0, 200.0), &projection);
assert!(not_found.is_none());
}
#[test]
fn area_layer_serde() {
let mut layer = AreaLayer::default();
layer.add_area(Area {
shape: AreaShape::Polygon(vec![(0.0, 0.0).into()]),
stroke: Stroke::new(1.0, Color32::RED),
fill: Color32::BLUE,
fill_type: Default::default(),
});
let json = serde_json::to_string(&layer).unwrap();
let deserialized: AreaLayer = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.areas.len(), 1);
assert_eq!(deserialized.mode, AreaMode::Disabled); }
#[test]
fn test_can_triangulate_valid() {
let projection = dummy_projection();
let area = Area {
shape: AreaShape::Polygon(vec![
(0.0, 0.0).into(),
(10.0, 0.0).into(),
(0.0, 10.0).into(),
]),
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
};
assert!(area.can_triangulate(&projection));
}
#[test]
fn test_can_triangulate_insufficient_points() {
let projection = dummy_projection();
let area = Area {
shape: AreaShape::Polygon(vec![(0.0, 0.0).into(), (10.0, 0.0).into()]),
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
};
assert!(area.can_triangulate(&projection));
}
#[cfg(feature = "geojson")]
mod geojson_tests {
use super::*;
#[test]
fn area_layer_geojson_polygon() {
let mut layer = AreaLayer::default();
layer.add_area(Area {
shape: AreaShape::Polygon(vec![
(10.0, 20.0).into(),
(30.0, 40.0).into(),
(50.0, 60.0).into(),
]),
stroke: Stroke::new(2.0, Color32::from_rgb(0, 0, 255)),
fill: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
fill_type: Default::default(),
});
let geojson_str = layer.to_geojson_str().unwrap();
let mut new_layer = AreaLayer::default();
new_layer.from_geojson_str(&geojson_str).unwrap();
assert_eq!(new_layer.areas.len(), 1);
assert_eq!(layer.areas[0], new_layer.areas[0]);
}
#[test]
fn area_layer_geojson_circle() {
let mut layer = AreaLayer::default();
layer.add_area(Area {
shape: AreaShape::Circle {
center: (10.0, 20.0).into(),
radius: 1000.0,
points: Some(32),
},
stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
});
let geojson_str = layer.to_geojson_str().unwrap();
let mut new_layer = AreaLayer::default();
new_layer.from_geojson_str(&geojson_str).unwrap();
assert_eq!(new_layer.areas.len(), 1);
assert_eq!(layer.areas[0].shape, new_layer.areas[0].shape);
}
}
#[test]
fn find_node_at_on_segment() {
let projection = dummy_projection();
let mut layer = AreaLayer::default();
let p1 = projection.unproject(pos2(100.0, 100.0));
let p2 = projection.unproject(pos2(200.0, 100.0));
layer.add_area(Area {
shape: AreaShape::Polygon(vec![p1, p2, projection.unproject(pos2(150.0, 200.0))]), stroke: Default::default(),
fill: Default::default(),
fill_type: Default::default(),
});
let click_pos = pos2(150.0, 100.0);
assert!(layer.find_node_at(click_pos, &projection).is_none());
let segment = layer.find_line_segment_at(click_pos, &projection);
assert!(segment.is_some());
assert_eq!(segment.unwrap().0, 0); assert_eq!(segment.unwrap().1, 0);
}
}