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
tracking_data*
target/

View file

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

View file

@ -5,10 +5,13 @@ use log::*;
use services::gtfs_pull;
use std::{fs::File, io::Read, sync::Arc};
use crate::routing::{bfs_rts, construct_graph};
mod controllers;
mod services;
mod session_middleware;
mod templates;
mod routing;
pub struct AppState {
gtfs_service: services::gtfs_pull::GtfsPullService,
@ -58,6 +61,7 @@ async fn main() -> ::anyhow::Result<()> {
.service(controllers::stop::get_stop_table_html)
.service(controllers::index::get_index_html)
.service(controllers::map::get_map_element_html)
.service(routing::get_routing_sample)
.service(actix_files::Files::new("/assets", "./assets"))
})
.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 crate::services;
use crate::{AppState, services, session_middleware::{SessionResponder, SessionResponse}, templates::RoutingTemplate};
pub struct RoutingNodePointer {
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,
all_stops: &HashMap<String, libseptastic::stop::Stop>
pub fn get_platforms_near(cds: Coordinates,
all_platforms: &HashMap<String, Arc<libseptastic::stop::Platform>>
) -> HashSet<String> {
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 {
let stop = stop_p.1;
for platform_p in all_platforms {
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 {
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() {
@ -103,17 +106,17 @@ pub fn get_stop_as_node() {
pub fn construct_graph(
dest: Coordinates,
all_stops: &HashMap<String, libseptastic::stop::Stop>,
all_platforms: &HashMap<String, Arc<libseptastic::stop::Platform>>,
gtfs_service: &services::gtfs_pull::GtfsPullService
) -> RoutingGraph {
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 {
let stop = stop_p.1;
for platform_p in all_platforms {
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 mut ret = false;
@ -130,24 +133,24 @@ pub fn construct_graph(
continue;
}
graph.insert(stop.id.clone(), RoutingNode {
stop_id: stop.id.clone(),
stop_name: stop.name.clone(),
graph.insert(platform.id.clone(), RoutingNode {
stop_id: platform.id.clone(),
stop_name: platform.name.clone(),
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();
for route in &routes {
let mut stops = gtfs_service.get_stops_by_route(&route);
stops.remove(&stop.id);
let mut platforms = gtfs_service.get_platforms_by_route(&route);
platforms.remove(&platform.id);
let rnps = {
let mut ret = BTreeSet::new();
for stop in &stops {
let stp = all_stops.get(stop).unwrap();
for platform in &platforms {
let plt = all_platforms.get(platform).unwrap();
ret.insert(RoutingNodePointer{
dest_dist: haversine_distance(dest.lat, dest.lng, stp.lat, stp.lng),
stop_id: stop.clone(),
dest_dist: haversine_distance(dest.lat, dest.lng, plt.lat, plt.lng),
stop_id: platform.clone(),
route_id: route.clone(),
stop_sequence: 0,
direction: 0
@ -167,23 +170,50 @@ pub fn construct_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 {
return None;
}
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);
}
if !limited_rts.contains(&route_id.as_str()) {
info!("{} is not in the allowlist", route_id.as_str());
return None;
}
info!("made it");
if let Some(origin_stop) = graph.get(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) {
return None;
}
@ -196,8 +226,9 @@ pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, des
}
for rnp in items.1 {
if let Some(rt) = bfs_rts_int(items.0, &rnp.stop_id, graph, dests, visited.clone(), max_legs - 1) {
return Some(format!("[stop {} via rt {}] >>[XFER]>> {}", origin_stop.stop_name, route_id, rt))
if let Some(mut rt) = bfs_rts_int(items.0, &rnp.stop_id, graph, dests, visited.clone(), max_legs - 1).clone() {
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
}
pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> String {
let mut resp = String::new();
pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> Vec<Vec<RouteStep>> {
let mut resp = vec![];
if let Some(origin_stop) = graph.get(origin) {
for items in &origin_stop.next_stops_per_routes {
let route_id = items.0;
for rnp in items.1 {
if let Some(rt) = bfs_rts_int(route_id, &rnp.stop_id, graph, dests, HashSet::new(), 3) {
resp += format!("ORIGIN --> [stop {} via rt {}] --> {}\n", origin_stop.stop_name, route_id, rt).as_str();
if let Some(mut rt) = bfs_rts_int(route_id, &rnp.stop_id, graph, dests, HashSet::new(), 3).clone() {
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
}
#[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
pub route_id_by_stops: 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>>,
}
@ -96,6 +97,7 @@ impl TransitData {
platforms: HashMap::new(),
route_id_by_stops: HashMap::new(),
stops_by_route_id: HashMap::new(),
platforms_by_route_id: HashMap::new(),
stops_by_platform_id: HashMap::new(),
calendar_days: HashMap::new(),
directions: HashMap::new(),
@ -207,6 +209,16 @@ impl GtfsPullService {
.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> {
let l_state = self.state.lock().unwrap();
match l_state.transit_data.stops.get(id) {
@ -532,6 +544,13 @@ impl GtfsPullService {
.or_insert(HashSet::new())
.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 {
arrival_time: i64::from(s.arrival_time?),
stop_sequence: i64::from(s.stop_sequence),

View file

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