From 6773e6ae30748ab6a2b171b0a69c4fc99e791671 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Wed, 14 Jan 2026 23:18:35 -0500 Subject: [PATCH] add live updating table --- api/src/controllers/stop.rs | 190 ++++++++++++++++++----------- api/src/main.rs | 1 + api/src/templates.rs | 34 +++++- api/templates/layout.html | 1 + api/templates/stop.html | 54 +------- api/templates/stop_table.html | 51 ++++++++ api/templates/stop_table_impl.html | 3 + 7 files changed, 210 insertions(+), 124 deletions(-) create mode 100644 api/templates/stop_table.html create mode 100644 api/templates/stop_table_impl.html diff --git a/api/src/controllers/stop.rs b/api/src/controllers/stop.rs index 37e97a9..4592098 100644 --- a/api/src/controllers/stop.rs +++ b/api/src/controllers/stop.rs @@ -1,6 +1,7 @@ -use actix_web::{get, web::{self, Data}, HttpRequest, Responder}; +use actix_web::{HttpRequest, HttpResponse, Responder, get, web::{self, Data}}; use anyhow::anyhow; -use chrono::Timelike; +use askama::Template; +use chrono::{NaiveTime, Timelike}; use chrono_tz::America::New_York; use libseptastic::stop_schedule::{self, LiveTrip, Trip, TripTracking}; use log::info; @@ -34,6 +35,118 @@ async fn get_stops_html(req: HttpRequest, state: Data>) -> impl Re } }).await } + +async fn get_trip_perspective_for_stop(state: &Data>,stop: &libseptastic::stop::Stop) -> Vec { + let routes: Vec = state.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| { + match state.gtfs_service.get_route(route.clone()) { + Ok(route) => Some(route), + Err(_) => None + } + }).collect(); + + let route_ids: HashSet = routes.iter().map(|route| route.id.clone()).collect(); + + let mut trips = state.gtfs_service.get_all_trips().iter().filter_map(|trip| { + if route_ids.contains(trip.0) { + Some(trip.1.clone()) + } else { + None + } + }).flatten().collect(); + + + state.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 = 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.get_arrival_time(&live); + 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| + match &f.trip.tracking_data { + TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live), + _ => f.perspective_stop.arrival_time + }); + + filtered_trips +} + +#[get("/stop/{stop_id}/table")] +async fn get_stop_table_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { + let stop_id = path; + + if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) { + let filtered_trips = get_trip_perspective_for_stop(&state, &stop).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()); + + + HttpResponse::Ok().body(crate::templates::StopTableTemplate { + trips: filtered_trips, + current_time: cur_time + }.render().unwrap()) + } + else { + HttpResponse::InternalServerError().body("Error") + } +} + #[get("/stop/{stop_id}")] async fn get_stop_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { crate::perform_action(req, move || { @@ -44,6 +157,7 @@ async fn get_stop_html(req: HttpRequest, state: Data>, path: web:: let start_time = Instant::now(); if let Some(stop) = statex.gtfs_service.get_stop_by_id(&stop_id) { + let routes: Vec = 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), @@ -51,83 +165,13 @@ async fn get_stop_html(req: HttpRequest, state: Data>, path: web:: } }).collect(); - let route_ids: HashSet = 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 filtered_trips = get_trip_perspective_for_stop(&statex, &stop).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 = 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.get_arrival_time(&live); - 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| - match &f.trip.tracking_data { - TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live), - _ => f.perspective_stop.arrival_time - }); - Ok(crate::templates::ContentTemplate { page_title: Some(stop.name.clone()), page_desc: Some(String::from("Stop information")), diff --git a/api/src/main.rs b/api/src/main.rs index 29e6169..bc7d958 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -110,6 +110,7 @@ async fn main() -> ::anyhow::Result<()> { // .service(controllers::route::get_directions) .service(controllers::stop::get_stops_html) .service(controllers::stop::get_stop_html) + .service(controllers::stop::get_stop_table_html) .service(get_index) .service(actix_files::Files::new("/assets", "./assets")) }) diff --git a/api/src/templates.rs b/api/src/templates.rs index ad5102f..806ac16 100644 --- a/api/src/templates.rs +++ b/api/src/templates.rs @@ -77,6 +77,13 @@ pub struct StopTemplate { pub current_time: i64 } +#[derive(askama::Template)] +#[template(path = "stop_table_impl.html")] +pub struct StopTableTemplate { + pub trips: Vec, + pub current_time: i64 +} + pub fn build_timetables( directions: Vec, trips: Vec, @@ -183,6 +190,7 @@ mod filters { return Ok(format!("{}ns", nanos)); } } + pub fn format_time( seconds_since_midnight: &i64, _: &dyn askama::Values, @@ -190,8 +198,10 @@ mod filters { let total_minutes = seconds_since_midnight / 60; let (hours, ampm) = { let hrs = total_minutes / 60; - if hrs >= 12 { + if hrs > 12 { (hrs - 12, "PM") + } else if hrs == 12 { + (12, "PM") } else if hrs > 0 { (hrs, "AM") } else { @@ -201,4 +211,26 @@ mod filters { let minutes = total_minutes % 60; Ok(format!("{}:{:02} {}", hours, minutes, ampm)) } + + pub fn format_time_with_seconds( + seconds_since_midnight: &i64, + _: &dyn askama::Values, + ) -> askama::Result { + let total_minutes = seconds_since_midnight / 60; + let (hours, ampm) = { + let hrs = total_minutes / 60; + if hrs > 12 { + (hrs - 12, "PM") + } else if hrs == 12 { + (12, "PM") + } else if hrs > 0 { + (hrs, "AM") + } else { + (12, "AM") + } + }; + let minutes = total_minutes % 60; + let seconds = seconds_since_midnight % 60; + Ok(format!("{}:{:02}:{:02} {}", hours, minutes, seconds, ampm)) + } } diff --git a/api/templates/layout.html b/api/templates/layout.html index ac4882c..8d35551 100644 --- a/api/templates/layout.html +++ b/api/templates/layout.html @@ -18,6 +18,7 @@ +