baby steps for routing

This commit is contained in:
Nicholas Orlowsky 2026-06-25 23:00:50 -04:00
parent 5998a56e98
commit 9d0586288d
No known key found for this signature in database
GPG key ID: A9F3BA4C0AA7A70B
7 changed files with 152 additions and 41 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
.env .env
tracking_data*
target/

View file

@ -6,9 +6,9 @@ use actix_web::{
HttpResponse, Responder, get, HttpResponse, Responder, get,
web::{self, Data}, web::{self, Data},
}; };
use libseptastic::{route::RouteType, stop_schedule::Trip}; use libseptastic::{route::RouteType, stop::StopType, stop_schedule::Trip};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashSet, sync::Arc}; use std::{collections::{HashMap, HashSet}, io::Write, sync::Arc};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
struct RouteQueryParams { struct RouteQueryParams {

View file

@ -5,10 +5,13 @@ use log::*;
use services::gtfs_pull; use services::gtfs_pull;
use std::{fs::File, io::Read, sync::Arc}; use std::{fs::File, io::Read, sync::Arc};
use crate::routing::{bfs_rts, construct_graph};
mod controllers; mod controllers;
mod services; mod services;
mod session_middleware; mod session_middleware;
mod templates; mod templates;
mod routing;
pub struct AppState { pub struct AppState {
gtfs_service: services::gtfs_pull::GtfsPullService, gtfs_service: services::gtfs_pull::GtfsPullService,
@ -58,6 +61,7 @@ async fn main() -> ::anyhow::Result<()> {
.service(controllers::stop::get_stop_table_html) .service(controllers::stop::get_stop_table_html)
.service(controllers::index::get_index_html) .service(controllers::index::get_index_html)
.service(controllers::map::get_map_element_html) .service(controllers::map::get_map_element_html)
.service(routing::get_routing_sample)
.service(actix_files::Files::new("/assets", "./assets")) .service(actix_files::Files::new("/assets", "./assets"))
}) })
.bind(("0.0.0.0", 8080))? .bind(("0.0.0.0", 8080))?

View file

@ -1,8 +1,10 @@
use std::{cmp::Ordering, collections::{BTreeSet, HashMap, HashSet}}; use std::{cmp::Ordering, collections::{BTreeSet, HashMap, HashSet}, sync::Arc};
use actix_web::{Responder, web::Data};
use libseptastic::stop::StopType;
use log::info; use log::info;
use crate::services; use crate::{AppState, services, session_middleware::{SessionResponder, SessionResponse}, templates::RoutingTemplate};
pub struct RoutingNodePointer { pub struct RoutingNodePointer {
pub stop_id: String, pub stop_id: String,
@ -79,23 +81,24 @@ pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
} }
pub fn get_stops_near(cds: Coordinates, pub fn get_platforms_near(cds: Coordinates,
all_stops: &HashMap<String, libseptastic::stop::Stop> all_platforms: &HashMap<String, Arc<libseptastic::stop::Platform>>
) -> HashSet<String> { ) -> HashSet<String> {
let near_thresh_km = 0.45; let near_thresh_km = 0.45;
let mut stops: HashSet<String> = HashSet::new(); let mut platforms: HashSet<String> = HashSet::new();
for stop_p in all_stops { for platform_p in all_platforms {
let stop = stop_p.1; let platform = platform_p.1;
let dist = haversine_distance(cds.lat, cds.lng, stop.lat, stop.lng); let dist = haversine_distance(cds.lat, cds.lng, platform.lat, platform.lng);
if dist.abs() < near_thresh_km { if dist.abs() < near_thresh_km {
stops.insert(stop.id.clone()); info!("dist {} for {} ({})", dist, platform.id.clone(), platform.name.clone());
platforms.insert(platform.id.clone());
} }
} }
stops platforms
} }
pub fn get_stop_as_node() { pub fn get_stop_as_node() {
@ -103,17 +106,17 @@ pub fn get_stop_as_node() {
pub fn construct_graph( pub fn construct_graph(
dest: Coordinates, dest: Coordinates,
all_stops: &HashMap<String, libseptastic::stop::Stop>, all_platforms: &HashMap<String, Arc<libseptastic::stop::Platform>>,
gtfs_service: &services::gtfs_pull::GtfsPullService gtfs_service: &services::gtfs_pull::GtfsPullService
) -> RoutingGraph { ) -> RoutingGraph {
let mut graph = RoutingGraph::new(); let mut graph = RoutingGraph::new();
let limited_rts = vec!["44", "65", "27", "38", "124", "125", "1"]; let limited_rts = vec!["SEPTABUS_44", "SEPTABUS_65", "SEPTABUS_27", "SEPTABUS_38", "SEPTABUS_124", "SEPTABUS_125", "SEPTABUS_1"];
for stop_p in all_stops { for platform_p in all_platforms {
let stop = stop_p.1; let platform = platform_p.1;
let ras = gtfs_service.get_routes_at_stop(&stop.id); let ras = gtfs_service.get_routes_at_stop(&platform.id);
let cont = { let cont = {
let mut ret = false; let mut ret = false;
@ -130,24 +133,24 @@ pub fn construct_graph(
continue; continue;
} }
graph.insert(stop.id.clone(), RoutingNode { graph.insert(platform.id.clone(), RoutingNode {
stop_id: stop.id.clone(), stop_id: platform.id.clone(),
stop_name: stop.name.clone(), stop_name: platform.name.clone(),
next_stops_per_routes: { next_stops_per_routes: {
let routes = gtfs_service.get_routes_at_stop(&stop.id); let routes = gtfs_service.get_routes_at_stop(&platform.id);
let mut other_stops = HashMap::<String, BTreeSet<RoutingNodePointer>>::new(); let mut other_stops = HashMap::<String, BTreeSet<RoutingNodePointer>>::new();
for route in &routes { for route in &routes {
let mut stops = gtfs_service.get_stops_by_route(&route); let mut platforms = gtfs_service.get_platforms_by_route(&route);
stops.remove(&stop.id); platforms.remove(&platform.id);
let rnps = { let rnps = {
let mut ret = BTreeSet::new(); let mut ret = BTreeSet::new();
for stop in &stops { for platform in &platforms {
let stp = all_stops.get(stop).unwrap(); let plt = all_platforms.get(platform).unwrap();
ret.insert(RoutingNodePointer{ ret.insert(RoutingNodePointer{
dest_dist: haversine_distance(dest.lat, dest.lng, stp.lat, stp.lng), dest_dist: haversine_distance(dest.lat, dest.lng, plt.lat, plt.lng),
stop_id: stop.clone(), stop_id: platform.clone(),
route_id: route.clone(), route_id: route.clone(),
stop_sequence: 0, stop_sequence: 0,
direction: 0 direction: 0
@ -167,23 +170,50 @@ pub fn construct_graph(
graph graph
} }
pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, dests: &HashSet<String>, mut visited: HashSet<String>, max_legs: u8) -> Option<String> {
#[derive(Clone)]
pub struct WalkRouteStep {
pub dist: f64,
pub dest: String
}
#[derive(Clone)]
pub struct TransitRouteStep {
pub platform: String,
pub route: String
}
#[derive(Clone)]
pub enum RouteStep {
Walk(WalkRouteStep),
Transit(TransitRouteStep),
Origin,
Destination
}
pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, dests: &HashSet<String>, mut visited: HashSet<String>, max_legs: u8) -> Option<Vec<RouteStep>> {
if max_legs == 0 { if max_legs == 0 {
return None; return None;
} }
let mut limited_rts = HashSet::new(); let mut limited_rts = HashSet::new();
for item in vec!["44", "65", "27", "38", "124", "125", "1"] {
for item in vec!["SEPTABUS_44", "SEPTABUS_65", "SEPTABUS_27", "SEPTABUS_38", "SEPTABUS_124", "SEPTABUS_125", "SEPTABUS_1"] {
limited_rts.insert(item); limited_rts.insert(item);
} }
if !limited_rts.contains(&route_id.as_str()) { if !limited_rts.contains(&route_id.as_str()) {
return None; info!("{} is not in the allowlist", route_id.as_str());
} return None;
}
info!("made it");
if let Some(origin_stop) = graph.get(origin) { if let Some(origin_stop) = graph.get(origin) {
if dests.contains(origin) { if dests.contains(origin) {
return Some(format!("[stop {} via rt {}] --> DEST", origin_stop.stop_name, route_id)) return Some(vec![ RouteStep::Transit(TransitRouteStep { platform: origin_stop.stop_name.clone(), route: route_id.clone() }) , RouteStep::Destination])
} }
if visited.contains(origin) { if visited.contains(origin) {
return None; return None;
} }
@ -196,8 +226,9 @@ pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, des
} }
for rnp in items.1 { for rnp in items.1 {
if let Some(rt) = bfs_rts_int(items.0, &rnp.stop_id, graph, dests, visited.clone(), max_legs - 1) { if let Some(mut rt) = bfs_rts_int(items.0, &rnp.stop_id, graph, dests, visited.clone(), max_legs - 1).clone() {
return Some(format!("[stop {} via rt {}] >>[XFER]>> {}", origin_stop.stop_name, route_id, rt)) rt.insert(0, RouteStep::Transit(TransitRouteStep{ platform: origin_stop.stop_name.clone(), route: route_id.clone()}));
return Some(rt)
} }
} }
} }
@ -206,16 +237,18 @@ pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, des
None None
} }
pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> String { pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> Vec<Vec<RouteStep>> {
let mut resp = String::new(); let mut resp = vec![];
if let Some(origin_stop) = graph.get(origin) { if let Some(origin_stop) = graph.get(origin) {
for items in &origin_stop.next_stops_per_routes { for items in &origin_stop.next_stops_per_routes {
let route_id = items.0; let route_id = items.0;
for rnp in items.1 { for rnp in items.1 {
if let Some(rt) = bfs_rts_int(route_id, &rnp.stop_id, graph, dests, HashSet::new(), 3) { if let Some(mut rt) = bfs_rts_int(route_id, &rnp.stop_id, graph, dests, HashSet::new(), 3).clone() {
resp += format!("ORIGIN --> [stop {} via rt {}] --> {}\n", origin_stop.stop_name, route_id, rt).as_str(); rt.insert(0, RouteStep::Walk(WalkRouteStep { dist: 0.0, dest: origin_stop.stop_name.clone() }));
rt.insert(0, RouteStep::Origin);
resp.push(rt);
} }
} }
} }
@ -223,3 +256,32 @@ pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -
resp resp
} }
#[actix_web::get("/routing")]
pub async fn get_routing_sample(
state: Data<Arc<AppState>>,
resp: SessionResponse,
) -> impl Responder {
let mut all_platforms = HashMap::new();
state.gtfs_service.get_all_stops().iter().map(|stop| {
match &stop.1.platforms {
StopType::SinglePlatform(sp) => vec![sp.clone()],
StopType::MultiPlatform(mp) => mp.clone()
}
}).flatten().for_each(|p| {
all_platforms.insert((&p.id).clone(), p.clone());
});
let srcs = get_platforms_near(Coordinates { lat: 39.95707, lng: -75.16625 }, &all_platforms);
let src = srcs.iter().next().unwrap();
let dests = get_platforms_near(Coordinates { lat: 40.009144, lng: -75.213796 }, &all_platforms);
let rg = construct_graph(Coordinates { lat: 40.009144, lng: -75.213796 }, &all_platforms, &state.gtfs_service);
resp.respond(
"test",
"test",
RoutingTemplate {
trips: bfs_rts(&String::from("SEPTABUS_3057"), &rg, &dests)
}
)
}

View file

@ -71,6 +71,7 @@ pub struct TransitData {
// extended lookup methods // extended lookup methods
pub route_id_by_stops: HashMap<String, HashSet<String>>, pub route_id_by_stops: HashMap<String, HashSet<String>>,
pub stops_by_route_id: HashMap<String, HashSet<String>>, pub stops_by_route_id: HashMap<String, HashSet<String>>,
pub platforms_by_route_id: HashMap<String, HashSet<String>>,
pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>, pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>,
} }
@ -96,6 +97,7 @@ impl TransitData {
platforms: HashMap::new(), platforms: HashMap::new(),
route_id_by_stops: HashMap::new(), route_id_by_stops: HashMap::new(),
stops_by_route_id: HashMap::new(), stops_by_route_id: HashMap::new(),
platforms_by_route_id: HashMap::new(),
stops_by_platform_id: HashMap::new(), stops_by_platform_id: HashMap::new(),
calendar_days: HashMap::new(), calendar_days: HashMap::new(),
directions: HashMap::new(), directions: HashMap::new(),
@ -206,6 +208,16 @@ impl GtfsPullService {
.unwrap_or(&HashSet::new()) .unwrap_or(&HashSet::new())
.clone() .clone()
} }
pub fn get_platforms_by_route(&self, id: &String) -> HashSet<String> {
let l_state = self.state.lock().unwrap();
l_state
.transit_data
.platforms_by_route_id
.get(id)
.unwrap_or(&HashSet::new())
.clone()
}
pub fn get_stop_by_id(&self, id: &String) -> Option<libseptastic::stop::Stop> { pub fn get_stop_by_id(&self, id: &String) -> Option<libseptastic::stop::Stop> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
@ -531,6 +543,13 @@ impl GtfsPullService {
.entry(global_rt_id.clone()) .entry(global_rt_id.clone())
.or_insert(HashSet::new()) .or_insert(HashSet::new())
.insert(platform.id.clone()); .insert(platform.id.clone());
state
.transit_data
.platforms_by_route_id
.entry(global_rt_id.clone())
.or_insert(HashSet::new())
.insert(platform.id.clone());
Some(libseptastic::stop_schedule::StopSchedule { Some(libseptastic::stop_schedule::StopSchedule {
arrival_time: i64::from(s.arrival_time?), arrival_time: i64::from(s.arrival_time?),

View file

@ -13,6 +13,7 @@ use std::{
}; };
use crate::controllers::stop::StopFilter; use crate::controllers::stop::StopFilter;
use crate::routing::RouteStep;
#[derive(askama::Template)] #[derive(askama::Template)]
#[template(path = "layout.html")] #[template(path = "layout.html")]
@ -93,6 +94,12 @@ pub struct StopTableTemplate {
pub stop_id: String, pub stop_id: String,
} }
#[derive(askama::Template)]
#[template(path = "routing.html")]
pub struct RoutingTemplate {
pub trips: Vec<Vec<RouteStep>>,
}
#[derive(askama::Template)] #[derive(askama::Template)]
#[template(path = "stop_search_results.html")] #[template(path = "stop_search_results.html")]
pub struct StopSearchResults { pub struct StopSearchResults {

View file

@ -0,0 +1,16 @@
{% for trip in trips %}
<div style="border-width: 1px; border-color: black; border-style: dotted; margin: 10px;">
{% for step in trip %}
{% match step %}
{% when RouteStep::Walk(walk) %}
<p>Walk {{ walk.dist }}m to {{ walk.dest }}</p>
{% when RouteStep::Transit(transit) %}
<p>Ride {{transit.route}} to {{ transit.platform }}</p>
{% when RouteStep::Origin %}
<p>begin at origin</p>
{% when RouteStep::Destination %}
<p>arrive at destination</p>
{% endmatch %}
{% endfor %}
</div>
{% endfor %}