use chrono::Timelike; use chrono_tz::America::New_York; use libseptastic::stop::Stop; use libseptastic::stop_schedule::TripTracking::Tracked; use libseptastic::{ direction::Direction, stop_schedule::{SeatAvailability, Trip, TripTracking}, }; use serde::Serialize; use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, }; use crate::controllers::stop::StopFilter; #[derive(askama::Template)] #[template(path = "layout.html")] pub struct ContentTemplate { pub content: T, pub page_title: Option, pub page_desc: Option, pub load_time_ms: Option, pub widescreen: bool, } #[derive(askama::Template)] #[template(path = "route.html")] pub struct RouteTemplate { pub route: libseptastic::route::Route, pub timetables: Vec, pub filter_stops: Option>, } #[derive(askama::Template)] #[template(path = "routes.html")] pub struct RoutesTemplate { pub rr_routes: Vec, pub subway_routes: Vec, pub trolley_routes: Vec, pub bus_routes: Vec, } #[derive(askama::Template)] #[template(path = "stops.html")] pub struct StopsTemplate { pub tc_stops: Vec, } #[derive(askama::Template)] #[template(path = "index.html")] pub struct IndexTemplate {} #[derive(Debug, Serialize)] pub struct TimetableStopRow { pub stop_id: String, pub stop_name: String, pub stop_sequence: i64, pub times: Vec>, } #[derive(Debug, Serialize)] pub struct TimetableDirection { pub direction: Direction, pub trip_ids: Vec, pub tracking_data: Vec, pub rows: Vec, pub next_id: Option, } pub struct TripPerspective { pub trip: libseptastic::stop_schedule::Trip, pub perspective_stop: libseptastic::stop_schedule::StopSchedule, } #[derive(askama::Template)] #[template(path = "stop.html")] pub struct StopTemplate { pub stop: libseptastic::stop::Stop, pub routes: BTreeSet, pub trips: Vec, pub current_time: i64, pub filters: Option, pub query_str: String, } #[derive(askama::Template)] #[template(path = "stop_table_impl.html")] pub struct StopTableTemplate { pub trips: Vec, pub current_time: i64, pub query_str: String, pub stop_id: String, } #[derive(askama::Template)] #[template(path = "stop_search_results.html")] pub struct StopSearchResults { pub results: Vec } pub struct MapItem { pub lat: f64, pub lng: f64, pub route_id: String, pub headsign: String, } #[derive(askama::Template)] #[template(path = "map.html")] pub struct MapTemplate { pub locations: Vec } pub fn build_timetables(directions: Vec, trips: Vec) -> Vec { let mut results = Vec::new(); for direction in directions { let now_utc = chrono::Utc::now(); let now = now_utc.with_timezone(&New_York); let naive_time = now.time(); let seconds_since_midnight = naive_time.num_seconds_from_midnight(); let mut next_id: Option = None; let mut direction_trips: Vec<&Trip> = trips .iter() .filter(|trip| trip.direction.direction == direction.direction) .collect(); direction_trips.sort_by_key(|trip| { trip.schedule .iter() .filter_map(|s| Some(s.arrival_time)) .min() .unwrap_or(i64::MAX) }); for trip in direction_trips.clone() { if let Some(last) = trip.schedule.iter().max_by_key(|x| x.arrival_time) { if next_id == None && i64::from(seconds_since_midnight) < last.arrival_time { next_id = Some(last.stop.id.to_string()); } } } let trip_ids: Vec = direction_trips.iter().map(|t| t.trip_id.clone()).collect(); let live_trips: Vec = direction_trips .iter() .map(|t| t.tracking_data.clone()) .collect(); let mut stop_map: BTreeMap>)> = BTreeMap::new(); for (trip_index, trip) in direction_trips.iter().enumerate() { for stop in &trip.schedule { let entry = stop_map.entry(stop.stop.id.clone()).or_insert(( stop.stop_sequence, stop.stop.name.clone(), vec![None; direction_trips.len()], )); // If this stop_id appears in multiple trips with different sequences, keep the lowest entry.0 = entry.0.max(stop.stop_sequence); entry.1 = stop.stop.name.clone(); entry.2[trip_index] = Some(stop.arrival_time); } } let mut rows: Vec = stop_map .into_iter() .map( |(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow { stop_id, stop_sequence, stop_name, times, }, ) .collect(); rows.sort_by(|a, b| { if a.stop_sequence < b.stop_sequence { Ordering::Less } else { Ordering::Greater } }); results.push(TimetableDirection { direction: direction.clone(), trip_ids, rows, tracking_data: live_trips, next_id, }); } results } mod filters { use askama::filter_fn; #[filter_fn] pub fn format_load_time(nanos: &u128, _: &dyn askama::Values) -> askama::Result { if *nanos >= 1000000000 { return Ok(format!("{}s", (nanos / 1000000000))); } else if *nanos >= 1000000 { return Ok(format!("{}ms", nanos / 1000000)); } if *nanos >= 1000 { return Ok(format!("{}us", nanos / 1000)); } else { return Ok(format!("{}ns", nanos)); } } #[filter_fn] pub fn format_time( 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; Ok(format!("{}:{:02} {}", hours, minutes, ampm)) } #[filter_fn] 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)) } }