From 1b326e2644d8253670f7d01db3bebca0162f797c Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Fri, 16 Aug 2024 08:57:43 +0100 Subject: [PATCH] Organize code into core graph stuff, with analysis-ish layer on top --- backend/src/buffer.rs | 5 +- backend/src/{ => graph}/amenity.rs | 0 backend/src/{ => graph}/costs.rs | 0 backend/src/graph/isochrone.rs | 72 ++++++ backend/src/{graph.rs => graph/mod.rs} | 30 ++- backend/src/{ => graph}/route.rs | 17 +- backend/src/{ => graph}/scrape.rs | 8 +- backend/src/graph/transit_route.rs | 289 +++++++++++++++++++++++++ backend/src/isochrone.rs | 71 +----- backend/src/lib.rs | 35 +-- backend/src/transit_route.rs | 289 ------------------------- 11 files changed, 412 insertions(+), 404 deletions(-) rename backend/src/{ => graph}/amenity.rs (100%) rename backend/src/{ => graph}/costs.rs (100%) create mode 100644 backend/src/graph/isochrone.rs rename backend/src/{graph.rs => graph/mod.rs} (94%) rename backend/src/{ => graph}/route.rs (95%) rename backend/src/{ => graph}/scrape.rs (97%) create mode 100644 backend/src/graph/transit_route.rs delete mode 100644 backend/src/transit_route.rs diff --git a/backend/src/buffer.rs b/backend/src/buffer.rs index e5e33dd..473cd46 100644 --- a/backend/src/buffer.rs +++ b/backend/src/buffer.rs @@ -5,8 +5,7 @@ use anyhow::Result; use chrono::NaiveTime; use geojson::{Feature, GeoJson, Geometry}; -use crate::route::PathStep; -use crate::{Graph, Mode}; +use crate::graph::{Graph, Mode, PathStep}; pub fn buffer_route( graph: &Graph, @@ -19,7 +18,7 @@ pub fn buffer_route( let mut route_roads = HashSet::new(); let mut starts = HashSet::new(); for step in steps { - if let crate::route::PathStep::Road { road, .. } = step { + if let PathStep::Road { road, .. } = step { route_roads.insert(road); let road = &graph.roads[road.0]; starts.insert(road.src_i); diff --git a/backend/src/amenity.rs b/backend/src/graph/amenity.rs similarity index 100% rename from backend/src/amenity.rs rename to backend/src/graph/amenity.rs diff --git a/backend/src/costs.rs b/backend/src/graph/costs.rs similarity index 100% rename from backend/src/costs.rs rename to backend/src/graph/costs.rs diff --git a/backend/src/graph/isochrone.rs b/backend/src/graph/isochrone.rs new file mode 100644 index 0000000..c5d11f5 --- /dev/null +++ b/backend/src/graph/isochrone.rs @@ -0,0 +1,72 @@ +use chrono::NaiveTime; +use std::collections::{BinaryHeap, HashMap, HashSet}; +use std::time::Duration; + +use utils::PriorityQueueItem; + +use super::costs::cost; +use crate::graph::{Graph, IntersectionID, Mode, RoadID}; + +impl Graph { + // TODO Doesn't account for start/end distance along roads + pub fn get_costs( + &self, + starts: Vec, + mode: Mode, + public_transit: bool, + start_time: NaiveTime, + end_time: NaiveTime, + ) -> HashMap { + let mut visited: HashSet = HashSet::new(); + let mut cost_per_road: HashMap = HashMap::new(); + let mut queue: BinaryHeap> = BinaryHeap::new(); + + for start in starts { + queue.push(PriorityQueueItem::new(start_time, start)); + } + + while let Some(current) = queue.pop() { + if visited.contains(¤t.value) { + continue; + } + visited.insert(current.value); + if current.cost > end_time { + continue; + } + + for r in &self.intersections[current.value.0].roads { + let road = &self.roads[r.0]; + let total_cost = current.cost + cost(road, mode); + cost_per_road + .entry(*r) + .or_insert((total_cost - start_time).to_std().unwrap()); + + if road.src_i == current.value && road.allows_forwards(mode) { + queue.push(PriorityQueueItem::new(total_cost, road.dst_i)); + } + if road.dst_i == current.value && road.allows_backwards(mode) { + queue.push(PriorityQueueItem::new(total_cost, road.src_i)); + } + + if public_transit { + for stop1 in &road.stops { + // Find all trips leaving from this step before the end_time + for next_step in self.gtfs.trips_from( + *stop1, + current.cost, + (end_time - current.cost).to_std().unwrap(), + ) { + // TODO Awkwardly, arrive at both intersections for the next stop's road + let stop2_road = &self.roads[self.gtfs.stops[next_step.stop2.0].road.0]; + for i in [stop2_road.src_i, stop2_road.dst_i] { + queue.push(PriorityQueueItem::new(next_step.time2, i)); + } + } + } + } + } + } + + cost_per_road + } +} diff --git a/backend/src/graph.rs b/backend/src/graph/mod.rs similarity index 94% rename from backend/src/graph.rs rename to backend/src/graph/mod.rs index 4dac66d..e987acc 100644 --- a/backend/src/graph.rs +++ b/backend/src/graph/mod.rs @@ -1,3 +1,10 @@ +mod amenity; +mod costs; +mod isochrone; +mod route; +mod scrape; +mod transit_route; + use anyhow::Result; use enum_map::{Enum, EnumMap}; use geo::{Coord, LineLocatePoint, LineString, Point, Polygon}; @@ -6,9 +13,10 @@ use rstar::{primitives::GeomWithData, RTree}; use serde::{Deserialize, Serialize}; use utils::Mercator; -use crate::amenity::Amenity; +use self::amenity::Amenity; +use self::route::Router; +use crate::gtfs::TripID; use crate::gtfs::{GtfsModel, StopID}; -use crate::route::Router; #[derive(Serialize, Deserialize)] pub struct Graph { @@ -212,3 +220,21 @@ pub struct Position { pub fraction_along: f64, pub intersection: IntersectionID, } + +pub enum GtfsSource { + Dir(String), + Geomedea(String), + None, +} + +pub enum PathStep { + Road { + road: RoadID, + forwards: bool, + }, + Transit { + stop1: StopID, + trip: TripID, + stop2: StopID, + }, +} diff --git a/backend/src/route.rs b/backend/src/graph/route.rs similarity index 95% rename from backend/src/route.rs rename to backend/src/graph/route.rs index cdc2069..8945ed0 100644 --- a/backend/src/route.rs +++ b/backend/src/graph/route.rs @@ -8,21 +8,8 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use utils::{deserialize_nodemap, LineSplit, NodeMap}; -use crate::costs::cost; -use crate::graph::{Graph, IntersectionID, Mode, Position, Road, RoadID}; -use crate::gtfs::{StopID, TripID}; - -pub enum PathStep { - Road { - road: RoadID, - forwards: bool, - }, - Transit { - stop1: StopID, - trip: TripID, - stop2: StopID, - }, -} +use super::costs::cost; +use crate::graph::{Graph, IntersectionID, Mode, PathStep, Position, Road}; #[derive(Serialize, Deserialize)] pub struct Router { diff --git a/backend/src/scrape.rs b/backend/src/graph/scrape.rs similarity index 97% rename from backend/src/scrape.rs rename to backend/src/graph/scrape.rs index 13ff23f..dbabe3a 100644 --- a/backend/src/scrape.rs +++ b/backend/src/graph/scrape.rs @@ -8,14 +8,14 @@ use osm_reader::OsmID; use rstar::RTree; use utils::Tags; -use crate::amenity::Amenity; +use super::amenity::Amenity; +use super::route::Router; use crate::graph::{ - AmenityID, Direction, EdgeLocation, Graph, Intersection, IntersectionID, Mode, Road, RoadID, + AmenityID, Direction, EdgeLocation, Graph, GtfsSource, Intersection, IntersectionID, Mode, + Road, RoadID, }; use crate::gtfs::{GtfsModel, StopID}; -use crate::route::Router; use crate::timer::Timer; -use crate::GtfsSource; struct ReadAmenities { amenities: Vec, diff --git a/backend/src/graph/transit_route.rs b/backend/src/graph/transit_route.rs new file mode 100644 index 0000000..ab4b2ca --- /dev/null +++ b/backend/src/graph/transit_route.rs @@ -0,0 +1,289 @@ +use std::collections::hash_map::Entry; +use std::collections::{BinaryHeap, HashMap}; +use std::time::Duration; + +use anyhow::{bail, Result}; +use chrono::NaiveTime; +use geo::{EuclideanDistance, LineString}; +use geojson::{Feature, GeoJson, Geometry}; +use utils::PriorityQueueItem; + +use super::costs::cost; +use crate::graph::{Graph, IntersectionID, Mode, PathStep, Position}; +use crate::timer::Timer; + +impl Graph { + pub fn transit_route_gj( + &self, + start: Position, + end: Position, + // TODO Parameterizing this function gets messy, but splitting into two separate doesn't seem + // like a good idea yet + debug_search: bool, + use_heuristic: bool, + start_time: NaiveTime, + mut timer: Timer, + ) -> Result { + if start == end { + bail!("start = end"); + } + // TODO Handle start.road == end.road case. Share code somewhere. + + let end_pt = self.intersections[end.intersection.0].point; + // TODO Share constant properly + // TODO Think through if this is admissible and/or consistent + let heuristic = |i: IntersectionID| { + if use_heuristic { + Duration::from_secs_f64( + self.intersections[i.0].point.euclidean_distance(&end_pt) / 1.34112, + ) + } else { + Duration::ZERO + } + }; + + // TODO stops are associated with roads, so the steps using transit are going to look a little + // weird / be a little ambiguous + // + // or rethink the nodes and edges in the graph. nodes are pathsteps -- a road in some direction + // or a transit thing. an edge is a turn or a transition to/from transit + + let mut backrefs: HashMap = HashMap::new(); + + // If we're debugging the search process, just remember the order of visited nodes + let mut search_record: Vec = Vec::new(); + + timer.step("dijkstra"); + // Store the actual cost/time to reach somewhere as the item. Include a heuristic + let mut queue: BinaryHeap> = + BinaryHeap::new(); + queue.push(PriorityQueueItem::new( + start_time + heuristic(start.intersection), + (start.intersection, start_time), + )); + + while let Some(current) = queue.pop() { + // Don't use current.cost, since it might include a heuristic + let (current_i, current_time) = current.value; + if current_i == end.intersection { + if debug_search { + return render_debug(search_record, backrefs, self, timer); + } else { + return render_path(backrefs, self, start, end, timer); + } + } + if debug_search { + search_record.push(current_i); + } + + for r in &self.intersections[current_i.0].roads { + let road = &self.roads[r.0]; + + // Handle walking to the other end of the road + let total_cost = current_time + cost(road, Mode::Foot); + if road.src_i == current_i && road.allows_forwards(Mode::Foot) { + if let Entry::Vacant(entry) = backrefs.entry(road.dst_i) { + entry.insert(Backreference { + src_i: current_i, + step: PathStep::Road { + road: *r, + forwards: true, + }, + time1: current_time, + time2: total_cost, + }); + queue.push(PriorityQueueItem::new( + total_cost + heuristic(road.dst_i), + (road.dst_i, total_cost), + )); + } + } else if road.dst_i == current_i && road.allows_backwards(Mode::Foot) { + if let Entry::Vacant(entry) = backrefs.entry(road.src_i) { + entry.insert(Backreference { + src_i: current_i, + step: PathStep::Road { + road: *r, + forwards: false, + }, + time1: current_time, + time2: total_cost, + }); + queue.push(PriorityQueueItem::new( + total_cost + heuristic(road.src_i), + (road.src_i, total_cost), + )); + } + } + + // Use transit! + for stop1 in &road.stops { + // Find all trips leaving from this step in the next 30 minutes + // TODO Figure out how to prune that search time better + for next_step in + self.gtfs + .trips_from(*stop1, current_time, Duration::from_secs(30 * 60)) + { + // TODO Here's the awkwardness -- arrive at both the intersections for that + // road + let stop2_road = &self.roads[self.gtfs.stops[next_step.stop2.0].road.0]; + for i in [stop2_road.src_i, stop2_road.dst_i] { + if let Entry::Vacant(entry) = backrefs.entry(i) { + entry.insert(Backreference { + src_i: current_i, + step: PathStep::Transit { + stop1: *stop1, + trip: next_step.trip, + stop2: next_step.stop2, + }, + time1: next_step.time1, + time2: next_step.time2, + }); + queue.push(PriorityQueueItem::new( + next_step.time2 + heuristic(i), + (i, next_step.time2), + )); + } + } + } + } + } + } + + bail!("No path found"); + } +} + +// How'd we get somewhere? +struct Backreference { + // Where were we at the beginning of this step? + src_i: IntersectionID, + step: PathStep, + // When'd we start this step? + time1: NaiveTime, + // When'd we finish? + time2: NaiveTime, +} + +fn render_path( + mut backrefs: HashMap, + graph: &Graph, + start: Position, + end: Position, + mut timer: Timer, +) -> Result { + timer.step("render"); + + // Just get PathSteps in order first (Step, time1, time2) + let mut steps: Vec<(PathStep, NaiveTime, NaiveTime)> = Vec::new(); + let mut at = end.intersection; + loop { + if at == start.intersection { + break; + } + let backref = backrefs.remove(&at).unwrap(); + steps.push((backref.step, backref.time1, backref.time2)); + at = backref.src_i; + } + steps.reverse(); + + // Assemble PathSteps into features. Group road and transit steps together + let mut features = Vec::new(); + for chunk in steps.chunk_by(|a, b| match (&a.0, &b.0) { + (PathStep::Road { .. }, PathStep::Road { .. }) => true, + (PathStep::Transit { trip: trip1, .. }, PathStep::Transit { trip: trip2, .. }) => { + trip1 == trip2 + } + _ => false, + }) { + let mut pts = Vec::new(); + let mut num_stops = 0; + let mut trip_id = None; + for (step, _, _) in chunk { + match step { + PathStep::Road { road, forwards } => { + let road = &graph.roads[road.0]; + if *forwards { + pts.extend(road.linestring.0.clone()); + } else { + let mut rev = road.linestring.0.clone(); + rev.reverse(); + pts.extend(rev); + } + } + PathStep::Transit { stop1, stop2, trip } => { + trip_id = Some(trip); + num_stops += 1; + pts.push(graph.gtfs.stops[stop1.0].point.into()); + pts.push(graph.gtfs.stops[stop2.0].point.into()); + } + } + } + pts.dedup(); + + let mut f = Feature::from(Geometry::from( + &graph.mercator.to_wgs84(&LineString::new(pts)), + )); + f.set_property("time1", chunk[0].1.to_string()); + f.set_property("time2", chunk.last().unwrap().2.to_string()); + + if let Some(trip) = trip_id { + f.set_property("kind", "transit"); + f.set_property("trip", trip.0); + f.set_property( + "route", + graph.gtfs.routes[graph.gtfs.trips[trip.0].route.0].describe(), + ); + f.set_property("num_stops", num_stops); + } else { + f.set_property("kind", "road"); + } + features.push(f); + } + timer.done(); + Ok(serde_json::to_string(&GeoJson::from(features))?) +} + +fn render_debug( + search_record: Vec, + mut backrefs: HashMap, + graph: &Graph, + mut timer: Timer, +) -> Result { + timer.step("render"); + // Create a FeatureCollection with the nodes searched, in order. Pairs of linestrings (a step + // to get somewhere) and points (for the intersection) + let mut features = Vec::new(); + // Skip the first node, because it'll have no backreference + for i in search_record.into_iter().skip(1) { + let backref = backrefs.remove(&i).unwrap(); + match backref.step { + PathStep::Road { road, .. } => { + let mut f = Feature::from(Geometry::from( + &graph.mercator.to_wgs84(&graph.roads[road.0].linestring), + )); + f.set_property("kind", "road"); + features.push(f); + } + PathStep::Transit { stop1, stop2, .. } => { + let mut f = Feature::from(Geometry::from(&graph.mercator.to_wgs84( + &LineString::new(vec![ + graph.gtfs.stops[stop1.0].point.into(), + graph.gtfs.stops[stop2.0].point.into(), + ]), + ))); + f.set_property("kind", "transit"); + features.push(f); + } + } + + let mut f = Feature::from(Geometry::from( + &graph.mercator.to_wgs84(&graph.intersections[i.0].point), + )); + f.set_property("time", backref.time2.to_string()); + features.push(f); + } + + let json = serde_json::to_string(&GeoJson::from(features))?; + timer.done(); + Ok(json) +} diff --git a/backend/src/isochrone.rs b/backend/src/isochrone.rs index 7d15dd5..a45bf56 100644 --- a/backend/src/isochrone.rs +++ b/backend/src/isochrone.rs @@ -1,79 +1,14 @@ -use std::collections::{BinaryHeap, HashMap, HashSet}; +use std::collections::HashMap; use std::time::Duration; use anyhow::Result; use chrono::NaiveTime; use geo::{Coord, Densify}; -use utils::{Grid, PriorityQueueItem}; +use utils::Grid; -use crate::costs::cost; -use crate::graph::{Graph, IntersectionID, Mode, RoadID}; +use crate::graph::{Graph, Mode, RoadID}; use crate::timer::Timer; -impl Graph { - // TODO Doesn't account for start/end distance along roads - pub fn get_costs( - &self, - starts: Vec, - mode: Mode, - public_transit: bool, - start_time: NaiveTime, - end_time: NaiveTime, - ) -> HashMap { - let mut visited: HashSet = HashSet::new(); - let mut cost_per_road: HashMap = HashMap::new(); - let mut queue: BinaryHeap> = BinaryHeap::new(); - - for start in starts { - queue.push(PriorityQueueItem::new(start_time, start)); - } - - while let Some(current) = queue.pop() { - if visited.contains(¤t.value) { - continue; - } - visited.insert(current.value); - if current.cost > end_time { - continue; - } - - for r in &self.intersections[current.value.0].roads { - let road = &self.roads[r.0]; - let total_cost = current.cost + cost(road, mode); - cost_per_road - .entry(*r) - .or_insert((total_cost - start_time).to_std().unwrap()); - - if road.src_i == current.value && road.allows_forwards(mode) { - queue.push(PriorityQueueItem::new(total_cost, road.dst_i)); - } - if road.dst_i == current.value && road.allows_backwards(mode) { - queue.push(PriorityQueueItem::new(total_cost, road.src_i)); - } - - if public_transit { - for stop1 in &road.stops { - // Find all trips leaving from this step before the end_time - for next_step in self.gtfs.trips_from( - *stop1, - current.cost, - (end_time - current.cost).to_std().unwrap(), - ) { - // TODO Awkwardly, arrive at both intersections for the next stop's road - let stop2_road = &self.roads[self.gtfs.stops[next_step.stop2.0].road.0]; - for i in [stop2_road.src_i, stop2_road.dst_i] { - queue.push(PriorityQueueItem::new(next_step.time2, i)); - } - } - } - } - } - } - - cost_per_road - } -} - pub fn calculate( graph: &Graph, req: Coord, diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 0b56b16..5425cbd 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -16,17 +16,12 @@ pub use graph::{Graph, Mode}; pub use gtfs::GtfsModel; pub use timer::Timer; -mod amenity; mod buffer; -mod costs; mod graph; mod gtfs; mod isochrone; -mod route; mod score; -mod scrape; mod timer; -mod transit_route; static START: Once = Once::new(); @@ -55,8 +50,8 @@ impl MapModel { }); let gtfs = match gtfs_url { - Some(url) => GtfsSource::Geomedea(url), - None => GtfsSource::None, + Some(url) => graph::GtfsSource::Geomedea(url), + None => graph::GtfsSource::None, }; let graph = if is_osm { Graph::new(input_bytes, gtfs, Timer::new("build graph", progress_cb)) @@ -198,16 +193,16 @@ impl MapModel { ); if req.mode == "transit" { - transit_route::route_gj( - &self.graph, - start, - end, - req.debug_search, - req.use_heuristic, - NaiveTime::parse_from_str(&req.start_time, "%H:%M").map_err(err_to_js)?, - Timer::new("route request", None), - ) - .map_err(err_to_js) + self.graph + .transit_route_gj( + start, + end, + req.debug_search, + req.use_heuristic, + NaiveTime::parse_from_str(&req.start_time, "%H:%M").map_err(err_to_js)?, + Timer::new("route request", None), + ) + .map_err(err_to_js) } else { self.graph.router[mode] .route_gj(&self.graph, start, end) @@ -260,9 +255,3 @@ pub struct ScoreRequest { fn err_to_js(err: E) -> JsValue { JsValue::from_str(&err.to_string()) } - -pub enum GtfsSource { - Dir(String), - Geomedea(String), - None, -} diff --git a/backend/src/transit_route.rs b/backend/src/transit_route.rs deleted file mode 100644 index f08e4ff..0000000 --- a/backend/src/transit_route.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::collections::hash_map::Entry; -use std::collections::{BinaryHeap, HashMap}; -use std::time::Duration; - -use anyhow::{bail, Result}; -use chrono::NaiveTime; -use geo::{EuclideanDistance, LineString}; -use geojson::{Feature, GeoJson, Geometry}; -use utils::PriorityQueueItem; - -use crate::costs::cost; -use crate::graph::{Graph, IntersectionID, Mode, Position}; -use crate::route::PathStep; -use crate::timer::Timer; - -pub fn route_gj( - graph: &Graph, - start: Position, - end: Position, - // TODO Parameterizing this function gets messy, but splitting into two separate doesn't seem - // like a good idea yet - debug_search: bool, - use_heuristic: bool, - start_time: NaiveTime, - mut timer: Timer, -) -> Result { - if start == end { - bail!("start = end"); - } - // TODO Handle start.road == end.road case. Share code somewhere. - - let end_pt = graph.intersections[end.intersection.0].point; - // TODO Share constant properly - // TODO Think through if this is admissible and/or consistent - let heuristic = |i: IntersectionID| { - if use_heuristic { - Duration::from_secs_f64( - graph.intersections[i.0].point.euclidean_distance(&end_pt) / 1.34112, - ) - } else { - Duration::ZERO - } - }; - - // TODO stops are associated with roads, so the steps using transit are going to look a little - // weird / be a little ambiguous - // - // or rethink the nodes and edges in the graph. nodes are pathsteps -- a road in some direction - // or a transit thing. an edge is a turn or a transition to/from transit - - let mut backrefs: HashMap = HashMap::new(); - - // If we're debugging the search process, just remember the order of visited nodes - let mut search_record: Vec = Vec::new(); - - timer.step("dijkstra"); - // Store the actual cost/time to reach somewhere as the item. Include a heuristic - let mut queue: BinaryHeap> = - BinaryHeap::new(); - queue.push(PriorityQueueItem::new( - start_time + heuristic(start.intersection), - (start.intersection, start_time), - )); - - while let Some(current) = queue.pop() { - // Don't use current.cost, since it might include a heuristic - let (current_i, current_time) = current.value; - if current_i == end.intersection { - if debug_search { - return render_debug(search_record, backrefs, graph, timer); - } else { - return render_path(backrefs, graph, start, end, timer); - } - } - if debug_search { - search_record.push(current_i); - } - - for r in &graph.intersections[current_i.0].roads { - let road = &graph.roads[r.0]; - - // Handle walking to the other end of the road - let total_cost = current_time + cost(road, Mode::Foot); - if road.src_i == current_i && road.allows_forwards(Mode::Foot) { - if let Entry::Vacant(entry) = backrefs.entry(road.dst_i) { - entry.insert(Backreference { - src_i: current_i, - step: PathStep::Road { - road: *r, - forwards: true, - }, - time1: current_time, - time2: total_cost, - }); - queue.push(PriorityQueueItem::new( - total_cost + heuristic(road.dst_i), - (road.dst_i, total_cost), - )); - } - } else if road.dst_i == current_i && road.allows_backwards(Mode::Foot) { - if let Entry::Vacant(entry) = backrefs.entry(road.src_i) { - entry.insert(Backreference { - src_i: current_i, - step: PathStep::Road { - road: *r, - forwards: false, - }, - time1: current_time, - time2: total_cost, - }); - queue.push(PriorityQueueItem::new( - total_cost + heuristic(road.src_i), - (road.src_i, total_cost), - )); - } - } - - // Use transit! - for stop1 in &road.stops { - // Find all trips leaving from this step in the next 30 minutes - // TODO Figure out how to prune that search time better - for next_step in - graph - .gtfs - .trips_from(*stop1, current_time, Duration::from_secs(30 * 60)) - { - // TODO Here's the awkwardness -- arrive at both the intersections for that - // road - let stop2_road = &graph.roads[graph.gtfs.stops[next_step.stop2.0].road.0]; - for i in [stop2_road.src_i, stop2_road.dst_i] { - if let Entry::Vacant(entry) = backrefs.entry(i) { - entry.insert(Backreference { - src_i: current_i, - step: PathStep::Transit { - stop1: *stop1, - trip: next_step.trip, - stop2: next_step.stop2, - }, - time1: next_step.time1, - time2: next_step.time2, - }); - queue.push(PriorityQueueItem::new( - next_step.time2 + heuristic(i), - (i, next_step.time2), - )); - } - } - } - } - } - } - - bail!("No path found"); -} - -// How'd we get somewhere? -struct Backreference { - // Where were we at the beginning of this step? - src_i: IntersectionID, - step: PathStep, - // When'd we start this step? - time1: NaiveTime, - // When'd we finish? - time2: NaiveTime, -} - -fn render_path( - mut backrefs: HashMap, - graph: &Graph, - start: Position, - end: Position, - mut timer: Timer, -) -> Result { - timer.step("render"); - - // Just get PathSteps in order first (Step, time1, time2) - let mut steps: Vec<(PathStep, NaiveTime, NaiveTime)> = Vec::new(); - let mut at = end.intersection; - loop { - if at == start.intersection { - break; - } - let backref = backrefs.remove(&at).unwrap(); - steps.push((backref.step, backref.time1, backref.time2)); - at = backref.src_i; - } - steps.reverse(); - - // Assemble PathSteps into features. Group road and transit steps together - let mut features = Vec::new(); - for chunk in steps.chunk_by(|a, b| match (&a.0, &b.0) { - (PathStep::Road { .. }, PathStep::Road { .. }) => true, - (PathStep::Transit { trip: trip1, .. }, PathStep::Transit { trip: trip2, .. }) => { - trip1 == trip2 - } - _ => false, - }) { - let mut pts = Vec::new(); - let mut num_stops = 0; - let mut trip_id = None; - for (step, _, _) in chunk { - match step { - PathStep::Road { road, forwards } => { - let road = &graph.roads[road.0]; - if *forwards { - pts.extend(road.linestring.0.clone()); - } else { - let mut rev = road.linestring.0.clone(); - rev.reverse(); - pts.extend(rev); - } - } - PathStep::Transit { stop1, stop2, trip } => { - trip_id = Some(trip); - num_stops += 1; - pts.push(graph.gtfs.stops[stop1.0].point.into()); - pts.push(graph.gtfs.stops[stop2.0].point.into()); - } - } - } - pts.dedup(); - - let mut f = Feature::from(Geometry::from( - &graph.mercator.to_wgs84(&LineString::new(pts)), - )); - f.set_property("time1", chunk[0].1.to_string()); - f.set_property("time2", chunk.last().unwrap().2.to_string()); - - if let Some(trip) = trip_id { - f.set_property("kind", "transit"); - f.set_property("trip", trip.0); - f.set_property( - "route", - graph.gtfs.routes[graph.gtfs.trips[trip.0].route.0].describe(), - ); - f.set_property("num_stops", num_stops); - } else { - f.set_property("kind", "road"); - } - features.push(f); - } - timer.done(); - Ok(serde_json::to_string(&GeoJson::from(features))?) -} - -fn render_debug( - search_record: Vec, - mut backrefs: HashMap, - graph: &Graph, - mut timer: Timer, -) -> Result { - timer.step("render"); - // Create a FeatureCollection with the nodes searched, in order. Pairs of linestrings (a step - // to get somewhere) and points (for the intersection) - let mut features = Vec::new(); - // Skip the first node, because it'll have no backreference - for i in search_record.into_iter().skip(1) { - let backref = backrefs.remove(&i).unwrap(); - match backref.step { - PathStep::Road { road, .. } => { - let mut f = Feature::from(Geometry::from( - &graph.mercator.to_wgs84(&graph.roads[road.0].linestring), - )); - f.set_property("kind", "road"); - features.push(f); - } - PathStep::Transit { stop1, stop2, .. } => { - let mut f = Feature::from(Geometry::from(&graph.mercator.to_wgs84( - &LineString::new(vec![ - graph.gtfs.stops[stop1.0].point.into(), - graph.gtfs.stops[stop2.0].point.into(), - ]), - ))); - f.set_property("kind", "transit"); - features.push(f); - } - } - - let mut f = Feature::from(Geometry::from( - &graph.mercator.to_wgs84(&graph.intersections[i.0].point), - )); - f.set_property("time", backref.time2.to_string()); - features.push(f); - } - - let json = serde_json::to_string(&GeoJson::from(features))?; - timer.done(); - Ok(json) -}