All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 5m38s
264 lines
7.5 KiB
Rust
264 lines
7.5 KiB
Rust
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<T: askama::Template> {
|
|
pub content: T,
|
|
pub page_title: Option<String>,
|
|
pub page_desc: Option<String>,
|
|
pub load_time_ms: Option<u128>,
|
|
pub widescreen: bool,
|
|
}
|
|
|
|
#[derive(askama::Template)]
|
|
#[template(path = "route.html")]
|
|
pub struct RouteTemplate {
|
|
pub route: libseptastic::route::Route,
|
|
pub timetables: Vec<TimetableDirection>,
|
|
pub filter_stops: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(askama::Template)]
|
|
#[template(path = "routes.html")]
|
|
pub struct RoutesTemplate {
|
|
pub rr_routes: Vec<libseptastic::route::Route>,
|
|
pub subway_routes: Vec<libseptastic::route::Route>,
|
|
pub trolley_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)]
|
|
#[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<Option<i64>>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TimetableDirection {
|
|
pub direction: Direction,
|
|
pub trip_ids: Vec<String>,
|
|
pub tracking_data: Vec<TripTracking>,
|
|
pub rows: Vec<TimetableStopRow>,
|
|
pub next_id: Option<String>,
|
|
}
|
|
|
|
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<libseptastic::route::Route>,
|
|
pub trips: Vec<TripPerspective>,
|
|
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 query_str: String,
|
|
pub stop_id: String,
|
|
}
|
|
|
|
#[derive(askama::Template)]
|
|
#[template(path = "stop_search_results.html")]
|
|
pub struct StopSearchResults {
|
|
pub results: Vec<Stop>
|
|
}
|
|
|
|
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<MapItem>
|
|
}
|
|
|
|
pub fn build_timetables(directions: Vec<Direction>, trips: Vec<Trip>) -> Vec<TimetableDirection> {
|
|
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<String> = 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<String> = direction_trips.iter().map(|t| t.trip_id.clone()).collect();
|
|
|
|
let live_trips: Vec<TripTracking> = direction_trips
|
|
.iter()
|
|
.map(|t| t.tracking_data.clone())
|
|
.collect();
|
|
|
|
let mut stop_map: BTreeMap<String, (i64, String, Vec<Option<i64>>)> = 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<TimetableStopRow> = 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<String> {
|
|
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<String> {
|
|
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<String> {
|
|
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))
|
|
}
|
|
}
|