add preliminary nta support

This commit is contained in:
Nicholas Orlowsky 2026-01-12 22:46:53 -05:00
parent a7d323056a
commit 1d66553398
No known key found for this signature in database
GPG key ID: A9F3BA4C0AA7A70B
21 changed files with 3318 additions and 257 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/home/nickorlow/programming/septastic/api" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/home/nickorlow/programming/septastic/api")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/home/nickorlow/programming/septastic/api" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/home/nickorlow/programming/septastic/api/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/home/nickorlow/programming/septastic/api/.envrc" "/home/nickorlow/programming/septastic/api/.direnv"/*.rc

View file

@ -0,0 +1 @@
/nix/store/jyg5pzxlxkbvzy1wb808kc5idmbij4r6-env-env

File diff suppressed because it is too large Load diff

18
api/Cargo.lock generated
View file

@ -311,12 +311,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -646,16 +640,16 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link 0.1.3", "windows-link 0.2.1",
] ]
[[package]] [[package]]
@ -1606,7 +1600,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.10", "socket2 0.6.1",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@ -1908,6 +1902,8 @@ dependencies = [
name = "libseptastic" name = "libseptastic"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"chrono-tz",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",

View file

@ -70,7 +70,7 @@ img {
.rr-container { .rr-container {
background-color: #4c748c; background-color: #4c748c;
color: #ffffff; color: #ffffff;
font-size: 1.5em; font-size: 1.2em;
padding: 0.3em; padding: 0.3em;
font-weight: bold; font-weight: bold;
display: flex; display: flex;
@ -81,6 +81,35 @@ img {
line-height: 1; line-height: 1;
} }
.train-direction-table {
width: 100%;
border-collapse: collapse;
font-family: mono;
font-size: 14px;
}
.train-direction-table th,
.train-direction-table td {
border: 1px solid #000;
padding: 4px 8px;
text-align: left;
cursor: pointer;
}
.train-direction-table th {
background-color: #f0f0f0;
font-weight: bold;
}
.highlight-row td,
.highlight-row th {
background-color: #d0ebff !important;
}
.highlight-col {
background-color: #d0ebff !important;
}
.bg-B1, .bg-B2, .bg-B3 { .bg-B1, .bg-B2, .bg-B3 {
background-color: #FF661C; background-color: #FF661C;
color: #ffffff; color: #ffffff;
@ -121,6 +150,8 @@ img {
background-color: #ffffff; background-color: #ffffff;
color: #000000; color: #000000;
width: max-content; width: max-content;
height: max-content;
aspect-ratio: 3/2;
line-height: 1; line-height: 1;
} }
.tscroll { .tscroll {

View file

@ -1,10 +1,38 @@
gtfs_zips: gtfs_zips:
- uri: "https://www3.septa.org/developer/gtfs_public.zip" - uri: "https://www3.septa.org/developer/gtfs_public.zip"
subzip: "google_rail.zip" subzip: "google_rail.zip"
prefix: "SEPTARAIL"
- uri: "https://www3.septa.org/developer/gtfs_public.zip" - uri: "https://www3.septa.org/developer/gtfs_public.zip"
prefix: "SEPTABUS"
subzip: "google_bus.zip" subzip: "google_bus.zip"
# - uri: "https://www.njtransit.com/rail_data.zip" # - uri: "https://www.njtransit.com/rail_data.zip"
# - uri: "https://www.njtransit.com/bus_data.zip" # - uri: "https://www.njtransit.com/bus_data.zip"
annotations: annotations:
multiplatform_stops:
- id: 'WTC'
name: 'Wissahickon Transit Center'
platform_station_ids:
- 'SEPTABUS_2'
- 'SEPTABUS_31032'
- 'SEPTABUS_32980'
- 'SEPTABUS_32988'
- 'SEPTABUS_32989'
- 'SEPTABUS_32990'
- 'SEPTABUS_32992'
- 'SEPTABUS_32993'
- id: 'STC'
name: "Susquehanna Transit Center"
platform_station_ids:
- 'SEPTABUS_740'
- 'SEPTABUS_703'
- 'SEPTABUS_699'
- 'SEPTABUS_22302'
- id: 'CCC'
name: "Center City Combined"
platform_station_ids:
- 'SEPTABUS_3057'
- 'SEPTABUS_2687'
- 'SEPTABUS_18451'
- 'SEPTABUS_17170'
synthetic_routes: synthetic_routes:
- id: 'NYC' - id: 'NYC'

View file

@ -1 +1,2 @@
pub mod route; pub mod route;
pub mod stop;

View file

@ -1,11 +1,12 @@
use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder}; use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder};
use anyhow::anyhow; use anyhow::anyhow;
use log::info;
use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant}; use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant};
use libseptastic::{direction, route::RouteType, stop_schedule::Trip}; use libseptastic::{direction, route::RouteType, stop_schedule::Trip};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::AppState; use crate::AppState;
//use crate::{routing::{bfs_rts, construct_graph, get_stops_near}, AppState};
//use crate::routing;
#[get("/routes")] #[get("/routes")]
async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder { async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
@ -21,7 +22,7 @@ async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl R
let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(); let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect();
Ok(crate::templates::ContentTemplate { Ok(crate::templates::ContentTemplate {
page_title: Some(String::from("SEPTASTIC | Routes")), page_title: Some(String::from("Routes")),
page_desc: Some(String::from("All SEPTA routes.")), page_desc: Some(String::from("All SEPTA routes.")),
widescreen: false, widescreen: false,
content: crate::templates::RoutesTemplate { content: crate::templates::RoutesTemplate {
@ -75,106 +76,37 @@ async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyho
schedule: trips schedule: trips
}) })
} }
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6371.0; // Earth's radius in kilometers
let d_lat = (lat2 - lat1).to_radians(); //#[get("/directions")]
let d_lon = (lon2 - lon1).to_radians(); //async fn get_directions(state: Data<Arc<AppState>>) -> impl Responder {
// let near_thresh = 0.45;
let lat1_rad = lat1.to_radians(); //
let lat2_rad = lat2.to_radians(); // let sig_cds = routing::Coordinates {
// lat: 40.008420,
let a = (d_lat / 2.0).sin().powi(2) // lng: -75.213439
+ lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2); // };
//
let c = 2.0 * a.sqrt().asin(); // let home_cds = routing::Coordinates {
// lat: 39.957210,
r * c // lng: -75.166214
} // };
//
enum RoutingNodeType { // let all_stops = state.gtfs_service.get_all_stops();
Origin, //
Destination, //
Midpoint //
} // let origin_stops: HashSet<String> = get_stops_near(home_cds, &all_stops);
// let dest_stops: HashSet<String> = get_stops_near(sig_cds.clone(), &all_stops);
struct RoutingNodePointer { //
pub stop_id: String, // let mut graph = construct_graph(sig_cds, &all_stops, &state.gtfs_service);
pub route_id: String, //
pub stop_sequence: u64, // let mut response = String::new();
pub direction: u64 // for stop in &origin_stops {
} // response += bfs_rts(&stop, &mut graph, &dest_stops).as_str();
// }
struct RoutingNode { //
pub node_type: RoutingNodeType, // return response;
pub stop_id: String, //}
pub next_stops_per_routes: HashMap<String, HashSet<RoutingNodePointer>>,
pub visited: bool
}
struct TripState {
pub used_lines: HashSet<String>
}
struct Coordinates {
pub lat: f64,
pub lng: f64,
}
#[get("/directions")]
async fn get_directions(state: Data<Arc<AppState>>) -> impl Responder {
let near_thresh = 0.45;
let sig_cds = Coordinates {
lat: 40.008420,
lng: -75.213439
};
let home_cds = Coordinates {
lat: 39.957210,
lng: -75.166214
};
let all_stops = state.gtfs_service.get_all_stops();
let mut response = String::new();
let mut origin_stops: HashSet<String> = HashSet::new();
let mut dest_stops: HashSet<String> = HashSet::new();
let mut graph = HashMap::<String, RoutingNode>::new();
for stop_p in &all_stops {
let stop = stop_p.1;
let dist = haversine_distance(sig_cds.lat, sig_cds.lng, stop.lat, stop.lng);
if dist.abs() < near_thresh {
origin_stops.insert(stop.id.clone());
graph.insert(stop.id.clone(), RoutingNode {
node_type: RoutingNodeType::Origin,
stop_id: stop.id.clone(),
next_stops_per_routes: ,
visited: false
});
}
}
for stop_p in &all_stops {
let stop = stop_p.1;
let dist = haversine_distance(home_cds.lat, home_cds.lng, stop.lat, stop.lng);
if dist.abs() < near_thresh {
dest_stops.insert(stop.id.clone());
}
}
return response;
}
#[get("/route/{route_id}")] #[get("/route/{route_id}")]
async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> impl Responder { async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> impl Responder {
@ -183,12 +115,12 @@ async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Quer
let infox = info.clone(); let infox = info.clone();
let statex = state.clone(); let statex = state.clone();
async move { async move {
let mut filters: Option<Vec<i64>> = None; let mut filters: Option<Vec<String>> = None;
if let Some (stops_v) = infox.stops.clone() { if let Some (stops_v) = infox.stops.clone() {
let mut items = Vec::new(); let mut items = Vec::new();
for sid in stops_v.split(",") { for sid in stops_v.split(",") {
items.push(sid.parse::<i64>().unwrap()); items.push(String::from(sid));
} }
filters = Some(items); filters = Some(items);
} }
@ -201,7 +133,7 @@ async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Quer
Ok(crate::templates::ContentTemplate { Ok(crate::templates::ContentTemplate {
widescreen: false, widescreen: false,
page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())), page_title: Some(format!("Schedules for {}", route_id.clone())),
page_desc: Some(format!("Schedule information for {}", route_id.clone())), page_desc: Some(format!("Schedule information for {}", route_id.clone())),
content: crate::templates::RouteTemplate { content: crate::templates::RouteTemplate {
route: route_info.route, route: route_info.route,

145
api/src/controllers/stop.rs Normal file
View file

@ -0,0 +1,145 @@
use actix_web::{get, web::{self, Data}, HttpRequest, Responder};
use anyhow::anyhow;
use chrono::Timelike;
use chrono_tz::America::New_York;
use libseptastic::stop_schedule::{self, Trip};
use log::info;
use std::{collections::HashSet, sync::Arc, time::Instant};
use crate::{AppState, templates::TripPerspective};
#[get("/stops")]
async fn get_stops_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
crate::perform_action(req, move || {
let statex = state.clone();
async move {
let start_time = Instant::now();
let stops = statex.gtfs_service.get_all_stops().iter().filter_map(|f| {
if f.1.id.contains("ANNOTATED") {
Some(libseptastic::stop::Stop::clone(f.1))
} else {
None
}
}).collect();
Ok(crate::ContentTemplate {
page_title: Some(String::from("Stops")),
page_desc: Some(String::from("Stops")),
widescreen: false,
content: crate::templates::StopsTemplate {
tc_stops: stops
},
load_time_ms: Some(start_time.elapsed().as_millis())
})
}
}).await
}
#[get("/stop/{stop_id}")]
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
crate::perform_action(req, move || {
let statex = state.clone();
let pathx = path.clone();
async move {
let stop_id = pathx;
let start_time = Instant::now();
if let Some(stop) = statex.gtfs_service.get_stop_by_id(&stop_id) {
let routes: Vec<libseptastic::route::Route> = statex.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| {
match statex.gtfs_service.get_route(route.clone()) {
Ok(route) => Some(route),
Err(_) => None
}
}).collect();
let route_ids: HashSet<String> = routes.iter().map(|route| route.id.clone()).collect();
let mut trips = statex.gtfs_service.get_all_trips().iter().filter_map(|trip| {
if route_ids.contains(trip.0) {
Some(trip.1.clone())
} else {
None
}
}).flatten().collect();
statex.trip_tracking_service.annotate_trips(&mut trips).await;
let now_utc = chrono::Utc::now();
let now = now_utc.with_timezone(&New_York);
let naive_time = now.time();
let cur_time = i64::from(naive_time.num_seconds_from_midnight());
let mut filtered_trips: Vec<TripPerspective> = trips.iter().filter_map(|trip| {
//if !trip.is_active_on(&now.naive_local()) {
// return None;
//}
//
// poor midnight handling?
if !trip.calendar_day.is_calendar_active_for_date(&now.naive_local().date()) {
return None
}
let mut est_arrival_time = 0;
let mut is_tracked = false;
let stop_sched : Vec<_> = trip.schedule.iter().filter(|stop_schedule| {
if stop_schedule.stop.id != stop_id {
return false;
}
match &trip.tracking_data {
libseptastic::stop_schedule::TripTracking::Tracked(live) => {
let actual_arrival_time = stop_schedule.arrival_time + (live.delay * 60 as f64) as i64;
est_arrival_time = stop_schedule.arrival_time;
is_tracked = true;
return
(actual_arrival_time - cur_time) > -(1 * 60)
&&
(actual_arrival_time - cur_time) < (60 * 60)
;
},
libseptastic::stop_schedule::TripTracking::Untracked => {
est_arrival_time = stop_schedule.arrival_time;
return
(stop_schedule.arrival_time - cur_time) > -(3 * 60)
&&
(stop_schedule.arrival_time - cur_time) < (60 * 60)
;
},
libseptastic::stop_schedule::TripTracking::Cancelled => {
return false;
}
}
}).filter_map(|ss| Some(ss.clone())).collect();
if stop_sched.len() > 0 {
Some(TripPerspective {
est_arrival_time,
is_tracked,
perspective_stop: stop_sched.first().unwrap().clone(),
trip: trip.clone()
})
} else {
None
}
}).collect();
filtered_trips.sort_by_key(|f| f.perspective_stop.arrival_time);
Ok(crate::templates::ContentTemplate {
page_title: Some(stop.name.clone()),
page_desc: Some(String::from("Stop information")),
widescreen: false,
content: crate::templates::StopTemplate {
stop: stop.clone(),
routes,
trips: filtered_trips,
current_time: cur_time
},
load_time_ms: Some(start_time.elapsed().as_millis())
})
}
else {
Err(anyhow!("Stop not found!"))
}
}
}).await
}

View file

@ -11,6 +11,7 @@ use askama::Template;
mod services; mod services;
mod controllers; mod controllers;
mod templates; mod templates;
//mod routing;
pub struct AppState { pub struct AppState {
gtfs_service: services::gtfs_pull::GtfsPullService, gtfs_service: services::gtfs_pull::GtfsPullService,
@ -51,7 +52,8 @@ where T: Template,
.cookie(cookie) .cookie(cookie)
.body(y.render().unwrap()) .body(y.render().unwrap())
}, },
Err(_) => { Err(err) => {
error!("Returned error b/c {:?}", err);
HttpResponse::InternalServerError().body("Error") HttpResponse::InternalServerError().body("Error")
} }
} }
@ -105,7 +107,9 @@ async fn main() -> ::anyhow::Result<()> {
.service(controllers::route::get_route) .service(controllers::route::get_route)
.service(controllers::route::get_routes_json) .service(controllers::route::get_routes_json)
.service(controllers::route::get_routes_html) .service(controllers::route::get_routes_html)
.service(controllers::route::get_directions) // .service(controllers::route::get_directions)
.service(controllers::stop::get_stops_html)
.service(controllers::stop::get_stop_html)
.service(get_index) .service(get_index)
.service(actix_files::Files::new("/assets", "./assets")) .service(actix_files::Files::new("/assets", "./assets"))
}) })

225
api/src/routing.rs Normal file
View file

@ -0,0 +1,225 @@
use std::{cmp::Ordering, collections::{BTreeSet, HashMap, HashSet}};
use log::info;
use crate::services;
pub struct RoutingNodePointer {
pub stop_id: String,
pub route_id: String,
pub stop_sequence: u64,
pub direction: u64,
pub dest_dist: f64
}
pub struct RoutingNode {
pub stop_id: String,
pub stop_name: String,
pub next_stops_per_routes: HashMap<String, BTreeSet<RoutingNodePointer>>,
pub visited: bool,
pub scratch: i64,
}
impl Ord for RoutingNodePointer {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.dest_dist > other.dest_dist {
Ordering::Greater
} else {
Ordering::Less
}
}
}
impl PartialOrd for RoutingNodePointer {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.dest_dist > other.dest_dist {
Some(Ordering::Greater)
} else {
Some(Ordering::Less)
}
}
}
impl PartialEq for RoutingNodePointer {
fn eq(&self, other: &Self) -> bool {
self.stop_id == other.stop_id
}
}
impl Eq for RoutingNodePointer {
}
struct TripState {
pub used_lines: HashSet<String>
}
#[derive(Clone)]
pub struct Coordinates {
pub lat: f64,
pub lng: f64,
}
pub type RoutingGraph = HashMap::<String, RoutingNode>;
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6371.0; // Earth's radius in kilometers
let d_lat = (lat2 - lat1).to_radians();
let d_lon = (lon2 - lon1).to_radians();
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let a = (d_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
r * c
}
pub fn get_stops_near(cds: Coordinates,
all_stops: &HashMap<String, libseptastic::stop::Stop>
) -> HashSet<String> {
let near_thresh_km = 0.45;
let mut stops: HashSet<String> = HashSet::new();
for stop_p in all_stops {
let stop = stop_p.1;
let dist = haversine_distance(cds.lat, cds.lng, stop.lat, stop.lng);
if dist.abs() < near_thresh_km {
stops.insert(stop.id.clone());
}
}
stops
}
pub fn get_stop_as_node() {
}
pub fn construct_graph(
dest: Coordinates,
all_stops: &HashMap<String, libseptastic::stop::Stop>,
gtfs_service: &services::gtfs_pull::GtfsPullService
) -> RoutingGraph {
let mut graph = RoutingGraph::new();
let limited_rts = vec!["44", "65", "27", "38", "124", "125", "1"];
for stop_p in all_stops {
let stop = stop_p.1;
let ras = gtfs_service.get_routes_at_stop(&stop.id);
let cont = {
let mut ret = false;
for l_rt in limited_rts.clone() {
if ras.contains(&String::from(l_rt)) {
ret = true;
break;
}
}
ret
};
if !cont {
continue;
}
graph.insert(stop.id.clone(), RoutingNode {
stop_id: stop.id.clone(),
stop_name: stop.name.clone(),
next_stops_per_routes: {
let routes = gtfs_service.get_routes_at_stop(&stop.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 rnps = {
let mut ret = BTreeSet::new();
for stop in &stops {
let stp = all_stops.get(stop).unwrap();
ret.insert(RoutingNodePointer{
dest_dist: haversine_distance(dest.lat, dest.lng, stp.lat, stp.lng),
stop_id: stop.clone(),
route_id: route.clone(),
stop_sequence: 0,
direction: 0
});
}
ret
};
other_stops.insert(route.clone(), rnps);
}
other_stops
},
visited: false,
scratch: 0
});
}
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> {
if max_legs == 0 {
return None;
}
let mut limited_rts = HashSet::new();
for item in vec!["44", "65", "27", "38", "124", "125", "1"] {
limited_rts.insert(item);
}
if !limited_rts.contains(&route_id.as_str()) {
return None;
}
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))
}
if visited.contains(origin) {
return None;
}
visited.insert(origin.clone());
for items in &origin_stop.next_stops_per_routes {
if route_id == items.0 {
continue;
}
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))
}
}
}
}
None
}
pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> String {
let mut resp = String::new();
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();
}
}
}
}
resp
}

View file

@ -1,19 +1,39 @@
use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex}, thread, time::Duration}; use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration};
use anyhow::anyhow; use anyhow::anyhow;
use libseptastic::{stop::Platform, stop_schedule::CalendarDay};
use log::{info, error}; use log::{info, error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zip::ZipArchive; use zip::ZipArchive;
macro_rules! make_global_id {
($prefix: expr, $id: expr) => (format!("{}_{}", $prefix, $id))
}
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
struct GtfsSource { struct GtfsSource {
pub uri: String, pub uri: String,
pub subzip: Option<String> pub subzip: Option<String>,
pub prefix: String
}
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
struct MultiplatformStopConfig {
pub id: String,
pub name: String,
pub platform_station_ids: Vec<String>
}
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
struct Annotations {
pub multiplatform_stops: Vec<MultiplatformStopConfig>
} }
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct Config { pub struct Config {
pub gtfs_zips: Vec<GtfsSource> pub gtfs_zips: Vec<GtfsSource>,
pub annotations: Annotations
} }
#[derive(Clone)] #[derive(Clone)]
@ -23,17 +43,24 @@ struct GtfsFile {
} }
struct TransitData { struct TransitData {
pub routes: HashMap<String, libseptastic::route::Route>, pub routes: HashMap<String, Arc<libseptastic::route::Route>>,
pub agencies: HashMap<String, libseptastic::agency::Agency>, pub agencies: HashMap<String, libseptastic::agency::Agency>,
pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>, pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
pub stops: HashMap<String, libseptastic::stop::Stop>, pub stops: HashMap<String, Arc<libseptastic::stop::Stop>>,
pub route_id_by_stops: HashMap<String, HashSet<String>> pub platforms: HashMap<String, Arc<libseptastic::stop::Platform>>,
pub calendar_days: HashMap<String, Arc<libseptastic::stop_schedule::CalendarDay>>,
// extended lookup methods
pub route_id_by_stops: HashMap<String, HashSet<String>>,
pub stops_by_route_id: HashMap<String, HashSet<String>>,
pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>
} }
struct GtfsPullServiceState { struct GtfsPullServiceState {
pub gtfs_files: Vec<GtfsFile>, pub gtfs_files: Vec<GtfsFile>,
pub tmp_dir: PathBuf, pub tmp_dir: PathBuf,
pub ready: bool, pub ready: bool,
pub annotations: Annotations,
pub transit_data: TransitData pub transit_data: TransitData
} }
@ -43,12 +70,13 @@ pub struct GtfsPullService {
impl TransitData { impl TransitData {
pub fn new() -> Self { pub fn new() -> Self {
return TransitData { routes: HashMap::new(), agencies: HashMap::new(), trips: HashMap::new(), stops: HashMap::new(), route_id_by_stops: HashMap::new() } return TransitData { routes: HashMap::new(), agencies: HashMap::new(), trips: HashMap::new(), stops: HashMap::new(), platforms: HashMap::new(), route_id_by_stops: HashMap::new(), stops_by_route_id: HashMap::new(), stops_by_platform_id: HashMap::new(), calendar_days: HashMap::new() }
} }
} }
impl GtfsPullService { impl GtfsPullService {
const UPDATE_SECONDS: u64 = 3600*24; const UPDATE_SECONDS: u64 = 3600*24;
const READYSTATE_CHECK_MILLISECONDS: u64 = 500;
pub fn new(config: Config) -> Self { pub fn new(config: Config) -> Self {
Self { Self {
@ -56,6 +84,7 @@ impl GtfsPullService {
GtfsPullServiceState { GtfsPullServiceState {
gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(), gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(),
tmp_dir: env::temp_dir(), tmp_dir: env::temp_dir(),
annotations: config.annotations.clone(),
ready: false, ready: false,
transit_data: TransitData::new() transit_data: TransitData::new()
} }
@ -65,7 +94,9 @@ impl GtfsPullService {
pub fn wait_for_ready(&self) { pub fn wait_for_ready(&self) {
while !(self.state.lock().unwrap()).ready { while !(self.state.lock().unwrap()).ready {
thread::sleep(Duration::from_millis(500)); thread::sleep(
Duration::from_millis(Self::READYSTATE_CHECK_MILLISECONDS)
);
} }
} }
@ -90,13 +121,13 @@ impl GtfsPullService {
pub fn get_routes(&self) -> Vec<libseptastic::route::Route> { pub fn get_routes(&self) -> Vec<libseptastic::route::Route> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
l_state.transit_data.routes.iter().map(|r| r.1.clone()).collect() l_state.transit_data.routes.iter().map(|r| libseptastic::route::Route::clone(r.1)).collect()
} }
pub fn get_route(&self, route_id: String) -> anyhow::Result<libseptastic::route::Route> { pub fn get_route(&self, route_id: String) -> anyhow::Result<libseptastic::route::Route> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
if let Some(route) = l_state.transit_data.routes.get(&route_id) { if let Some(route) = l_state.transit_data.routes.get(&route_id) {
Ok(route.clone()) Ok(libseptastic::route::Route::clone(route))
} else { } else {
Err(anyhow!("")) Err(anyhow!(""))
} }
@ -104,10 +135,10 @@ impl GtfsPullService {
pub fn get_all_routes(&self) -> HashMap<String, libseptastic::route::Route> { pub fn get_all_routes(&self) -> HashMap<String, libseptastic::route::Route> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
l_state.transit_data.routes.clone() l_state.transit_data.routes.iter().map(|r| (r.0.clone(), libseptastic::route::Route::clone(r.1))).collect()
} }
pub fn get_all_stops(&self) -> HashMap<String, libseptastic::stop::Stop> { pub fn get_all_stops(&self) -> HashMap<String, Arc<libseptastic::stop::Stop>> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
l_state.transit_data.stops.clone() l_state.transit_data.stops.clone()
} }
@ -117,9 +148,22 @@ impl GtfsPullService {
l_state.transit_data.trips.clone() l_state.transit_data.trips.clone()
} }
pub fn get_routes_at_stop(&self, id: String) -> HashSet<String> { pub fn get_routes_at_stop(&self, id: &String) -> HashSet<String> {
let l_state = self.state.lock().unwrap(); let l_state = self.state.lock().unwrap();
l_state.transit_data.route_id_by_stops.get(&id).unwrap_or(&HashSet::new()).clone() l_state.transit_data.route_id_by_stops.get(id).unwrap_or(&HashSet::new()).clone()
}
pub fn get_stops_by_route(&self, id: &String) -> HashSet<String> {
let l_state = self.state.lock().unwrap();
l_state.transit_data.stops_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) {
Some(stop) => Some(libseptastic::stop::Stop::clone(stop)),
None => None
}
} }
pub fn get_schedule(&self, route_id: String) -> anyhow::Result<Vec<libseptastic::stop_schedule::Trip>> { pub fn get_schedule(&self, route_id: String) -> anyhow::Result<Vec<libseptastic::stop_schedule::Trip>> {
@ -131,9 +175,155 @@ impl GtfsPullService {
} }
} }
fn postprocess_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>) -> anyhow::Result<()> {
for annotated_stop in state.annotations.multiplatform_stops.clone() {
let global_id = make_global_id!("ANNOTATED", annotated_stop.id.clone());
let stop = Arc::new(libseptastic::stop::Stop {
id: global_id.clone(),
name: annotated_stop.name.clone(),
platforms: libseptastic::stop::StopType::MultiPlatform(annotated_stop.platform_station_ids.iter().map(|platform_id| {
info!("Folding {} stop into stop {} as platform", platform_id.clone(), annotated_stop.id.clone());
let platform = match state.transit_data.stops.remove(platform_id).unwrap().platforms.clone() {
libseptastic::stop::StopType::SinglePlatform(plat) => Ok(plat),
_ => Err(anyhow!(""))
}.unwrap();
state.transit_data.stops_by_platform_id.remove(&platform.id).unwrap();
platform
}).collect())
});
state.transit_data.stops.insert(global_id.clone(), stop.clone());
match &stop.platforms {
libseptastic::stop::StopType::MultiPlatform(platforms) => {
for platform in platforms {
state.transit_data.stops_by_platform_id.insert(platform.id.clone(), stop.clone());
}
Ok(())
},
_ => Err(anyhow!(""))
}?
}
Ok(())
}
fn populate_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: &gtfs_structures::Gtfs) -> anyhow::Result<()> {
for stop in &gtfs.stops {
let global_id = make_global_id!(prefix, stop.1.id.clone());
let platform = Arc::new(Platform {
id : global_id.clone(),
name: stop.1.name.clone().unwrap(),
lat: stop.1.latitude.unwrap(),
lng: stop.1.longitude.unwrap(),
platform_location: libseptastic::stop::PlatformLocationType::Normal
});
let stop = Arc::new(libseptastic::stop::Stop {
id: global_id.clone(),
name: stop.1.name.clone().unwrap(),
platforms: libseptastic::stop::StopType::SinglePlatform(platform.clone())
});
state.transit_data.stops.insert(global_id.clone(), stop.clone());
state.transit_data.platforms.insert(global_id.clone(), platform.clone());
state.transit_data.stops_by_platform_id.insert(global_id.clone(), stop.clone());
}
Ok(())
}
fn populate_routes(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: &gtfs_structures::Gtfs) -> anyhow::Result<()> {
for route in &gtfs.routes {
let global_rt_id = make_global_id!(prefix, route.1.id);
let rt_name = match route.1.long_name.clone() {
Some(x) => x,
_ => String::from("Unknown")
};
state.transit_data.routes.insert(global_rt_id.clone(), Arc::new(libseptastic::route::Route{
name: rt_name,
short_name: match route.1.short_name.clone() {
Some(x) => x,
_ => String::from("unknown")
},
color_hex: match route.1.color{
Some(x) => x.to_string(),
_ => String::from("unknown")
},
id: global_rt_id,
route_type: match route.1.route_type {
gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus,
gtfs_structures::RouteType::Rail => libseptastic::route::RouteType::RegionalRail,
gtfs_structures::RouteType::Subway => libseptastic::route::RouteType::SubwayElevated,
gtfs_structures::RouteType::Tramway => libseptastic::route::RouteType::Trolley,
_ => libseptastic::route::RouteType::TracklessTrolley
}
}));
}
Ok(())
}
fn populate_trips(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: &gtfs_structures::Gtfs) -> anyhow::Result<()> {
for trip in &gtfs.trips {
let global_rt_id = make_global_id!(prefix, trip.1.route_id);
let sched = trip.1.stop_times.iter().map(|s| {
let global_stop_id = make_global_id!(prefix, s.stop.id);
let stop = state.transit_data.stops_by_platform_id.get(&global_stop_id).unwrap().clone();
let platform = state.transit_data.platforms.get(&global_stop_id).unwrap().clone();
state.transit_data.route_id_by_stops.entry(stop.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone());
state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(stop.id.clone());
state.transit_data.route_id_by_stops.entry(platform.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone());
state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(platform.id.clone());
libseptastic::stop_schedule::StopSchedule{
arrival_time: i64::from(s.arrival_time.unwrap()),
stop_sequence: i64::from(s.stop_sequence),
stop,
platform
}
}).collect();
if let Some(calendar_day) = state.transit_data.calendar_days.get(&trip.1.service_id.clone()) {
let trip = libseptastic::stop_schedule::Trip{
trip_id: trip.1.id.clone(),
route: state.transit_data.routes.get(&make_global_id!(prefix, trip.1.route_id)).unwrap().clone(),
direction: libseptastic::direction::Direction {
direction: match trip.1.direction_id.unwrap() {
gtfs_structures::DirectionType::Outbound => libseptastic::direction::CardinalDirection::Outbound,
gtfs_structures::DirectionType::Inbound => libseptastic::direction::CardinalDirection::Inbound
},
direction_destination: trip.1.trip_headsign.clone().unwrap()
},
tracking_data: libseptastic::stop_schedule::TripTracking::Untracked,
schedule: sched,
service_id: trip.1.service_id.clone(),
calendar_day: calendar_day.clone()
};
if let Some(trip_arr) = state.transit_data.trips.get_mut(&global_rt_id) {
trip_arr.push(trip);
} else {
state.transit_data.trips.insert(global_rt_id, vec![trip]);
}
}
}
Ok(())
}
pub fn update_gtfs_data(state: Arc<Mutex<GtfsPullServiceState>>) -> anyhow::Result<()> { pub fn update_gtfs_data(state: Arc<Mutex<GtfsPullServiceState>>) -> anyhow::Result<()> {
let mut l_state = state.lock().unwrap(); let mut l_state = state.lock().unwrap();
let files = l_state.gtfs_files.clone(); let files = l_state.gtfs_files.clone();
l_state.transit_data = TransitData::new();
let mut gtfses = Vec::new();
for gtfs_file in files.iter() { for gtfs_file in files.iter() {
let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() { let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() {
@ -146,121 +336,43 @@ impl GtfsPullService {
let mut file_path = l_state.tmp_dir.clone(); let mut file_path = l_state.tmp_dir.clone();
file_path.push(subzip.clone()); file_path.push(subzip.clone());
info!("Downloaded, parsing");
gtfs_structures::Gtfs::new(file_path.to_str().unwrap())? gtfs_structures::Gtfs::new(file_path.to_str().unwrap())?
} else { } else {
info!("Reading GTFS file at {}", gtfs_file.source.uri); info!("Reading GTFS file at {}", gtfs_file.source.uri);
gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())? gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())?
}; };
let mut hack_agency = None; gtfses.push((gtfs, gtfs_file.source.prefix.clone()));
}
for agency in &gtfs.agencies {
if let Some(a_id) = &agency.id { info!("Data loaded, processing...");
l_state.transit_data.agencies.insert(a_id.clone(), libseptastic::agency::Agency{
id: a_id.clone(), for (gtfs, prefix) in &gtfses {
name: agency.name.clone() GtfsPullService::populate_routes(&mut l_state, &prefix, &gtfs)?;
}); GtfsPullService::populate_stops(&mut l_state, &prefix, &gtfs)?;
hack_agency = Some(libseptastic::agency::Agency{ for calendar in &gtfs.calendar {
id: a_id.clone(), l_state.transit_data.calendar_days.insert(calendar.1.id.clone(), Arc::new(CalendarDay{
name: agency.name.clone() id: calendar.1.id.clone(),
}); monday: calendar.1.monday,
} tuesday: calendar.1.tuesday,
wednesday: calendar.1.wednesday,
thursday: calendar.1.thursday,
friday: calendar.1.friday,
saturday: calendar.1.saturday,
sunday: calendar.1.sunday,
start_date: calendar.1.start_date,
end_date: calendar.1.end_date
}));
} }
}
for route in &gtfs.routes { GtfsPullService::postprocess_stops(&mut l_state)?;
let agency = route.1.agency_id.as_ref()
.and_then(|agency_id| l_state.transit_data.agencies.get(agency_id))
.map(|agency| agency.clone());
let global_rt_id = match &agency { for (gtfs, prefix) in &gtfses {
Some(a) => format!("{}_{}", a.id, route.0.clone()), GtfsPullService::populate_trips(&mut l_state, &prefix, &gtfs)?;
None => format!("{}", route.0.clone())
};
let rt_name = match route.1.long_name.clone() {
Some(x) => x,
_ => match route.1.short_name.clone() {
Some(y) => match agency {
Some(z) => format!("{} {}", z.name, y),
None => y
},
None => String::from("unknown")
}
};
l_state.transit_data.routes.insert(global_rt_id.clone(), libseptastic::route::Route{
name: rt_name,
short_name: match route.1.short_name.clone() {
Some(x) => x,
_ => String::from("unknown")
},
color_hex: match route.1.color{
Some(x) => x.to_string(),
_ => String::from("unknown")
},
id: global_rt_id,
route_type: match route.1.route_type {
gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus,
gtfs_structures::RouteType::Rail => libseptastic::route::RouteType::RegionalRail,
gtfs_structures::RouteType::Subway => libseptastic::route::RouteType::SubwayElevated,
gtfs_structures::RouteType::Tramway => libseptastic::route::RouteType::Trolley,
_ => libseptastic::route::RouteType::TracklessTrolley
}
});
}
for stop in &gtfs.stops {
l_state.transit_data.stops.insert(stop.1.id.clone(), libseptastic::stop::Stop {
id: stop.1.id.clone(),
name: stop.1.name.clone().unwrap(),
lat: stop.1.latitude.unwrap(),
lng: stop.1.longitude.unwrap(),
stop_type: libseptastic::stop::StopType::Normal
});
}
for trip in &gtfs.trips {
let global_rt_id = match &hack_agency {
Some(a) => format!("{}_{}", a.id, trip.1.route_id.clone()),
None => format!("{}", trip.1.route_id.clone())
};
let sched = trip.1.stop_times.iter().map(|s| {
l_state.transit_data.route_id_by_stops.entry(s.stop.id.clone()).or_insert(HashSet::new()).insert(trip.1.route_id.clone());
libseptastic::stop_schedule::StopSchedule{
arrival_time: i64::from(s.arrival_time.unwrap()),
stop_sequence: i64::from(s.stop_sequence),
stop: libseptastic::stop::Stop {
name: s.stop.name.clone().unwrap(),
lat: s.stop.latitude.unwrap(),
lng: s.stop.longitude.unwrap(),
id: s.stop.id.parse().unwrap(),
stop_type: libseptastic::stop::StopType::Normal
}}
}).collect();
let trip = libseptastic::stop_schedule::Trip{
trip_id: trip.1.id.clone(),
direction: libseptastic::direction::Direction {
direction: match trip.1.direction_id.unwrap() {
gtfs_structures::DirectionType::Outbound => libseptastic::direction::CardinalDirection::Outbound,
gtfs_structures::DirectionType::Inbound => libseptastic::direction::CardinalDirection::Inbound
},
direction_destination: trip.1.trip_headsign.clone().unwrap()
},
tracking_data: libseptastic::stop_schedule::TripTracking::Untracked,
schedule: sched,
service_id: trip.1.service_id.clone()
};
if let Some(trip_arr) = l_state.transit_data.trips.get_mut(&global_rt_id) {
trip_arr.push(trip);
} else {
l_state.transit_data.trips.insert(global_rt_id, vec![trip]);
}
}
info!("Added {} routes", gtfs.routes.len());
} }
l_state.ready = true; l_state.ready = true;

View file

@ -20,7 +20,7 @@ pub struct ContentTemplate<T: askama::Template> {
pub struct RouteTemplate { pub struct RouteTemplate {
pub route: libseptastic::route::Route, pub route: libseptastic::route::Route,
pub timetables: Vec<TimetableDirection>, pub timetables: Vec<TimetableDirection>,
pub filter_stops: Option<Vec<i64>> pub filter_stops: Option<Vec<String>>
} }
#[derive(askama::Template)] #[derive(askama::Template)]
@ -32,6 +32,12 @@ pub struct RoutesTemplate {
pub bus_routes: Vec<libseptastic::route::Route> pub bus_routes: Vec<libseptastic::route::Route>
} }
#[derive(askama::Template)]
#[template(path = "stops.html")]
pub struct StopsTemplate {
pub tc_stops: Vec<libseptastic::stop::Stop>,
}
#[derive(askama::Template)] #[derive(askama::Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
pub struct IndexTemplate { pub struct IndexTemplate {
@ -39,7 +45,7 @@ pub struct IndexTemplate {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct TimetableStopRow { pub struct TimetableStopRow {
pub stop_id: i64, pub stop_id: String,
pub stop_name: String, pub stop_name: String,
pub stop_sequence: i64, pub stop_sequence: i64,
pub times: Vec<Option<i64>> pub times: Vec<Option<i64>>
@ -55,6 +61,21 @@ pub struct TimetableDirection {
pub next_id: Option<String> pub next_id: Option<String>
} }
pub struct TripPerspective {
pub trip:libseptastic::stop_schedule::Trip,
pub perspective_stop: libseptastic::stop_schedule::StopSchedule,
pub est_arrival_time: i64,
pub is_tracked: bool
}
#[derive(askama::Template)]
#[template(path = "stop.html")]
pub struct StopTemplate {
pub stop: libseptastic::stop::Stop,
pub routes: Vec<libseptastic::route::Route>,
pub trips: Vec<TripPerspective>,
pub current_time: i64
}
pub fn build_timetables( pub fn build_timetables(
directions: Vec<Direction>, directions: Vec<Direction>,
@ -120,7 +141,7 @@ pub fn build_timetables(
let mut rows: Vec<TimetableStopRow> = stop_map let mut rows: Vec<TimetableStopRow> = stop_map
.into_iter() .into_iter()
.map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow { .map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
stop_id: stop_id.parse().unwrap(), stop_id,
stop_sequence, stop_sequence,
stop_name, stop_name,
times, times,

View file

@ -3,7 +3,7 @@
<head> <head>
{% if let Some(title) = page_title %} {% if let Some(title) = page_title %}
<title>{{ title }}</title> <title>{{ title }} | SEPTASTIC</title>
{% else %} {% else %}
<title>SEPTASTIC</title> <title>SEPTASTIC</title>
{% endif %} {% endif %}
@ -58,6 +58,7 @@ window.onload = function () {
<div> <div>
<a class="nav-link" href="/">[ Home ]</a> <a class="nav-link" href="/">[ Home ]</a>
<a class="nav-link" href="/routes">[ Routes ]</a> <a class="nav-link" href="/routes">[ Routes ]</a>
<a class="nav-link" href="/stops">[ Stops ]</a>
</div> </div>
<div> <div>
</div> </div>

View file

@ -3,7 +3,7 @@
.train-direction-table { .train-direction-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-family: sans-serif; font-family: mono;
font-size: 14px; font-size: 14px;
} }
@ -148,11 +148,7 @@ document.addEventListener("DOMContentLoaded", () => {
{% let live_o = timetable.tracking_data[loop.index0] %} {% let live_o = timetable.tracking_data[loop.index0] %}
{% if let Tracked(live) = live_o %} {% if let Tracked(live) = live_o %}
{% let time = (t + (live.delay * 60.0) as i64) %} {% let time = (t + (live.delay * 60.0) as i64) %}
{% if live.next_stop_id == Some(*row.stop_id) %}
<td style="background-color: #007700">
{% else %}
<td style="background-color: #003300"> <td style="background-color: #003300">
{% endif %}
<span style="color: #22bb22"> {{ time | format_time }} </span> <span style="color: #22bb22"> {{ time | format_time }} </span>
</td> </td>
{% elif let TripTracking::Cancelled = live_o %} {% elif let TripTracking::Cancelled = live_o %}

71
api/templates/stop.html Normal file
View file

@ -0,0 +1,71 @@
{%- import "route_symbol.html" as scope -%}
<style>
.trip-desc {
display: flex;
justify-content: start;
}
</style>
<div style="display: flex; align-items: center;">
<h1>{{ stop.name }}</h1>
</div>
<p>With service available on:</p>
<div style="display: flex; justify-content: start; padding-top: 5px; padding-bottom: 5px; flex-wrap: wrap; gap: 5px;">
{% for route in routes %}
<div style="margin-right: 5px">
{% call scope::route_symbol(route) %}
</div>
{% endfor %}
</div>
{#{% if let libseptastic::stop::StopType::MultiPlatform(platforms) = stop.platforms %}
<div>
<p>Platforms at this station:</p>
{% for platform in platforms %}
<p><a href="/stop/{{ platform.id }}">{{ platform.name }}</a></p>
{% endfor %}
</div>
{% endif %}#}
<table class="train-direction-table">
<tr>
<th>ROUTE</th>
<th>DESTINATION</th>
<th>BOARDING AREA</th>
<th>TIME</th>
<th>VEHICLE</th>
</tr>
{% for trip in trips %}
<tr>
<td>
{% call scope::route_symbol(trip.trip.route) %}
</td>
<td>
<p>{{ trip.trip.direction.direction_destination }}</p>
</td>
<td>
<p>{{ trip.perspective_stop.platform.name }}</p>
</td>
{% if trip.is_tracked %}
<td style="color: #008800">
{% else %}
<td>
{% endif %}
<p style="font-size: small;">{{ trip.perspective_stop.arrival_time | format_time }}</p>
<p style="font-size: x-small; font-style: italic;">{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins</p>
</td>
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
<td>
{{ tracked_trip.vehicle_ids.join(", ") }}
</td>
{% else %}
<td>
-
</td>
{% endif %}
</tr>
{% endfor %}
</table>

27
api/templates/stops.html Normal file
View file

@ -0,0 +1,27 @@
<h1>Stops</h1>
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
<fieldset>
<legend><h2>Transit Centers</h2></legend>
<p style="margin-top: 10px; margin-bottom: 10px;">Hubs to connect between different modes of transit</p>
{% for stop in tc_stops %}
<a href="/stop/{{ stop.id }}" style="display: flex; justify-content: space-between;">
<p class="line-link">[ {{ stop.name }} </p><p>]</p>
</a>
{% endfor %}
</fieldset>
<style>
.line-link, .lines-label {
white-space: pre;
margin-top: 3px;
margin-bottom: 3px;
}
.lines-label {
color: #ffffff;
background-color: #000000;
width: max-content;
}
</style>

237
libseptastic/Cargo.lock generated
View file

@ -8,6 +8,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -53,6 +62,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -65,12 +80,46 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
dependencies = [
"chrono",
"phf",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -86,6 +135,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -212,6 +267,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.1" version = "0.11.1"
@ -390,6 +451,30 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.0.0" version = "2.0.0"
@ -513,6 +598,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -538,6 +633,8 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
name = "libseptastic" name = "libseptastic"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"chrono-tz",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
@ -688,6 +785,24 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -822,6 +937,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -900,6 +1021,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signature" name = "signature"
version = "2.2.0" version = "2.2.0"
@ -910,6 +1037,12 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.10" version = "0.4.10"
@ -1325,6 +1458,51 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.6.0" version = "1.6.0"
@ -1335,6 +1513,65 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
chrono = { version = "0.4.42", features = [ "serde" ] }
chrono-tz = "0.10.4"
serde = "1.0.219" serde = "1.0.219"
serde_json = "1.0.140" serde_json = "1.0.140"
sqlx = "0.8.6" sqlx = "0.8.6"

View file

@ -1,18 +1,41 @@
use std::{hash::{Hash, Hasher}, sync::Arc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(sqlx::Type, PartialEq, Debug, Clone, Serialize, Deserialize)] #[derive(sqlx::Type, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
#[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")] #[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")]
pub enum StopType { pub enum PlatformLocationType {
FarSide, FarSide,
MiddleBlockNearSide, MiddleBlockNearSide,
Normal Normal
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Stop { pub enum StopType {
SinglePlatform(Arc<Platform>),
MultiPlatform(Vec<Arc<Platform>>)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Platform {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub lat: f64, pub lat: f64,
pub lng: f64, pub lng: f64,
pub stop_type: StopType pub platform_location: PlatformLocationType
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Stop {
pub id: String,
pub name: String,
pub platforms: StopType,
}
impl Hash for Stop {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
impl Eq for Stop {}

View file

@ -1,21 +1,51 @@
use std::sync::Arc;
use chrono::{Datelike, Days, TimeZone, Weekday};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::direction::Direction; use crate::{direction::Direction, route::Route, stop::Platform};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopSchedule { pub struct StopSchedule {
pub arrival_time: i64, pub arrival_time: i64,
pub stop_sequence: i64, pub stop_sequence: i64,
pub stop: crate::stop::Stop pub stop: Arc<crate::stop::Stop>,
pub platform: Arc<Platform>
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trip { pub struct Trip {
pub service_id: String, pub service_id: String,
pub route: Arc<Route>,
pub trip_id: String, pub trip_id: String,
pub direction: Direction, pub direction: Direction,
pub tracking_data: TripTracking, pub tracking_data: TripTracking,
pub schedule: Vec<StopSchedule> pub schedule: Vec<StopSchedule>,
pub calendar_day: Arc<CalendarDay>
}
impl Trip {
pub fn is_active_on(&self, datetime: &chrono::NaiveDateTime) -> bool {
if !self.calendar_day.is_calendar_active_for_date(&datetime.date()) {
return false;
}
let time_trip_start = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.first().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap();
let mut dt_trip_start = chrono::NaiveDateTime::new(datetime.date(), time_trip_start);
if self.schedule.first().unwrap().arrival_time > (60*60*24) {
dt_trip_start = dt_trip_start.checked_add_days(Days::new(1)).unwrap();
}
let time_trip_end = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.last().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap();
let mut dt_trip_end = chrono::NaiveDateTime::new(datetime.date(), time_trip_end);
if self.schedule.last().unwrap().arrival_time > (60*60*24) {
dt_trip_end = dt_trip_end.checked_add_days(Days::new(1)).unwrap();
}
return *datetime >= dt_trip_start && *datetime <= dt_trip_end;
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -25,6 +55,38 @@ pub enum TripTracking {
Cancelled Cancelled
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarDay {
pub id: String,
pub monday: bool,
pub tuesday: bool,
pub wednesday: bool,
pub thursday: bool,
pub friday: bool,
pub saturday: bool,
pub sunday: bool,
pub start_date: chrono::NaiveDate,
pub end_date: chrono::NaiveDate
}
impl CalendarDay {
pub fn is_calendar_active_for_date(&self, date: &chrono::NaiveDate) -> bool {
if *date < self.start_date || *date > self.end_date {
return false;
}
match date.weekday() {
Weekday::Mon => self.monday,
Weekday::Tue => self.tuesday,
Weekday::Wed => self.wednesday,
Weekday::Thu => self.thursday,
Weekday::Fri => self.friday,
Weekday::Sat => self.saturday,
Weekday::Sun => self.sunday,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiveTrip { pub struct LiveTrip {
pub delay: f64, pub delay: f64,