From b7ec6a292f1aad3120a8b9c7fe7d60b8d296a5db Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Mon, 16 Feb 2026 18:49:47 -0500 Subject: [PATCH] messy filter support --- api/Cargo.lock | 43 +++++++++--- api/Cargo.toml | 3 +- api/assets/style.css | 8 ++- api/config.yaml | 1 + api/src/controllers/stop.rs | 104 +++++++++++++++++++++------ api/src/services/gtfs_pull.rs | 47 ++++++++++++- api/src/services/trip_tracking.rs | 9 ++- api/src/templates.rs | 22 ++++-- api/templates/route.html | 1 + api/templates/stop.html | 92 +++++++++++++++++++++--- api/templates/stop_table.html | 108 +++++++++++++++++------------ api/templates/stop_table_impl.html | 3 +- libseptastic/src/direction.rs | 2 +- libseptastic/src/route.rs | 25 ++++++- libseptastic/src/stop_schedule.rs | 80 ++++++++++++++++++++- 15 files changed, 445 insertions(+), 103 deletions(-) diff --git a/api/Cargo.lock b/api/Cargo.lock index 3b088e5..74f75f3 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -387,11 +387,11 @@ dependencies = [ [[package]] name = "askama" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57" dependencies = [ - "askama_derive", + "askama_macros", "itoa", "percent-encoding", "serde", @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37" dependencies = [ "askama_parser", "basic-toml", @@ -416,14 +416,24 @@ dependencies = [ ] [[package]] -name = "askama_parser" -version = "0.14.0" +name = "askama_macros" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b" dependencies = [ - "memchr", + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c" +dependencies = [ + "rustc-hash", "serde", "serde_derive", + "unicode-ident", "winnow", ] @@ -2792,6 +2802,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_qs", "serde_yaml", "sqlx", "sqlx-cli", @@ -2831,6 +2842,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" +dependencies = [ + "actix-web", + "futures", + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/api/Cargo.toml b/api/Cargo.toml index 141b61b..c8b3382 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -12,7 +12,7 @@ log = "0.4.27" serde_json = "1.0.140" sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres"] } libseptastic = { path = "../libseptastic/" } -askama = "0.14.0" +askama = "0.15.4" actix-files = "0.6.6" serde = "1.0.219" chrono = "0.4.41" @@ -29,3 +29,4 @@ gtfs-realtime = "0.2.0" prost = "0.14.1" futures = "0.3.31" tokio = "1.48.0" +serde_qs = { version = "1.0.0", features = ["actix4"] } diff --git a/api/assets/style.css b/api/assets/style.css index cb86bf6..3210184 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -151,8 +151,6 @@ img { color: #000000; width: max-content; height: max-content; - aspect-ratio: 3/2; - line-height: 1; } .tscroll { width: 100%; @@ -174,3 +172,9 @@ img { details summary > * { display: inline; } + +details { + margin-top: 5px; + margin-bottom: 5px; + padding: 5px; +} diff --git a/api/config.yaml b/api/config.yaml index e24345b..84f020c 100644 --- a/api/config.yaml +++ b/api/config.yaml @@ -20,6 +20,7 @@ annotations: - 'SEPTABUS_32990' - 'SEPTABUS_32992' - 'SEPTABUS_32993' + - 'SEPTARAIL_90220' - id: 'STC' name: "Susquehanna Transit Center" platform_station_ids: diff --git a/api/src/controllers/stop.rs b/api/src/controllers/stop.rs index 4592098..116e1d3 100644 --- a/api/src/controllers/stop.rs +++ b/api/src/controllers/stop.rs @@ -1,11 +1,13 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, get, web::{self, Data}}; +use actix_web::{HttpRequest, HttpResponse, Responder, get, guard::Header, http::header::Header, web::{self, Data}}; use anyhow::anyhow; use askama::Template; -use chrono::{NaiveTime, Timelike}; +use chrono::{NaiveTime, TimeDelta, Timelike}; use chrono_tz::America::New_York; -use libseptastic::stop_schedule::{self, LiveTrip, Trip, TripTracking}; +use libseptastic::stop_schedule::{self, LiveTrip, SeatAvailability, Trip, TripTracking}; use log::info; -use std::{collections::HashSet, sync::Arc, time::Instant}; +use serde::{Deserialize, Serialize}; +use serde_qs::actix::QsQuery; +use std::{collections::{BTreeSet, HashSet}, sync::Arc, time::Instant}; use crate::{AppState, templates::TripPerspective}; @@ -36,7 +38,8 @@ 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 { +async fn get_trip_perspective_for_stop(state: &Data>,stop: &libseptastic::stop::Stop, filter: &StopFilter) -> 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), @@ -63,12 +66,9 @@ async fn get_trip_perspective_for_stop(state: &Data>,stop: &libsep 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()) { + // poor midnight handling? -- going to offset by 4 hours, assume next 'schedule day' + // starts at 4a. Still may miss some trips. Oh well! + if !trip.calendar_day.is_calendar_active_for_date(&now.naive_local().checked_add_signed(TimeDelta::hours(-4))?.date()) { return None } let mut est_arrival_time = 0; @@ -103,7 +103,7 @@ async fn get_trip_perspective_for_stop(state: &Data>,stop: &libsep } }).filter_map(|ss| Some(ss.clone())).collect(); - if stop_sched.len() > 0 { + if stop_sched.len() > 0 && filter.trip_matches(trip) { Some(TripPerspective { est_arrival_time, is_tracked, @@ -123,23 +123,80 @@ async fn get_trip_perspective_for_stop(state: &Data>,stop: &libsep filtered_trips } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StopFilter { + pub routes: Option>, + pub live_tracked: Option, + pub scheduled: Option, + pub crowding: Option>, + pub unknown_crowding: Option +} + +impl StopFilter { + pub fn trip_matches(&self, trip: &Trip) -> bool { + let unspecified = self.live_tracked == None && self.scheduled == None; + let unknown_crowding = self.unknown_crowding.unwrap_or(true); + + if (Some(false) == self.scheduled || (!unspecified && self.scheduled == None)) && match trip.tracking_data { + TripTracking::Untracked => true, + _ => false + } { + return false; + } + + if (Some(false) == self.live_tracked || (!unspecified && self.live_tracked == None)) && match trip.tracking_data { + TripTracking::Tracked(_) => true, + _ => false + } { + return false; + } + + if let Some(routes) = &self.routes { + let route_str = format!("{},{}", trip.route.id, trip.direction.direction); + if !routes.contains(&route_str) { + return false; + } + } + + if let Some(crowding) = &self.crowding { + if let TripTracking::Tracked(live_trip) = &trip.tracking_data { + if let Some(seat_availability) = &live_trip.seat_availability { + if !crowding.contains(seat_availability) { + return false; + } + } else { + return unknown_crowding; + } + } else { + return unknown_crowding; + } + } + + return true; + } +} #[get("/stop/{stop_id}/table")] -async fn get_stop_table_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { +async fn get_stop_table_html(req: HttpRequest, state: Data>, path: web::Path, query: QsQuery) -> 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 filtered_trips = get_trip_perspective_for_stop(&state, &stop, &query).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 query_str =serde_qs::Config::new().array_format(serde_qs::ArrayFormat::Unindexed).serialize_string(&query.0.clone()).unwrap(); - - HttpResponse::Ok().body(crate::templates::StopTableTemplate { - trips: filtered_trips, - current_time: cur_time + HttpResponse::Ok().append_header(("HX-Replace-Url", format!("/stop/{}?{}", stop_id,&query_str).as_str())) + .body(crate::templates::StopTableTemplate { + trips: filtered_trips, + current_time: cur_time, + filters: Some(query.0.clone()), + query_str, + stop_id: stop_id.to_string() }.render().unwrap()) } else { @@ -148,10 +205,11 @@ async fn get_stop_table_html(req: HttpRequest, state: Data>, path: } #[get("/stop/{stop_id}")] -async fn get_stop_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { +async fn get_stop_html(req: HttpRequest, state: Data>, path: web::Path, query: QsQuery) -> impl Responder { crate::perform_action(req, move || { let statex = state.clone(); let pathx = path.clone(); + let queryx = query.clone(); async move { let stop_id = pathx; let start_time = Instant::now(); @@ -165,7 +223,7 @@ async fn get_stop_html(req: HttpRequest, state: Data>, path: web:: } }).collect(); - let filtered_trips = get_trip_perspective_for_stop(&statex, &stop).await; + let filtered_trips = get_trip_perspective_for_stop(&statex, &stop, &queryx).await; let now_utc = chrono::Utc::now(); let now = now_utc.with_timezone(&New_York); @@ -178,9 +236,11 @@ async fn get_stop_html(req: HttpRequest, state: Data>, path: web:: widescreen: false, content: crate::templates::StopTemplate { stop: stop.clone(), - routes, + routes: BTreeSet::from_iter(routes.into_iter()), trips: filtered_trips, - current_time: cur_time + current_time: cur_time, + filters: Some(queryx.0.clone()), + query_str: serde_qs::Config::new().array_format(serde_qs::ArrayFormat::Unindexed).serialize_string(&queryx.0).unwrap(), }, load_time_ms: Some(start_time.elapsed().as_millis()) }) diff --git a/api/src/services/gtfs_pull.rs b/api/src/services/gtfs_pull.rs index ce47f21..dcb5574 100644 --- a/api/src/services/gtfs_pull.rs +++ b/api/src/services/gtfs_pull.rs @@ -1,8 +1,8 @@ -use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration}; +use std::{cmp::Ordering, collections::{HashMap, HashSet, hash_map::Entry}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration}; use anyhow::anyhow; use libseptastic::{stop::Platform, stop_schedule::CalendarDay}; -use log::{info, error}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use zip::ZipArchive; @@ -49,6 +49,7 @@ struct TransitData { pub stops: HashMap>, pub platforms: HashMap>, pub calendar_days: HashMap>, + pub directions: HashMap>>, // extended lookup methods pub route_id_by_stops: HashMap>, @@ -70,7 +71,7 @@ pub struct GtfsPullService { impl TransitData { pub fn new() -> Self { - 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() } + 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() ,directions: HashMap::new() } } } @@ -237,14 +238,24 @@ impl GtfsPullService { fn populate_routes(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { for route in >fs.routes { let global_rt_id = make_global_id!(prefix, route.1.id); + info!("{}", global_rt_id); let rt_name = match route.1.long_name.clone() { Some(x) => x, _ => String::from("Unknown") }; + let dirs = match state.transit_data.directions.get(&global_rt_id) { + Some(x) => x.iter().map(|f| libseptastic::direction::Direction::clone(f)).collect(), + None => { + warn!("Excluding {} because it has no directions", global_rt_id); + continue + } + }; + state.transit_data.routes.insert(global_rt_id.clone(), Arc::new(libseptastic::route::Route{ name: rt_name, + directions: dirs, short_name: match route.1.short_name.clone() { Some(x) => x, _ => String::from("unknown") @@ -267,6 +278,35 @@ impl GtfsPullService { Ok(()) } + fn populate_directions(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { + for trip in >fs.trips { + let global_rt_id = make_global_id!(prefix, trip.1.route_id); + + let dir = 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() + }; + + match state.transit_data.directions.entry(global_rt_id) { + Entry::Vacant(e) => { e.insert(vec![Arc::new(dir)]); }, + Entry::Occupied(mut e) => { + if e.get().iter().filter(|x| x.direction == dir.direction).count() == 0 { + e.get_mut().push(Arc::new(dir)); + } + } + } + } + + for dir in &mut state.transit_data.directions { + dir.1.sort_by(|x,y| if x.direction > y.direction {Ordering::Greater} else {Ordering::Less}); + } + + Ok(()) + } + fn populate_trips(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { for trip in >fs.trips { let global_rt_id = make_global_id!(prefix, trip.1.route_id); @@ -351,6 +391,7 @@ impl GtfsPullService { info!("Data loaded, processing..."); for (gtfs, prefix) in >fses { + GtfsPullService::populate_directions(&mut l_state, &prefix, >fs)?; GtfsPullService::populate_routes(&mut l_state, &prefix, >fs)?; GtfsPullService::populate_stops(&mut l_state, &prefix, >fs)?; for calendar in >fs.calendar { diff --git a/api/src/services/trip_tracking.rs b/api/src/services/trip_tracking.rs index 6189160..9bccb70 100644 --- a/api/src/services/trip_tracking.rs +++ b/api/src/services/trip_tracking.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::time::Duration; use log::{error, info}; use serde::{Serialize, Deserialize, Deserializer}; -use libseptastic::stop_schedule::{LiveTrip, TripTracking}; +use libseptastic::stop_schedule::{LiveTrip, SeatAvailability, TripTracking}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LiveTripJson { @@ -87,7 +87,10 @@ impl TripTrackingService { separated.push_bind(live_data.latitude); separated.push_bind(live_data.longitude); separated.push_bind(live_data.heading); - separated.push_bind(live_data.seat_availability.clone()); + separated.push_bind(match &live_data.seat_availability { + Some(s) => Some(s.to_string()), + None => None + }); separated.push_bind(live_data.vehicle_ids.clone()); separated.push_bind(live_data.trip_id.clone()); separated.push_bind(live_data.route_id.clone()); @@ -160,7 +163,7 @@ impl TripTrackingService { trip_id: live_track.trip_id.clone(), route_id: live_track.route_id, delay: live_track.delay, - seat_availability: live_track.seat_availability, + seat_availability: SeatAvailability::from_opt_string(&live_track.seat_availability), heading: match live_track.heading { Some(hdg) => if hdg != "" { Some(hdg.parse::()?)} else {None}, None => None diff --git a/api/src/templates.rs b/api/src/templates.rs index 806ac16..7ac8dc7 100644 --- a/api/src/templates.rs +++ b/api/src/templates.rs @@ -1,10 +1,12 @@ use chrono_tz::America::New_York; -use libseptastic::{direction::Direction, stop_schedule::{Trip, TripTracking}}; -use std::{cmp::Ordering, collections::BTreeMap}; +use libseptastic::{direction::Direction, stop_schedule::{Trip, TripTracking, SeatAvailability}}; +use std::{cmp::Ordering, collections::{BTreeMap, BTreeSet}}; use serde::{Serialize}; use libseptastic::stop_schedule::TripTracking::Tracked; use chrono::Timelike; +use crate::controllers::stop::StopFilter; + #[derive(askama::Template)] #[template(path = "layout.html")] pub struct ContentTemplate { @@ -72,16 +74,21 @@ pub struct TripPerspective { #[template(path = "stop.html")] pub struct StopTemplate { pub stop: libseptastic::stop::Stop, - pub routes: Vec, + pub routes: BTreeSet, pub trips: Vec, - pub current_time: i64 + 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 current_time: i64, + pub filters: Option, + pub query_str: String, + pub stop_id: String } pub fn build_timetables( @@ -176,6 +183,9 @@ pub fn build_timetables( } mod filters { + use askama::filter_fn; + + #[filter_fn] pub fn format_load_time( nanos: &u128, _: &dyn askama::Values, @@ -191,6 +201,7 @@ mod filters { } } + #[filter_fn] pub fn format_time( seconds_since_midnight: &i64, _: &dyn askama::Values, @@ -212,6 +223,7 @@ mod filters { Ok(format!("{}:{:02} {}", hours, minutes, ampm)) } + #[filter_fn] pub fn format_time_with_seconds( seconds_since_midnight: &i64, _: &dyn askama::Values, diff --git a/api/templates/route.html b/api/templates/route.html index 1684adf..85cfba4 100644 --- a/api/templates/route.html +++ b/api/templates/route.html @@ -73,6 +73,7 @@ document.addEventListener("DOMContentLoaded", () => {
{% call scope::route_symbol(route) %} + {% endcall %}

{{ route.name }}

diff --git a/api/templates/stop.html b/api/templates/stop.html index 2f6c132..472ccd3 100644 --- a/api/templates/stop.html +++ b/api/templates/stop.html @@ -10,19 +10,91 @@ {% for route in routes %}
{% call scope::route_symbol(route) %} + {% endcall %}
{% endfor %} -{#{% if let libseptastic::stop::StopType::MultiPlatform(platforms) = stop.platforms %} -
-

Platforms at this station:

- {% for platform in platforms %} -

{{ platform.name }}

- {% endfor %} -
-{% endif %}#} +
+

Filters

+
+
+
+
+ Route + {% for route in routes %} + {% for dir in route.directions %} + {% if let Some(fil) = filters && let Some(rts) = fil.routes %} + {% let route_filter_id = format!("{},{}", route.id, dir.direction) %} + + {% else %} + + {% endif %} -
- {% call stop_table::stop_table(trips, current_time) %} + +
+ {% endfor %} + {% endfor %} + + +
+
+
+ Ride Options + {% if let Some(fil) = filters && let Some(lt) = fil.live_tracked %} + + {% else %} + + {% endif %} + +
+ {% if let Some(fil) = filters && let Some(sc) = fil.scheduled %} + + {% else %} + + {% endif %} + +
+
+
+ Crowding + {% for avail in SeatAvailability::iter() %} + + {% if let Some(fil) = filters && let Some(crd) = fil.crowding %} + + {% else %} + + {% endif %} + +
+ {% endfor %} + + {% if let Some(fil) = filters && let Some(uc) = fil.unknown_crowding %} + + {% else %} + + {% endif %} + +
+
+
+ +
+
+
+ +
+ {% call stop_table::stop_table(trips, current_time, stop.id, query_str) %} + {% endcall %}
diff --git a/api/templates/stop_table.html b/api/templates/stop_table.html index 71ff6b1..6118618 100644 --- a/api/templates/stop_table.html +++ b/api/templates/stop_table.html @@ -1,51 +1,73 @@ {%- import "route_symbol.html" as scope -%} -{% macro stop_table(trips, current_time) %} - - - - - - - - -{% for trip in trips %} - - - - - {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} - - {% else %} - - {% endif %} - {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} +{% macro stop_table(trips, current_time, stop_id, query_str) %} +
+
ROUTEDESTINATIONBOARDING AREATIMEVEHICLE
- {% call scope::route_symbol(trip.trip.route) %} - -

{{ trip.trip.direction.direction_destination }}

-
-

{{ trip.perspective_stop.platform.name }}

-
-

{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}

-

{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins

-
-

{{ trip.perspective_stop.arrival_time | format_time }}

-

{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins

-
+ + + + + + + + + + {% for trip in trips %} + - {% else %} - {% endif %} - -{% endfor %} - - + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + + {% else %} + + {% endif %} + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + - -
ROUTEDESTINATIONBOARDING AREATIMEVEHICLETRIPCROWDING
- {{ tracked_trip.vehicle_ids.join(", ") }} + {% call scope::route_symbol(trip.trip.route) %} + {% endcall %} - - +

{{ trip.trip.direction.direction_destination }}

-

Updated at: {{ current_time | format_time_with_seconds }}

+
+

{{ trip.perspective_stop.platform.name }}

+
+

{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}

+

{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins

+

{{ tracked_trip.delay.round() }} late

+
+

{{ trip.perspective_stop.arrival_time | format_time }}

+

{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins

+
+ {{ tracked_trip.vehicle_ids.join(", ") }}
+ {% else %} + + - + + {% endif %} + {{ trip.trip.trip_id }} + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + {% if let Some(seat_avail) = tracked_trip.seat_availability %} + + {{ seat_avail.to_human_string() }} + + {% else %} + + N/A + + {% endif %} + {% else %} + + - + + {% endif %} + + {% endfor %} + + +

Updated at: {{ current_time | format_time_with_seconds }}

+ + + + {% endmacro %} diff --git a/api/templates/stop_table_impl.html b/api/templates/stop_table_impl.html index e230ec6..a3a6bf9 100644 --- a/api/templates/stop_table_impl.html +++ b/api/templates/stop_table_impl.html @@ -1,3 +1,4 @@ {%- import "stop_table.html" as stop_table -%} -{% call stop_table::stop_table(trips, current_time) %} +{% call stop_table::stop_table(trips, current_time, stop_id, query_str) %} +{% endcall %} diff --git a/libseptastic/src/direction.rs b/libseptastic/src/direction.rs index 88bbc54..4264f9f 100644 --- a/libseptastic/src/direction.rs +++ b/libseptastic/src/direction.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Eq)] +#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Eq, PartialOrd, Ord)] #[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")] pub enum CardinalDirection { Northbound, diff --git a/libseptastic/src/route.rs b/libseptastic/src/route.rs index 4189f71..c94ea9a 100644 --- a/libseptastic/src/route.rs +++ b/libseptastic/src/route.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use serde::{Deserialize, Serialize}; #[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone)] @@ -16,7 +18,28 @@ pub struct Route { pub short_name: String, pub color_hex: String, pub route_type: RouteType, - pub id: String + pub id: String, + pub directions: Vec +} + +impl PartialEq for Route { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Route {} + +impl Ord for Route { + fn cmp(&self, other: &Self) -> Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for Route { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.id.cmp(&other.id)) + } } diff --git a/libseptastic/src/stop_schedule.rs b/libseptastic/src/stop_schedule.rs index 44c11ab..f9a1474 100644 --- a/libseptastic/src/stop_schedule.rs +++ b/libseptastic/src/stop_schedule.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use chrono::{Datelike, Days, TimeZone, Weekday}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer, de::Error}; use crate::{direction::Direction, route::Route, stop::Platform}; @@ -93,6 +93,82 @@ impl CalendarDay { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SeatAvailability { + Full = 4, + CrushedStandingRoomOnly = 3, + FewSeats = 2, + ManySeats = 1, + Empty = 0 +} + +impl<'de> Deserialize<'de> for SeatAvailability { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> { + let string = String::deserialize(deserializer)?; + return match SeatAvailability::from_string(&string) { + Some(x) => Ok(x), + None => Err(serde::de::Error::custom("")) + }; + } +} + +impl Serialize for SeatAvailability { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + + +impl SeatAvailability { + pub fn iter() -> Vec { + vec![Self::Empty, Self::ManySeats, Self::FewSeats, Self::CrushedStandingRoomOnly, Self::Full] + } + + pub fn to_string(&self) -> String { + String::from(match &self { + Self::Full => "FULL", + Self::CrushedStandingRoomOnly => "CRUSHED_STANDING_ROOM_ONLY", + Self::FewSeats => "FEW_SEATS_AVAILABLE", + Self::ManySeats => "MANY_SEATS_AVAILABLE", + Self::Empty => "EMPTY", + }) + } + + pub fn to_human_string(&self) -> String { + String::from(match &self { + Self::Full => "Full", + Self::CrushedStandingRoomOnly => "Sardines", + Self::FewSeats => "Few seats", + Self::ManySeats => "Many seats", + Self::Empty => "Empty" + }) + } + + pub fn from_string(str: &String) -> Option { + match str.as_str() { + "FULL" => Some(Self::Full), + "CRUSHED_STANDING_ROOM_ONLY" => Some(Self::CrushedStandingRoomOnly), + "FEW_SEATS_AVAILABLE" => Some(Self::FewSeats), + "MANY_SEATS_AVAILABLE" => Some(Self::ManySeats), + "EMPTY" => Some(Self::Empty), + _ => None + } + } + + pub fn from_opt_string(opt_str: &Option) -> Option { + if let Some(str) = &opt_str { + Self::from_string(str) + } else { + None + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LiveTrip { pub delay: f64, @@ -102,7 +178,7 @@ pub struct LiveTrip { pub latitude: Option, pub longitude: Option, pub heading: Option, - pub seat_availability: Option, + pub seat_availability: Option, pub trip_id: String, pub route_id: String, }