messy filter support
This commit is contained in:
parent
6773e6ae30
commit
b7ec6a292f
15 changed files with 445 additions and 103 deletions
43
api/Cargo.lock
generated
43
api/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ annotations:
|
|||
- 'SEPTABUS_32990'
|
||||
- 'SEPTABUS_32992'
|
||||
- 'SEPTABUS_32993'
|
||||
- 'SEPTARAIL_90220'
|
||||
- id: 'STC'
|
||||
name: "Susquehanna Transit Center"
|
||||
platform_station_ids:
|
||||
|
|
|
|||
|
|
@ -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<Arc<AppState>>) -> impl Re
|
|||
}).await
|
||||
}
|
||||
|
||||
async fn get_trip_perspective_for_stop(state: &Data<Arc<AppState>>,stop: &libseptastic::stop::Stop) -> Vec<TripPerspective> {
|
||||
async fn get_trip_perspective_for_stop(state: &Data<Arc<AppState>>,stop: &libseptastic::stop::Stop, filter: &StopFilter) -> Vec<TripPerspective> {
|
||||
|
||||
let routes: Vec<libseptastic::route::Route> = 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<Arc<AppState>>,stop: &libsep
|
|||
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()) {
|
||||
// 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<Arc<AppState>>,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<Arc<AppState>>,stop: &libsep
|
|||
|
||||
filtered_trips
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct StopFilter {
|
||||
pub routes: Option<HashSet<String>>,
|
||||
pub live_tracked: Option<bool>,
|
||||
pub scheduled: Option<bool>,
|
||||
pub crowding: Option<HashSet<SeatAvailability>>,
|
||||
pub unknown_crowding: Option<bool>
|
||||
}
|
||||
|
||||
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<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
async fn get_stop_table_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>, query: QsQuery<StopFilter>) -> 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 {
|
||||
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
|
||||
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<Arc<AppState>>, path:
|
|||
}
|
||||
|
||||
#[get("/stop/{stop_id}")]
|
||||
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>, query: QsQuery<StopFilter>) -> 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<Arc<AppState>>, 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<Arc<AppState>>, 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())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<String, Arc<libseptastic::stop::Stop>>,
|
||||
pub platforms: HashMap<String, Arc<libseptastic::stop::Platform>>,
|
||||
pub calendar_days: HashMap<String, Arc<libseptastic::stop_schedule::CalendarDay>>,
|
||||
pub directions: HashMap<String, Vec<Arc<libseptastic::direction::Direction>>>,
|
||||
|
||||
// extended lookup methods
|
||||
pub route_id_by_stops: HashMap<String, HashSet<String>>,
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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::<f64>()?)} else {None},
|
||||
None => None
|
||||
|
|
|
|||
|
|
@ -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<T: askama::Template> {
|
||||
|
|
@ -72,16 +74,21 @@ pub struct TripPerspective {
|
|||
#[template(path = "stop.html")]
|
||||
pub struct StopTemplate {
|
||||
pub stop: libseptastic::stop::Stop,
|
||||
pub routes: Vec<libseptastic::route::Route>,
|
||||
pub routes: BTreeSet<libseptastic::route::Route>,
|
||||
pub trips: Vec<TripPerspective>,
|
||||
pub current_time: i64
|
||||
pub current_time: i64,
|
||||
pub filters: Option<StopFilter>,
|
||||
pub query_str: String
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "stop_table_impl.html")]
|
||||
pub struct StopTableTemplate {
|
||||
pub trips: Vec<TripPerspective>,
|
||||
pub current_time: i64
|
||||
pub current_time: i64,
|
||||
pub filters: Option<StopFilter>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
<div style="display: flex; align-items: center;">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
<h1 style="margin-left: 15px;">{{ route.name }}</h1>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,19 +10,91 @@
|
|||
{% for route in routes %}
|
||||
<div style="margin-right: 5px">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
</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 %}#}
|
||||
<details>
|
||||
<summary><p style="font-weight: bold; font-size: large;">Filters</p></summary>
|
||||
<form hx-trigger="submit" hx-get="/stop/{{ stop.id }}/table" hx-target="#nta-table" hx-swap="outerHTML" hx-push-url="/stop/{{ stop.id}}">
|
||||
<div style="margin: 5px; padding: 10px; background-color: #eee;">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<fieldset style="flex-grow: 1;">
|
||||
<legend>Route</legend>
|
||||
{% 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) %}
|
||||
<input type="checkbox" class="route-checkbox" name="routes" id="{{ route.id }},{{ dir.direction }}" value="{{ route.id }},{{ dir.direction}}" checked="{{ rts.contains(&*route_filter_id) }}">
|
||||
{% else %}
|
||||
<input type="checkbox" class="route-checkbox" name="routes" id="{{ route.id }},{{ dir.direction }}" value="{{ route.id }},{{ dir.direction}}" checked="true">
|
||||
{% endif %}
|
||||
|
||||
<div hx-get="/stop/{{ stop.id }}/table" hx-trigger="every 5s">
|
||||
{% call stop_table::stop_table(trips, current_time) %}
|
||||
<label for="{{ route.id }},{{ dir.direction }}">
|
||||
<b>{{ route.short_name }}</b>: {{ dir.direction_destination }}
|
||||
</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<input type="checkbox" id="master"
|
||||
hx-on:click="document.querySelectorAll('.route-checkbox').forEach(c => c.checked = this.checked)">
|
||||
<label for="master">Select/Deselect All</label>
|
||||
</fieldset>
|
||||
<div style="flex-grow: 1;">
|
||||
<fieldset>
|
||||
<legend>Ride Options</legend>
|
||||
{% if let Some(fil) = filters && let Some(lt) = fil.live_tracked %}
|
||||
<input type="checkbox" name="live_tracked" id="live_tracked" value="true" checked="{{ lt }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="live_tracked" id="live_tracked" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="live-tracked">
|
||||
Live Tracked
|
||||
</label>
|
||||
<br>
|
||||
{% if let Some(fil) = filters && let Some(sc) = fil.scheduled %}
|
||||
<input type="checkbox" name="scheduled" id="scheduled" value="true" checked="{{ sc }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="scheduled" id="scheduled" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">
|
||||
Scheduled
|
||||
</label>
|
||||
<br>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Crowding</legend>
|
||||
{% for avail in SeatAvailability::iter() %}
|
||||
|
||||
{% if let Some(fil) = filters && let Some(crd) = fil.crowding %}
|
||||
<input type="checkbox" name="crowding" id="{{ avail.to_string() }}" value="{{ avail.to_string() }}" checked="{{ crd.contains(&avail) }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="crowding" id="{{ avail.to_string() }}" value="{{ avail.to_string() }}" checked="true">
|
||||
{% endif %}
|
||||
<label for="{{ avail.to_string() }}">
|
||||
{{ avail.to_human_string() }}
|
||||
</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
{% if let Some(fil) = filters && let Some(uc) = fil.unknown_crowding %}
|
||||
<input type="checkbox" name="unknown_crowding" id="unknown_crowding" value="true" checked="{{ uc }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="unknown_crowding" id="unknown_crowding" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">
|
||||
Unknown
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Apply">
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<div style="overflow-x: scroll; max-width: 100%;">
|
||||
{% call stop_table::stop_table(trips, current_time, stop.id, query_str) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
|
||||
{% macro stop_table(trips, current_time) %}
|
||||
{% macro stop_table(trips, current_time, stop_id, query_str) %}
|
||||
<div id="nta-table" hx-get="/stop/{{ stop_id }}/table?{{ query_str }}" hx-trigger="every 5s" hx-swap="outer-html">
|
||||
<table class="train-direction-table">
|
||||
<tr>
|
||||
<th>ROUTE</th>
|
||||
|
|
@ -8,11 +9,14 @@
|
|||
<th>BOARDING AREA</th>
|
||||
<th>TIME</th>
|
||||
<th>VEHICLE</th>
|
||||
<th>TRIP</th>
|
||||
<th>CROWDING</th>
|
||||
</tr>
|
||||
{% for trip in trips %}
|
||||
<tr>
|
||||
<td>
|
||||
{% call scope::route_symbol(trip.trip.route) %}
|
||||
{% endcall %}
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ trip.trip.direction.direction_destination }}</p>
|
||||
|
|
@ -24,6 +28,7 @@
|
|||
<td style="color: #008800">
|
||||
<p style="font-size: small;">{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ tracked_trip.delay.round() }} late</p>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
|
|
@ -40,12 +45,29 @@
|
|||
-
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ trip.trip.trip_id }}</td>
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
{% if let Some(seat_avail) = tracked_trip.seat_availability %}
|
||||
<td>
|
||||
{{ seat_avail.to_human_string() }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
N/A
|
||||
</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td>
|
||||
-
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="7">
|
||||
<p>Updated at: {{ current_time | format_time_with_seconds }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<crate::direction::Direction>
|
||||
}
|
||||
|
||||
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<Ordering> {
|
||||
Some(self.id.cmp(&other.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl SeatAvailability {
|
||||
pub fn iter() -> Vec<SeatAvailability> {
|
||||
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<SeatAvailability> {
|
||||
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<String>) -> Option<SeatAvailability> {
|
||||
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<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub heading: Option<f64>,
|
||||
pub seat_availability: Option<String>,
|
||||
pub seat_availability: Option<SeatAvailability>,
|
||||
pub trip_id: String,
|
||||
pub route_id: String,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue