From 765d451d35e2109ecee792d25c0ba9bc60fb5942 Mon Sep 17 00:00:00 2001 From: Calli Evers Date: Tue, 1 Oct 2024 22:03:39 +0200 Subject: [PATCH] feat: got the main algorithm mostly working --- Cargo.toml | 1 + benches/map_algo_benchmarks.rs | 33 ++- exisiting_maps/disjointed_test.json | 22 ++ src/algorithm/cost_calculation.rs | 334 ++++++++++++++++++++++++++++ src/algorithm/edge_dijkstra.rs | 143 +++++++++--- src/algorithm/mod.rs | 26 ++- src/algorithm/order_edges.rs | 108 +++++++-- src/algorithm/recalculate_map.rs | 29 ++- src/algorithm/route_edges.rs | 205 ++++++++++------- src/algorithm/utils.rs | 87 +++++++- src/components/molecules/canvas.rs | 4 + src/components/state/map.rs | 36 +++ src/lib.rs | 1 + src/models/edge.rs | 40 +++- src/models/grid_node.rs | 15 +- src/models/map.rs | 9 + src/models/station.rs | 28 ++- src/utils/error.rs | 10 + 18 files changed, 980 insertions(+), 151 deletions(-) create mode 100644 exisiting_maps/disjointed_test.json create mode 100644 src/algorithm/cost_calculation.rs diff --git a/Cargo.toml b/Cargo.toml index 33fc259..5592ed4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ serde = { version = "1", features = ["derive"] } csscolorparser = "0.7" rand = "0.8" priority-queue = "2.1" +ordered-float = "4.3" [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/benches/map_algo_benchmarks.rs b/benches/map_algo_benchmarks.rs index 3466381..8dc0118 100644 --- a/benches/map_algo_benchmarks.rs +++ b/benches/map_algo_benchmarks.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use criterion::{ black_box, criterion_group, @@ -7,6 +9,9 @@ use criterion::{ use metro_map_editor::{ algorithm::run_a_star, models::GridNode, + utils::json, + CanvasState, + MapState, }; pub fn a_star_benchmark(c: &mut Criterion) { @@ -18,5 +23,31 @@ pub fn a_star_benchmark(c: &mut Criterion) { }); } +pub fn full_recalculation_benchmark(c: &mut Criterion) { + let mut canvas = CanvasState::new(); + canvas.set_square_size(5); + + let test_file_content = std::fs::read_to_string("exisiting_maps/routing_test.json") + .expect("test data file does not exist"); + let map = json::decode_map(&test_file_content, canvas).expect("failed to decode graphml"); + + let state = MapState::new(map); + + c.bench_function("full_recalculation", |b| { + b.iter(|| { + let mut alg_state = state.clone(); + MapState::run_algorithm(black_box(&mut alg_state)) + }) + }); +} + +criterion_group!( + name = full_recalculation_benches; + config = Criterion::default().measurement_time(Duration::from_secs(20)).sample_size(20); + targets = full_recalculation_benchmark +); criterion_group!(a_star_benches, a_star_benchmark); -criterion_main!(a_star_benches); +criterion_main!( + a_star_benches, + full_recalculation_benches +); diff --git a/exisiting_maps/disjointed_test.json b/exisiting_maps/disjointed_test.json new file mode 100644 index 0000000..ed97aca --- /dev/null +++ b/exisiting_maps/disjointed_test.json @@ -0,0 +1,22 @@ +{ + "stations": [ + { "id": "s4", "name": null, "x": 140.0, "y": 84.0 }, + { "id": "s5", "name": null, "x": 175.0, "y": 84.0 }, + { "id": "s7", "name": null, "x": 49.0, "y": 35.0 }, + { "id": "s6", "name": null, "x": 210.0, "y": 140.0 }, + { "id": "s1", "name": null, "x": 70.0, "y": 70.0 }, + { "id": "s2", "name": null, "x": 105.0, "y": 105.0 }, + { "id": "s3", "name": null, "x": 140.0, "y": 175.0 } + ], + "lines": [ + { "id": "l1", "name": "Line 1", "color": "#FF0000" }, + { "id": "l2", "name": "Line 2", "color": "#00FF00" }, + { "id": "l3", "name": "Line 3", "color": "#0000FF" } + ], + "edges": [ + { "source": "s5", "target": "s4", "lines": ["l2"] }, + { "source": "s6", "target": "s5", "lines": ["l2"] }, + { "source": "s2", "target": "s1", "lines": ["l1"] }, + { "source": "s3", "target": "s2", "lines": ["l1"] } + ] +} diff --git a/src/algorithm/cost_calculation.rs b/src/algorithm/cost_calculation.rs new file mode 100644 index 0000000..c05ed4f --- /dev/null +++ b/src/algorithm/cost_calculation.rs @@ -0,0 +1,334 @@ +use std::collections::HashSet; + +use leptos::logging; + +use super::{ + calculate_angle, + node_outside_grid, + overlap_amount, + AlgorithmSettings, +}; +use crate::{ + models::{ + Edge, + GridNode, + Map, + Station, + }, + utils::Result, + Error, +}; + +/// Check if the station can be approached from the given node. +/// Considers if the approach leaves enough open nodes on all sides for future +/// connections on those sides. +fn station_approach_available(map: &Map, station: &Station, node: GridNode) -> Result { + let neighbor_nodes = node.get_neighbors(); + let mut left_wards = Vec::new(); + let mut right_wards = Vec::new(); + + for edge_id in station.get_edges() { + let edge = map + .get_edge(*edge_id) + .ok_or(Error::other( + "edge connected to station not found", + ))?; + + // FIXME: get the original ordering of the edges + + for edge_node in edge.get_nodes() { + if let Some(opp_node) = neighbor_nodes + .iter() + .find(|n| *n == edge_node) + { + left_wards.push(( + edge.clone(), + calculate_angle( + node, + station.get_pos(), + *opp_node, + false, + ), + )); + + right_wards.push(( + edge.clone(), + calculate_angle( + *opp_node, + station.get_pos(), + node, + false, + ), + )); + } + } + } + + left_wards.sort_by(|a, b| { + a.1.partial_cmp(&b.1) + .unwrap() + }); + right_wards.sort_by(|a, b| { + a.1.partial_cmp(&b.1) + .unwrap() + }); + + let mut cost = 0; + + let possible_angle = move |angle| { + match angle { + 315.0 => cost < 7, + 270.0 => cost < 6, + 225.0 => cost < 5, + 180.0 => cost < 4, + 135.0 => cost < 3, + 90.0 => cost < 2, + 45.0 => cost < 1, + 0.0 => false, + _ => panic!("found impossible angle of {angle}"), + } + }; + + for (edge, angle) in left_wards { + if edge.is_settled() { + if !possible_angle(angle) { + return Ok(false); + } + break; + } else { + cost += 1; + } + } + + cost = 0; + for (edge, angle) in right_wards { + if edge.is_settled() { + if !possible_angle(angle) { + return Ok(false); + } + break; + } else { + cost += 1; + } + } + + Ok(true) +} + +/// Calculate the cost of the angle between three nodes. +/// The second point is assumed to be the middle node where the angle is +/// located. +fn calc_angle_cost(first: GridNode, second: GridNode, third: GridNode) -> Result { + let angle = calculate_angle(first, second, third, true); + + Ok(match angle { + 180.0 => 0.0, + 135.0 => 1.0, + 90.0 => 1.5, + 45.0 => 2.0, + 0.0 => f64::INFINITY, + _ => { + Err(Error::other(format!( + "found invalid angle of {angle} between {first}, {second}, {third}", + )))? + }, + }) +} + +/// Calculate the cost of the node attached to the given station on the path +/// going away from the station. +fn calc_station_exit_cost( + map: &Map, + current_edge: &Edge, + station: &Station, + node: GridNode, +) -> Result { + if !station.is_settled() { + return Ok(0.0); + } + + let mut biggest_settled = None; + let mut current = 0; + + for edge_id in station.get_edges() { + let edge = map + .get_edge(*edge_id) + .ok_or(Error::other( + "edge connected to station not found", + ))?; + if !edge.is_settled() { + continue; + } + + let overlap = overlap_amount( + edge.get_lines(), + current_edge.get_lines(), + ); + if overlap > current { + biggest_settled = Some(edge); + current = overlap; + } + } + + if let Some(opposite_edge) = biggest_settled { + let neighbor_nodes = station + .get_pos() + .get_neighbors(); + for edge_node in opposite_edge.get_nodes() { + if let Some(opp_node) = neighbor_nodes + .iter() + .find(|n| *n == edge_node) + { + return calc_angle_cost(*opp_node, station.get_pos(), node); + } + } + Err(Error::other("no neighbor node found")) + } else { + Ok(0.0) + } +} + +/// Calculate the cost of the node on the path between two stations. +/// The cost is dependent on the angle between the previous two nodes and if the +/// node is exiting or approaching a station. It also validates if the node can +/// be used for a path, and else giving a cost of infinity. +/// This is the Calculate Node Cost function from the paper. +pub fn calc_node_cost( + settings: AlgorithmSettings, + map: &Map, + edge: &Edge, + node: GridNode, + previous: &[GridNode], + from_station: &Station, + to_station: &Station, + occupied: &HashSet, +) -> Result { + if node_outside_grid(settings, node) { + return Ok(f64::INFINITY); + } + + if to_station.is_settled() && node == to_station.get_pos() { + if !station_approach_available(map, to_station, node)? { + logging::debug_warn!( + "station approach to {} not available", + to_station.get_id() + ); + return Ok(f64::INFINITY); + } + } else { + if occupied.contains(&node) { + return Ok(f64::INFINITY); + } + } + + if previous.len() < 2 { + return calc_station_exit_cost(map, edge, from_station, node) // cost of exiting station + .map(|c| c + settings.move_cost) // standard cost of a move + .map(|c| c + node.diagonal_distance_to(to_station.get_pos())); // cost of distance to target + } + + calc_angle_cost(previous[0], previous[1], node) // cost of angle between previous nodes + .map(|c| c + settings.move_cost) // standard cost of a move + .map(|c| c + node.diagonal_distance_to(to_station.get_pos())) // cost of + // distance + // to target +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_station_approach_available() { + let mut map = Map::new(); + + let station1 = Station::new(GridNode::from((0, 5)), None); + let station2 = Station::new(GridNode::from((5, 0)), None); + let station3 = Station::new(GridNode::from((5, 10)), None); + let mut station4 = Station::new(GridNode::from((5, 5)), None); + + map.add_station(station1.clone()); + map.add_station(station2.clone()); + map.add_station(station3.clone()); + map.add_station(station4.clone()); + + let mut edge1 = Edge::new( + station1.get_id(), + station4.get_id(), + None, + ); + let edge2 = Edge::new( + station2.get_id(), + station4.get_id(), + None, + ); + let mut edge3 = Edge::new( + station3.get_id(), + station4.get_id(), + None, + ); + edge3.settle(); + edge3.set_nodes(vec![ + GridNode::from((5, 6)), + GridNode::from((5, 7)), + GridNode::from((5, 8)), + GridNode::from((5, 9)), + ]); + + map.add_edge(edge1.clone()); + map.add_edge(edge2); + map.add_edge(edge3); + + let node = GridNode::from((6, 6)); + + station4 = map + .get_station(station4.get_id()) + .cloned() + .unwrap(); + + let neg_result = station_approach_available(&map, &station4, node).unwrap(); + assert!(!neg_result); + + edge1.settle(); + map.add_edge(edge1); + let pos_result = station_approach_available(&map, &station4, node).unwrap(); + assert!(pos_result); + } + + #[test] + fn test_calc_angle_cost() { + let first_180 = GridNode::from((0, 2)); + let second_180 = GridNode::from((1, 1)); + let third_180 = GridNode::from((2, 0)); + let result_180 = calc_angle_cost(first_180, second_180, third_180); + assert_eq!(result_180, Ok(0.0)); + + let first_135 = GridNode::from((2, 2)); + let second_135 = GridNode::from((1, 1)); + let third_135 = GridNode::from((1, 0)); + let result_135 = calc_angle_cost(first_135, second_135, third_135); + assert_eq!(result_135, Ok(1.0)); + + let first_90 = GridNode::from((0, 0)); + let second_90 = GridNode::from((1, 1)); + let third_90 = GridNode::from((2, 0)); + let result_90 = calc_angle_cost(first_90, second_90, third_90); + assert_eq!(result_90, Ok(1.5)); + + let first_45 = GridNode::from((1, 0)); + let second_45 = GridNode::from((1, 1)); + let third_45 = GridNode::from((2, 0)); + let result_45 = calc_angle_cost(first_45, second_45, third_45); + assert_eq!(result_45, Ok(2.0)); + } + + #[test] + fn test_calc_station_exit_cost() { + // FIXME: implement test + } + + #[test] + fn test_calc_node_cost() { + // FIXME: implement test + } +} diff --git a/src/algorithm/edge_dijkstra.rs b/src/algorithm/edge_dijkstra.rs index 1a454ca..8d8fab2 100644 --- a/src/algorithm/edge_dijkstra.rs +++ b/src/algorithm/edge_dijkstra.rs @@ -1,19 +1,28 @@ use std::{ cmp::Reverse, + collections::HashSet, hash::{ Hash, Hasher, }, }; +use ordered_float::NotNan; use priority_queue::PriorityQueue; +use super::{ + cost_calculation::calc_node_cost, + AlgorithmSettings, +}; use crate::{ models::{ + Edge, GridNode, Map, + Station, }, utils::Result, + Error, }; /// Holds the state for an item in the Dijkstra algorithm queue. @@ -44,7 +53,7 @@ impl QueueItem { start: parent.start, }; new.path - .push(node); + .push(parent.node); new } } @@ -69,30 +78,56 @@ impl Hash for QueueItem { /// A Dijkstra implementation that finds the shortest path between two start and /// end node sets. This is the Edge Dijkstra algorithm in the paper. pub fn edge_dijkstra( + settings: AlgorithmSettings, map: &Map, - from: Vec, - to: Vec, + edge: &Edge, + from: &[(GridNode, f64)], + from_station: &Station, + to: &[(GridNode, f64)], + to_station: &Station, + occupied: &HashSet, ) -> Result<(GridNode, Vec, GridNode)> { let mut queue = PriorityQueue::new(); - let mut visited = Vec::new(); - let mut to_visited = Vec::new(); - - for node in from { + let mut visited = HashSet::new(); + let mut to_visited = None; + let to_nodes = to + .iter() + .map(|(node, _)| *node) + .collect::>(); + + for (node, cost) in from { // FIXME: the cost is dependent upon the distance from the original station // location. - queue.push(QueueItem::new(node), Reverse(0)); + queue.push( + QueueItem::new(*node), + Reverse(NotNan::new(*cost)?), + ); } while let Some((current, current_cost)) = queue.pop() { - visited.push(current.node); + visited.insert(current.node); - if to.contains(¤t.node) { - to_visited.push((current.clone(), current_cost)); + if current_cost + .0 + .is_infinite() + { + break; } - if to_visited.len() == to.len() { + + if to_nodes.contains(¤t.node) { + // FIXME: this returns on first found from to set, not sure if we shouldn't + // search for more or when it is enough then + to_visited = Some((current.clone(), current_cost)); break; } + let previous = ¤t + .path + .last() + .map_or(vec![current.node], |p| { + vec![*p, current.node] + }); + for neighbor in current .node .get_neighbors() @@ -101,32 +136,47 @@ pub fn edge_dijkstra( continue; } - let cost = todo!("calculate the cost"); + let cost = NotNan::new(calc_node_cost( + settings, + map, + edge, + neighbor, + previous, + from_station, + to_station, + occupied, + )?)? + current_cost.0; let neighbor_item = QueueItem::from_parent(¤t, neighbor); if let Some((_, old_cost)) = queue.get(&neighbor_item) { if old_cost.0 > cost { queue.push_increase(neighbor_item, Reverse(cost)); } - todo!("update path if needed"); } else { queue.push(neighbor_item, Reverse(cost)); - todo!("set path if needed"); } } } - // Get the node from the to node set with the cheapest path and return it. - to_visited.sort_unstable_by_key(|(_, cost)| cost.0); - let mut best = to_visited[0] - .0 - .clone(); - best.path - .truncate( - best.path - .len() - - 1, - ); + if to_visited.is_none() { + return Err(Error::other("no path found")); + } + + let mut best = to_visited + .unwrap() + .0; + + if best + .path + .len() + > 1 + { + best.path + .drain(..1); + } else { + best.path + .clear(); + } Ok((best.start, best.path, best.node)) } @@ -137,11 +187,34 @@ mod tests { #[test] fn test_edge_dijkstra() { - let map = Map::new(); - let from = vec![GridNode::from((0, 0))]; - let to = vec![GridNode::from((3, 3))]; - - let result = edge_dijkstra(&map, from, to); + let mut map = Map::new(); + let occupied = HashSet::new(); + let from_station = Station::new(GridNode::from((0, 0)), None); + let from_nodes = vec![(from_station.get_pos(), 0.0)]; + let to_station = Station::new(GridNode::from((8, 4)), None); + let to_nodes = vec![ + (GridNode::from((7, 4)), 1.0), + (to_station.get_pos(), 0.0), + (GridNode::from((9, 4)), 1.0), + ]; + let edge = Edge::new( + from_station.get_id(), + to_station.get_id(), + None, + ); + map.add_station(from_station.clone()); + map.add_station(to_station.clone()); + + let result = edge_dijkstra( + AlgorithmSettings::default(), + &map, + &edge, + &from_nodes, + &from_station, + &to_nodes, + &to_station, + &occupied, + ); assert_eq!( result, @@ -149,9 +222,13 @@ mod tests { GridNode::from((0, 0)), vec![ GridNode::from((1, 1)), - GridNode::from((2, 2)) + GridNode::from((2, 2)), + GridNode::from((3, 3)), + GridNode::from((4, 4)), + GridNode::from((5, 4)), + GridNode::from((6, 4)) ], - GridNode::from((3, 3)) + GridNode::from((7, 4)) )) ); } diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs index 782f9c9..94384af 100644 --- a/src/algorithm/mod.rs +++ b/src/algorithm/mod.rs @@ -3,6 +3,7 @@ mod a_star; mod calc_direction; +mod cost_calculation; pub mod drawing; mod edge_dijkstra; mod order_edges; @@ -24,13 +25,36 @@ pub struct AlgorithmSettings { /// Max amount of attempts allowed of routing edges before erroring out. /// Default: 5 pub edge_routing_attempts: usize, + /// The cost of moving from one node to another. + pub move_cost: f64, + /// The highest and lowest possible x values of the grid. + pub grid_x_limits: (i32, i32), + /// The highest and lowest possible y values of the grid. + pub grid_y_limits: (i32, i32), +} + +impl AlgorithmSettings { + /// Set the highest and lowest possible x values of the grid. + pub fn set_grid_x_limits(mut self, x_limits: (i32, i32)) -> Self { + self.grid_x_limits = x_limits; + self + } + + /// Set the highest and lowest possible y values of the grid. + pub fn set_grid_y_limits(mut self, y_limits: (i32, i32)) -> Self { + self.grid_y_limits = y_limits; + self + } } impl Default for AlgorithmSettings { fn default() -> Self { Self { node_set_radius: 3, - edge_routing_attempts: 5, + edge_routing_attempts: 3, + move_cost: 1.0, + grid_x_limits: (0, 0), + grid_y_limits: (0, 0), } } } diff --git a/src/algorithm/order_edges.rs b/src/algorithm/order_edges.rs index 3a562ba..183275a 100644 --- a/src/algorithm/order_edges.rs +++ b/src/algorithm/order_edges.rs @@ -21,9 +21,10 @@ fn line_degree(map: &Map, station_id: StationID) -> Result { let station = map .get_station(station_id) - .ok_or(Error::other( - "station not found when calculating line degree", - ))?; + .ok_or(Error::other(format!( + "station {} not found when calculating line degree", + station_id + )))?; for edge_id in station.get_edges() { let edge = map @@ -106,11 +107,54 @@ fn calc_all_degrees( /// Order the edges in the map by the line degree of the stations they are /// connected to. This is the Order Edges algorithm in the paper. pub fn order_edges(map: &Map) -> Result> { + let (mut line_degree_map, mut highest) = calc_all_degrees(map)?; + + let mut edges = Vec::new(); + while map + .get_edges() + .len() + > edges.len() + { + edges.append(&mut order_edges_alg( + map, + line_degree_map.clone(), + highest, + )?); + + // If there are still edges left, then there are disjoint parts of the map + if map + .get_edges() + .len() + > edges.len() + { + // Remove the stations that are connected to edges that were already dealt with + // and calculate the new highest among the remaining stations + for edge in &edges { + line_degree_map.remove(&edge.get_from()); + line_degree_map.remove(&edge.get_to()); + } + highest = HeapStation::new(0.into(), usize::MIN); + for (_, station) in &line_degree_map { + if station.degree > highest.degree { + highest = *station; + } + } + } + } + + Ok(edges) +} + +/// The underlying algorithm for ordering the edges. +fn order_edges_alg( + map: &Map, + line_degree_map: HashMap, + start: HeapStation, +) -> Result> { let mut edges = Vec::new(); let mut queue = BinaryHeap::new(); - let (line_degree_map, highest) = calc_all_degrees(map)?; - queue.push(highest); + queue.push(start); while let Some(station_with_degree) = queue.pop() { let station = map @@ -141,14 +185,7 @@ pub fn order_edges(map: &Map) -> Result> { "station not found in line degree map", ))?; - station_edges.push(( - edge.clone(), - line_degree_map - .get(&opposite_id) - .ok_or(Error::other( - "station not found in line degree map", - ))?, - )); + station_edges.push((edge.clone(), neighbor)); queue.push(*neighbor); } } @@ -235,5 +272,50 @@ mod tests { edge9_id, edge10_id, edge11_id ] ); + + let disjoint_file_content = std::fs::read_to_string("exisiting_maps/disjointed_test.json") + .expect("test data file does not exist"); + let disjoint_map = + json::decode_map(&disjoint_file_content, canvas).expect("failed to decode graphml"); + + let edge1_id = disjoint_map + .get_edge_id_between_if_exists(1.into(), 2.into()) + .unwrap(); + let edge2_id = disjoint_map + .get_edge_id_between_if_exists(2.into(), 3.into()) + .unwrap(); + let edge3_id = disjoint_map + .get_edge_id_between_if_exists(4.into(), 5.into()) + .unwrap(); + let edge4_id = disjoint_map + .get_edge_id_between_if_exists(5.into(), 6.into()) + .unwrap(); + + let disjoint_sorted = order_edges(&disjoint_map).unwrap(); + let disjoint_sorted_ids: Vec = disjoint_sorted + .iter() + .map(|edge| edge.get_id()) + .collect(); + + assert_eq!( + disjoint_sorted_ids, + vec![edge3_id, edge4_id, edge1_id, edge2_id] + ); + } + + #[test] + fn test_calc_all_degrees() { + let mut canvas = CanvasState::new(); + canvas.set_square_size(5); + + let test_file_content = std::fs::read_to_string("exisiting_maps/routing_test.json") + .expect("test data file does not exist"); + let map = json::decode_map(&test_file_content, canvas).expect("failed to decode graphml"); + + let (line_degree_map, highest) = calc_all_degrees(&map).unwrap(); + + assert_eq!(line_degree_map.len(), 10); + assert_eq!(highest.station, 5.into()); + assert_eq!(highest.degree, 6); } } diff --git a/src/algorithm/recalculate_map.rs b/src/algorithm/recalculate_map.rs index 07bc567..f27c37d 100644 --- a/src/algorithm/recalculate_map.rs +++ b/src/algorithm/recalculate_map.rs @@ -7,12 +7,13 @@ use super::{ order_edges::order_edges, randomize_edges, route_edges::route_edges, - unsettle_stations, + unsettle_map, AlgorithmSettings, }; use crate::{ models::Map, utils::Result, + Error, }; /// Recalculate the map, all the positions of the stations and the edges between @@ -26,30 +27,46 @@ pub fn recalculate_map(settings: AlgorithmSettings, map: &mut Map) -> Result<()> return Ok(()); } - unsettle_stations(map); + logging::log!( + "Recalculating map with {} edges", + map.get_edges() + .len() + ); + + map.quickcalc_edges(); + unsettle_map(map); let mut edges = order_edges(map)?; let mut attempt = 0; let mut found = false; - while !found && attempt < settings.edge_routing_attempts { + // logging::log!("Ordered {} edges", edges.len()); + + while !found { + if attempt >= settings.edge_routing_attempts { + return Err(Error::other( + "Reached max amount of retries when routing edges.", + )); + } + let mut alg_map = map.clone(); attempt += 1; let res = route_edges(settings, &mut alg_map, edges.clone()); - if res.is_err() { + if let Err(e) = res { + logging::warn!("Failed to route edges: {e}"); randomize_edges(&mut edges); } else { found = true; - *map = alg_map; - map.update_edges(res.unwrap())?; } } // TODO: Implement the rest of the algorithm + logging::log!("Recalculated map"); + Ok(()) } diff --git a/src/algorithm/route_edges.rs b/src/algorithm/route_edges.rs index b9cf8ab..017ed32 100644 --- a/src/algorithm/route_edges.rs +++ b/src/algorithm/route_edges.rs @@ -1,8 +1,11 @@ //! This module contains the Route Edges algorithm and all it needs. +use std::collections::HashSet; + +use leptos::logging; + use super::{ edge_dijkstra::edge_dijkstra, - have_overlap, AlgorithmSettings, }; use crate::{ @@ -17,13 +20,9 @@ use crate::{ }; /// Get a set of nodes in the radius around the given station. -fn get_node_set( - settings: AlgorithmSettings, - _map: &Map, // FIXME: unused parameter - station: &Station, -) -> Result> { +fn get_node_set(settings: AlgorithmSettings, station: &Station) -> Vec<(GridNode, f64)> { if station.is_settled() { - return Ok(vec![station.get_pos()]); + return vec![(station.get_pos(), 0.0)]; } let radius = settings.node_set_radius; @@ -34,41 +33,55 @@ fn get_node_set( for x in (station_pos.0 - radius)..=(station_pos.0 + radius) { for y in (station_pos.1 - radius)..=(station_pos.1 + radius) { let node = GridNode::from((x, y)); - if node - .diagonal_distance_to(station_pos) - .ceil() as i32 - <= radius - { - nodes.push(node); + let distance = node.diagonal_distance_to(station_pos); // FIXME: use better distance calculation + if distance.ceil() as i32 <= radius { + nodes.push((node, distance * settings.move_cost)); } } } - // TODO: include distance cost on the nodes - Ok(nodes) + nodes +} + +/// Check if two slices of grid nodes have any overlap. +fn have_overlap(left: &[(GridNode, f64)], right: &[(GridNode, f64)]) -> bool { + for (node, _) in left { + if right + .iter() + .any(|(n, _)| n == node) + { + return true; + } + } + false } /// Split the overlap between the two node sets based on the distance to their /// source. +#[allow(clippy::type_complexity)] // the return type is complex but makes sense here fn split_overlap( - mut from_set: Vec, + mut from_set: Vec<(GridNode, f64)>, from: GridNode, - mut to_set: Vec, + mut to_set: Vec<(GridNode, f64)>, to: GridNode, -) -> (Vec, Vec) { - for node in from_set - .clone() - .iter() - { +) -> ( + Vec<(GridNode, f64)>, + Vec<(GridNode, f64)>, +) { + for (node, _) in &from_set.clone() { if node == &from { continue; } - if *node == to || to_set.contains(node) { + if *node == to + || to_set + .iter() + .any(|(n, _)| n == node) + { if node.diagonal_distance_to(from) > node.diagonal_distance_to(to) { - from_set.retain(|n| n != node); + from_set.retain(|(n, _)| n != node); } else { - to_set.retain(|n| n != node); + to_set.retain(|(n, _)| n != node); } } } @@ -83,7 +96,9 @@ pub fn route_edges( map: &mut Map, mut edges: Vec, ) -> Result> { - for edge in edges.iter_mut() { + let mut occupied = HashSet::new(); + + for edge in &mut edges { let from_station = map .get_station(edge.get_from()) .ok_or(Error::other( @@ -95,8 +110,8 @@ pub fn route_edges( "to station on edge not found", ))?; - let mut from_nodes = get_node_set(settings, map, from_station)?; - let mut to_nodes = get_node_set(settings, map, to_station)?; + let mut from_nodes = get_node_set(settings, from_station); + let mut to_nodes = get_node_set(settings, to_station); if have_overlap(&from_nodes, &to_nodes) { (from_nodes, to_nodes) = split_overlap( @@ -107,10 +122,35 @@ pub fn route_edges( ); } - let (start, nodes, end) = edge_dijkstra(map, from_nodes, to_nodes)?; + logging::log!( + "routing edge from {}{} to {}{}, sets:\nfrom: {:?}\nto: {:?}", + from_station.get_id(), + from_station.get_pos(), + to_station.get_id(), + to_station.get_pos(), + from_nodes, + to_nodes + ); + let (start, nodes, end) = edge_dijkstra( + settings, + map, + edge, + &from_nodes, + from_station, + &to_nodes, + to_station, + &occupied, + )?; + + logging::log!("routed edge from {start} to {end}",); + + occupied.extend(&nodes); edge.set_nodes(nodes); + edge.settle(); + occupied.insert(start); + occupied.insert(end); map.get_mut_station(edge.get_from()) .ok_or(Error::other( "edge from-station not found", @@ -121,6 +161,7 @@ pub fn route_edges( "edge to-station not found", ))? .settle(end); + map.add_edge(edge.clone()); } Ok(edges) } @@ -136,57 +177,65 @@ mod tests { let station = Station::new((0, 0).into(), None); map.add_station(station.clone()); - let result = get_node_set( - AlgorithmSettings::default(), - &map, - &station, - ); + let result = get_node_set(AlgorithmSettings::default(), &station); assert_eq!( result, - Ok(vec![ - GridNode::from((-3, 0)), - GridNode::from((-2, 0)), - GridNode::from((-1, -1)), - GridNode::from((-1, 0)), - GridNode::from((-1, 1)), - GridNode::from((0, -3)), - GridNode::from((0, -2)), - GridNode::from((0, -1)), - GridNode::from((0, 0)), - GridNode::from((0, 1)), - GridNode::from((0, 2)), - GridNode::from((0, 3)), - GridNode::from((1, -1)), - GridNode::from((1, 0)), - GridNode::from((1, 1)), - GridNode::from((2, 0)), - GridNode::from((3, 0)), - ]) + vec![ + (GridNode::from((-3, 0)), 3.0), + (GridNode::from((-2, 0)), 2.0), + ( + GridNode::from((-1, -1)), + GridNode::from((1, -1)).diagonal_distance_to(GridNode::from((0, 0))) + ), + (GridNode::from((-1, 0)), 1.0), + ( + GridNode::from((-1, 1)), + GridNode::from((1, -1)).diagonal_distance_to(GridNode::from((0, 0))) + ), + (GridNode::from((0, -3)), 3.0), + (GridNode::from((0, -2)), 2.0), + (GridNode::from((0, -1)), 1.0), + (GridNode::from((0, 0)), 0.0), + (GridNode::from((0, 1)), 1.0), + (GridNode::from((0, 2)), 2.0), + (GridNode::from((0, 3)), 3.0), + ( + GridNode::from((1, -1)), + GridNode::from((1, -1)).diagonal_distance_to(GridNode::from((0, 0))) + ), + (GridNode::from((1, 0)), 1.0), + ( + GridNode::from((1, 1)), + GridNode::from((1, -1)).diagonal_distance_to(GridNode::from((0, 0))) + ), + (GridNode::from((2, 0)), 2.0), + (GridNode::from((3, 0)), 3.0), + ] ); } #[test] fn test_split_overlap() { let from_set = vec![ - GridNode::from((0, 0)), - GridNode::from((1, 1)), - GridNode::from((1, 2)), - GridNode::from((2, 2)), - GridNode::from((3, 3)), - GridNode::from((3, 4)), - GridNode::from((4, 4)), - GridNode::from((4, 5)), + (GridNode::from((0, 0)), 0.0), + (GridNode::from((1, 1)), 0.0), + (GridNode::from((1, 2)), 0.0), + (GridNode::from((2, 2)), 0.0), + (GridNode::from((3, 3)), 0.0), + (GridNode::from((3, 4)), 0.0), + (GridNode::from((4, 4)), 0.0), + (GridNode::from((4, 5)), 0.0), ]; let to_set = vec![ - GridNode::from((1, 1)), - GridNode::from((1, 2)), - GridNode::from((2, 2)), - GridNode::from((3, 3)), - GridNode::from((3, 4)), - GridNode::from((4, 4)), - GridNode::from((4, 5)), - GridNode::from((5, 5)), + (GridNode::from((1, 1)), 0.0), + (GridNode::from((1, 2)), 0.0), + (GridNode::from((2, 2)), 0.0), + (GridNode::from((3, 3)), 0.0), + (GridNode::from((3, 4)), 0.0), + (GridNode::from((4, 4)), 0.0), + (GridNode::from((4, 5)), 0.0), + (GridNode::from((5, 5)), 0.0), ]; let (from_set, to_set) = split_overlap( @@ -199,20 +248,20 @@ mod tests { assert_eq!( from_set, vec![ - GridNode::from((0, 0)), - GridNode::from((1, 1)), - GridNode::from((1, 2)), - GridNode::from((2, 2)), + (GridNode::from((0, 0)), 0.0), + (GridNode::from((1, 1)), 0.0), + (GridNode::from((1, 2)), 0.0), + (GridNode::from((2, 2)), 0.0), ] ); assert_eq!( to_set, vec![ - GridNode::from((3, 3)), - GridNode::from((3, 4)), - GridNode::from((4, 4)), - GridNode::from((4, 5)), - GridNode::from((5, 5)), + (GridNode::from((3, 3)), 0.0), + (GridNode::from((3, 4)), 0.0), + (GridNode::from((4, 4)), 0.0), + (GridNode::from((4, 5)), 0.0), + (GridNode::from((5, 5)), 0.0), ] ); } diff --git a/src/algorithm/utils.rs b/src/algorithm/utils.rs index 2386bbb..0ed9d6f 100644 --- a/src/algorithm/utils.rs +++ b/src/algorithm/utils.rs @@ -5,18 +5,22 @@ use rand::{ thread_rng, }; +use super::AlgorithmSettings; use crate::models::{ Edge, GridNode, Map, }; -/// Marks all stations on the map as unsettled, freeing their location for -/// moving in the algorithm. -pub fn unsettle_stations(map: &mut Map) { +/// Marks all stations and edges on the map as unsettled, freeing their location +/// for moving in the algorithm. +pub fn unsettle_map(map: &mut Map) { for station in map.get_mut_stations() { station.unsettle(); } + for edge in map.get_mut_edges() { + edge.unsettle(); + } } /// Randomizes the order of the edges in the given vector. @@ -25,12 +29,75 @@ pub fn randomize_edges(edges: &mut Vec) { edges.shuffle(&mut rng); } -/// Check if two slices of grid nodes have any overlap. -pub fn have_overlap(left: &[GridNode], right: &[GridNode]) -> bool { - for node in left { - if right.contains(node) { - return true; - } +/// Returns true if the given node is outside the grid limits. +pub fn node_outside_grid(settings: AlgorithmSettings, node: GridNode) -> bool { + node.0 + < settings + .grid_x_limits + .0 + || node.0 + > settings + .grid_x_limits + .1 + || node.1 + < settings + .grid_y_limits + .0 + || node.1 + > settings + .grid_y_limits + .1 +} + +/// Returns the amount of overlap between two slices. +pub fn overlap_amount(left: &[T], right: &[T]) -> usize { + left.iter() + .filter(|&l| right.contains(l)) + .count() +} + +/// Calculates the angle formed by three grid nodes and returns it in rounded +/// degrees. The second point is assumed to be the middle node where the angle +/// is located. +pub fn calculate_angle( + first: GridNode, + second: GridNode, + third: GridNode, + round_to_180: bool, +) -> f64 { + let l = (f64::from(first.1 - second.1)).atan2(f64::from(first.0 - second.0)); + let r = (f64::from(third.1 - second.1)).atan2(f64::from(third.0 - second.0)); + let angle = (l - r) + .abs() + .to_degrees() + .round(); + + if round_to_180 && angle > 180.0 { + angle - 180.0 + } else { + angle + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_overlap_amount() { + let left = vec![1, 2, 3, 0, 4, 5, 2]; + let right = vec![8, 3, 4, 10, 5, 6, 7]; + assert_eq!(overlap_amount(&left, &right), 3); + } + + #[test] + fn test_calculate_angle() { + let first = GridNode::from((0, 0)); + let second = GridNode::from((1, 1)); + let third = GridNode::from((2, 0)); + assert_eq!( + calculate_angle(first, second, third, true), + 90.0 + ); } - false } diff --git a/src/components/molecules/canvas.rs b/src/components/molecules/canvas.rs index a970320..e227810 100644 --- a/src/components/molecules/canvas.rs +++ b/src/components/molecules/canvas.rs @@ -191,6 +191,10 @@ fn on_mouse_down(map_state: &mut MapState, ev: &UiEvent) { selected_station.add_after(after); } + selected_station + .get_station() + .print_info(); + map_state.set_selected_station(selected_station); return; } diff --git a/src/components/state/map.rs b/src/components/state/map.rs index ca9330f..3443fab 100644 --- a/src/components/state/map.rs +++ b/src/components/state/map.rs @@ -1,5 +1,7 @@ //! Contains the [`MapState`] and all its methods. +use std::i32; + use leptos::{ html::Canvas, *, @@ -175,8 +177,42 @@ impl MapState { } } + /// Recalculate the algorithm settings based on the current map. + pub fn calculate_algorithm_settings(&mut self) { + self.algorithm_settings = AlgorithmSettings::default(); + + let mut x_limits = (i32::MAX, i32::MIN); + let mut y_limits = (i32::MAX, i32::MIN); + + for station in self + .map + .get_mut_stations() + { + let pos = station.get_pos(); + + x_limits.0 = x_limits + .0 + .min(pos.0); + x_limits.1 = x_limits + .1 + .max(pos.0); + y_limits.0 = y_limits + .0 + .min(pos.1); + y_limits.1 = y_limits + .1 + .max(pos.1); + } + + self.algorithm_settings + .grid_x_limits = (x_limits.0 - 2, x_limits.1 + 2); + self.algorithm_settings + .grid_y_limits = (y_limits.0 - 2, y_limits.1 + 2); + } + /// Run the full algorithm on the map. pub fn run_algorithm(&mut self) { + self.calculate_algorithm_settings(); unwrap_or_return!(recalculate_map( self.algorithm_settings, &mut self.map diff --git a/src/lib.rs b/src/lib.rs index ca66816..cd3291a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod utils; pub use components::{ CanvasState, + MapState, Home, StateProvider, }; diff --git a/src/models/edge.rs b/src/models/edge.rs index c127c20..b644abe 100644 --- a/src/models/edge.rs +++ b/src/models/edge.rs @@ -1,6 +1,13 @@ -use std::sync::atomic::{ - AtomicU64, - Ordering as AtomicOrdering, +use std::{ + fmt::{ + self, + Display, + Formatter, + }, + sync::atomic::{ + AtomicU64, + Ordering as AtomicOrdering, + }, }; use super::{ @@ -34,6 +41,12 @@ impl From for EdgeID { } } +impl Display for EdgeID { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + /// Represents an edge, which is the connection between two stations. #[derive(Clone, Debug)] pub struct Edge { @@ -47,6 +60,8 @@ pub struct Edge { nodes: Vec, /// Lines that use this edge lines: Vec, + /// If the edge is settled in the Dijkstra algorithm + is_settled: bool, } impl Edge { @@ -62,6 +77,7 @@ impl Edge { }), nodes: Vec::new(), lines: Vec::new(), + is_settled: false, } } @@ -151,6 +167,22 @@ impl Edge { self.nodes = nodes; } + /// A getter for if the edge is settled. + #[inline] + pub fn is_settled(&self) -> bool { + self.is_settled + } + + /// Settle the edge. + pub fn settle(&mut self) { + self.is_settled = true; + } + + /// Unsettle the edge. + pub fn unsettle(&mut self) { + self.is_settled = false; + } + /// Returns if the edge visits the node. pub fn visits_node(&self, map: &Map, node: GridNode) -> bool { if self @@ -215,7 +247,7 @@ impl Edge { Some((None, None)) } - /// Recalculates the nodes between the stations. + /// Recalculates the nodes between the stations using the A* algorithm. pub fn calculate_nodes(&mut self, map: &Map) { let from = map .get_station(self.get_from()) diff --git a/src/models/grid_node.rs b/src/models/grid_node.rs index e903951..59f0a81 100644 --- a/src/models/grid_node.rs +++ b/src/models/grid_node.rs @@ -1,6 +1,9 @@ -use std::ops::{ - Add, - Mul, +use std::{ + fmt::Display, + ops::{ + Add, + Mul, + }, }; use crate::components::CanvasState; @@ -104,6 +107,12 @@ impl PartialEq<(i32, i32)> for GridNode { } } +impl Display for GridNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.0, self.1) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/models/map.rs b/src/models/map.rs index 2c59037..541bbc2 100644 --- a/src/models/map.rs +++ b/src/models/map.rs @@ -308,4 +308,13 @@ impl Map { station.draw(canvas, state); } } + + /// Use the A* algorithm to calculate the edges between all stations + /// quickly. + pub fn quickcalc_edges(&mut self) { + let temp_map = self.clone(); + for edge in self.get_mut_edges() { + edge.calculate_nodes(&temp_map); + } + } } diff --git a/src/models/station.rs b/src/models/station.rs index 732daa2..c91a7a9 100644 --- a/src/models/station.rs +++ b/src/models/station.rs @@ -2,12 +2,15 @@ use std::{ f64, + fmt::Display, sync::atomic::{ AtomicU64, Ordering, }, }; +use leptos::logging; + use super::{ EdgeID, GridNode, @@ -36,6 +39,12 @@ impl From for u64 { } } +impl Display for StationID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + /// Represents a metro station, including its grid position on the map, its id, /// name and if the station should be greyed out when drawn to the canvas. #[derive(Clone, Debug)] @@ -50,7 +59,8 @@ pub struct Station { name: String, /// The edges that are connected to this station edges: Vec, - /// Marks the location of the station as locked by the user in the algorithm. + /// Marks the location of the station as locked by the user in the + /// algorithm. is_locked: bool, /// Marks the location of the station as settled in the algorithm. is_settled: bool, @@ -143,7 +153,7 @@ impl Station { /// Check if the station is settled. #[inline] pub fn is_settled(&self) -> bool { - self.is_settled + self.is_settled || self.is_locked } /// If the given node is a neighboring grid node to the station. @@ -177,6 +187,20 @@ impl Station { &self.edges } + #[allow(dead_code)] + pub fn print_info(&self) { + logging::log!( + "Station: {} at {:?} with edges [{:?}]", + self.id, + self.pos, + self.get_edges() + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join(", ") + ); + } + /// Draw the station to the given canvas. pub fn draw(&self, canvas: &CanvasContext<'_>, state: CanvasState) { if !state.is_on_canvas(self.get_pos()) { diff --git a/src/utils/error.rs b/src/utils/error.rs index f0597a3..ae973ed 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use leptos::logging; +use ordered_float::FloatIsNan; use wasm_bindgen::JsValue; /// A custom error type for the application. @@ -8,6 +9,7 @@ use wasm_bindgen::JsValue; pub enum Error { Json(serde_json::Error), GraphML(quick_xml::DeError), + InvalidFloat(FloatIsNan), DecodeError(String), Other(String), } @@ -34,6 +36,7 @@ impl Display for Error { match self { Self::Json(e) => write!(f, "JSON error: {e}"), Self::GraphML(e) => write!(f, "GraphML error: {e}"), + Self::InvalidFloat(e) => write!(f, "Invalid float error: {e}"), Self::DecodeError(e) => write!(f, "Decode error: {e}"), Self::Other(e) => write!(f, "Other error: {e}"), } @@ -52,6 +55,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: FloatIsNan) -> Self { + Self::InvalidFloat(e) + } +} + impl From for Error { fn from(e: csscolorparser::ParseColorError) -> Self { Self::DecodeError(format!("Failed to parse color: {e}")) @@ -74,6 +83,7 @@ impl PartialEq for Error { match (self, other) { (Self::Json(e1), Self::Json(e2)) => e1.to_string() == e2.to_string(), (Self::GraphML(e1), Self::GraphML(e2)) => e1.to_string() == e2.to_string(), + (Self::InvalidFloat(e1), Self::InvalidFloat(e2)) => e1 == e2, (Self::DecodeError(e1), Self::DecodeError(e2)) | (Self::Other(e1), Self::Other(e2)) => { e1 == e2 },