live tracking and filter
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
This commit is contained in:
parent
534c36b0f7
commit
f5e0a31bb7
16 changed files with 414 additions and 115 deletions
|
|
@ -1 +0,0 @@
|
|||
use nix
|
||||
2
api/Cargo.lock
generated
2
api/Cargo.lock
generated
|
|
@ -2531,7 +2531,9 @@ dependencies = [
|
|||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.11",
|
||||
"http 1.3.1",
|
||||
"http-body",
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ serde = "1.0.219"
|
|||
chrono = "0.4.41"
|
||||
chrono-tz = "0.10.4"
|
||||
actix-cors = "0.7.1"
|
||||
reqwest = { version = "0.12.22", features = [ "json" ] }
|
||||
reqwest = { version = "0.12.22", features = [ "json", "blocking" ] }
|
||||
sqlx-cli = "0.8.6"
|
||||
|
|
|
|||
|
|
@ -132,3 +132,7 @@ img {
|
|||
|
||||
.tscroll td, .tscroll th {
|
||||
}
|
||||
|
||||
details summary > * {
|
||||
display: inline;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
with import <nixpkgs> {};
|
||||
stdenv.mkDerivation {
|
||||
name = "env";
|
||||
nativeBuildInputs = [ pkg-config ];
|
||||
buildInputs = [
|
||||
cryptsetup
|
||||
];
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
use actix_web::Route;
|
||||
use std::collections::HashMap;
|
||||
use libseptastic::{direction::CardinalDirection, route::RouteType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
|
||||
use crate::services::trip_tracking::TripTracking;
|
||||
|
||||
pub async fn get_route_by_id(
|
||||
id: String,
|
||||
transaction: &mut Transaction<'_, Postgres>,
|
||||
|
|
@ -36,6 +36,39 @@ pub async fn get_route_by_id(
|
|||
});
|
||||
}
|
||||
|
||||
pub async fn get_all_routes(
|
||||
transaction: &mut Transaction<'_, Postgres>,
|
||||
) -> ::anyhow::Result<Vec<libseptastic::route::Route>> {
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"SELECT
|
||||
id,
|
||||
name,
|
||||
short_name,
|
||||
color_hex,
|
||||
route_type as "route_type: libseptastic::route::RouteType"
|
||||
FROM
|
||||
septa_routes
|
||||
;"#
|
||||
)
|
||||
.fetch_all(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
let mut routes = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
routes.push(libseptastic::route::Route {
|
||||
name: row.name,
|
||||
short_name: row.short_name,
|
||||
color_hex: row.color_hex,
|
||||
route_type: row.route_type,
|
||||
id: row.id,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(routes);
|
||||
}
|
||||
|
||||
pub async fn get_direction_by_route_id(
|
||||
id: String,
|
||||
transaction: &mut Transaction<'_, Postgres>,
|
||||
|
|
@ -89,9 +122,11 @@ pub struct Trip {
|
|||
pub route_id: String,
|
||||
pub trip_id: String,
|
||||
pub direction_id: i64,
|
||||
pub tracking_data: TripTracking,
|
||||
pub schedule: Vec<StopSchedule>
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_schedule_by_route_id(
|
||||
id: String,
|
||||
transaction: &mut Transaction<'_, Postgres>,
|
||||
|
|
@ -133,7 +168,7 @@ pub async fn get_schedule_by_route_id(
|
|||
|
||||
let mut sched_groups: HashMap<String, Vec<StopSchedule>> = HashMap::new();
|
||||
for row in rows {
|
||||
let mut arr = match sched_groups.get_mut(&row.trip_id) {
|
||||
let arr = match sched_groups.get_mut(&row.trip_id) {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
sched_groups.insert(row.trip_id.clone(), Vec::new());
|
||||
|
|
@ -159,7 +194,8 @@ pub async fn get_schedule_by_route_id(
|
|||
trip_id: group.0,
|
||||
route_id: group.1[0].route_id.clone(),
|
||||
schedule: group.1.clone(),
|
||||
direction_id: group.1[0].direction_id.clone()
|
||||
direction_id: group.1[0].direction_id.clone(),
|
||||
tracking_data: TripTracking::Untracked
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
156
api/src/main.rs
156
api/src/main.rs
|
|
@ -2,18 +2,22 @@ use actix_web::{get, web::{self, Data}, App, HttpResponse, HttpServer, Responder
|
|||
use chrono::TimeDelta;
|
||||
use database::{get_direction_by_route_id, get_nta_by_stop_id, get_schedule_by_route_id};
|
||||
use env_logger::Env;
|
||||
use libseptastic::{direction::Direction};
|
||||
use database::{Trip, StopSchedule};
|
||||
use libseptastic::{direction::Direction, route::RouteType};
|
||||
use database::{Trip};
|
||||
use log::*;
|
||||
use dotenv::dotenv;
|
||||
use services::trip_tracking::{self, TripTracking};
|
||||
use std::{cmp::Ordering, collections::BTreeMap, sync::Arc};
|
||||
use askama::Template;
|
||||
use serde::{Serialize};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::TripTracking::Tracked;
|
||||
mod database;
|
||||
mod services;
|
||||
|
||||
struct AppState {
|
||||
database: ::sqlx::postgres::PgPool
|
||||
database: ::sqlx::postgres::PgPool,
|
||||
trip_tracking_service: services::trip_tracking::TripTrackingService
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -30,11 +34,13 @@ pub struct TimetableStopRow {
|
|||
pub times: Vec<Option<i64>>, // one per trip, None if trip doesn't stop
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TimetableDirection {
|
||||
pub direction: Direction,
|
||||
pub trip_ids: Vec<String>, // column headers
|
||||
pub rows: Vec<TimetableStopRow>, // one per unique stop
|
||||
pub tracking_data: Vec<TripTracking>,
|
||||
pub rows: Vec<TimetableStopRow>
|
||||
}
|
||||
|
||||
pub fn build_timetables(
|
||||
|
|
@ -48,20 +54,26 @@ pub fn build_timetables(
|
|||
.iter()
|
||||
.filter(|trip| trip.direction_id == direction.direction_id)
|
||||
.collect();
|
||||
direction_trips.sort_by_key(|trip| {
|
||||
trip.schedule
|
||||
.iter()
|
||||
.filter_map(|s| Some(s.arrival_time))
|
||||
.min()
|
||||
.unwrap_or(i64::MAX)
|
||||
});
|
||||
|
||||
direction_trips.sort_by_key(|trip| {
|
||||
trip.schedule
|
||||
.iter()
|
||||
.filter_map(|s| Some(s.arrival_time))
|
||||
.min()
|
||||
.unwrap_or(i64::MAX)
|
||||
});
|
||||
|
||||
let trip_ids: Vec<String> = direction_trips
|
||||
.iter()
|
||||
.map(|t| t.trip_id.clone())
|
||||
.collect();
|
||||
|
||||
// Map of stop_id -> (stop_sequence, Vec<Option<arrival_time>>)
|
||||
let live_trips: Vec<TripTracking> = direction_trips
|
||||
.iter()
|
||||
.map(|t| t.tracking_data.clone())
|
||||
.collect();
|
||||
|
||||
|
||||
let mut stop_map: BTreeMap<i64, (i64, String, Vec<Option<i64>>)> = BTreeMap::new();
|
||||
|
||||
for (trip_index, trip) in direction_trips.iter().enumerate() {
|
||||
|
|
@ -95,11 +107,17 @@ direction_trips.sort_by_key(|trip| {
|
|||
}
|
||||
});
|
||||
|
||||
assert!(trip_ids.len() == live_trips.len());
|
||||
for row in &rows {
|
||||
assert!(row.times.len() == live_trips.len());
|
||||
}
|
||||
|
||||
|
||||
results.push(TimetableDirection {
|
||||
direction: direction.clone(),
|
||||
direction: direction.clone(),
|
||||
trip_ids,
|
||||
rows,
|
||||
tracking_data: live_trips
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -138,12 +156,24 @@ struct ContentTemplate<T: askama::Template> {
|
|||
struct RouteTemplate {
|
||||
route: libseptastic::route::Route,
|
||||
directions: Vec<libseptastic::direction::Direction>,
|
||||
timetables: Vec<TimetableDirection>
|
||||
timetables: Vec<TimetableDirection>,
|
||||
filter_stops: Option<Vec<i64>>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RouteResponse {
|
||||
route: libseptastic::route::Route,
|
||||
directions: Vec<libseptastic::direction::Direction>,
|
||||
schedule: Vec<database::Trip>
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "routes.html")]
|
||||
struct RoutesTemplate {
|
||||
rr_routes: Vec<libseptastic::route::Route>,
|
||||
subway_routes: Vec<libseptastic::route::Route>,
|
||||
trolley_routes: Vec<libseptastic::route::Route>,
|
||||
bus_routes: Vec<libseptastic::route::Route>
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
|
|
@ -152,15 +182,37 @@ struct IndexTemplate {
|
|||
}
|
||||
|
||||
#[get("/routes")]
|
||||
async fn get_routes() -> impl Responder {
|
||||
async fn get_routes_html(state: Data<Arc<AppState>>) -> impl Responder {
|
||||
|
||||
let mut all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap();
|
||||
all_routes.sort_by(|x, y| {
|
||||
if let Ok(x_p) = x.id.parse::<u32>() {
|
||||
if let Ok(y_p) = y.id.parse::<u32>() {
|
||||
return if y_p > x_p { Ordering::Less } else { Ordering::Greater };
|
||||
}
|
||||
}
|
||||
|
||||
return if y.id > x.id { Ordering::Less } else { Ordering::Greater };
|
||||
});
|
||||
|
||||
HttpResponse::Ok().body(ContentTemplate {
|
||||
page_title: None,
|
||||
page_desc: None,
|
||||
content: RoutesTemplate {}
|
||||
content: RoutesTemplate {
|
||||
rr_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect(),
|
||||
subway_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect(),
|
||||
trolley_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(),
|
||||
bus_routes: all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(),
|
||||
}
|
||||
}.render().unwrap())
|
||||
}
|
||||
|
||||
#[get("/routes.json")]
|
||||
async fn get_routes_json(state: Data<Arc<AppState>>) -> impl Responder {
|
||||
let all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap();
|
||||
HttpResponse::Ok().json(all_routes)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn get_index() -> impl Responder {
|
||||
|
||||
|
|
@ -171,20 +223,34 @@ async fn get_index() -> impl Responder {
|
|||
}.render().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MyQueryParams {
|
||||
#[serde(default)] // Optional: handle missing parameters with a default value
|
||||
stops: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/route/{route_id}")]
|
||||
async fn get_route(state: Data<Arc<AppState>>, path: web::Path<(String)>) -> impl Responder {
|
||||
async fn get_route(state: Data<Arc<AppState>>, info: web::Query<MyQueryParams>, path: web::Path<String>) -> impl Responder {
|
||||
let mut fils: Option<Vec<i64>> = None;
|
||||
if let Some (stops_v) = info.stops.clone() {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for sid in stops_v.split(",") {
|
||||
items.push(sid.parse::<i64>().unwrap());
|
||||
}
|
||||
fils = Some(items);
|
||||
}
|
||||
let route_id = path.into_inner();
|
||||
let route_r = get_route_by_id(route_id.clone(), state.clone()).await;
|
||||
let directions = get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await.unwrap()).await.unwrap();
|
||||
let trips = get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await.unwrap();
|
||||
if let Ok(route) = route_r {
|
||||
let route_info_r = get_route_info(route_id, state).await;
|
||||
if let Ok(route_info) = route_info_r {
|
||||
HttpResponse::Ok().body(ContentTemplate {
|
||||
page_title: None,
|
||||
page_desc: None,
|
||||
content: RouteTemplate {
|
||||
route,
|
||||
directions: directions.clone(),
|
||||
timetables: build_timetables(directions.as_slice(), trips.as_slice())
|
||||
route: route_info.route,
|
||||
directions: route_info.directions.clone(),
|
||||
timetables: build_timetables(route_info.directions.as_slice(), route_info.schedule.as_slice()),
|
||||
filter_stops: fils.clone()
|
||||
}
|
||||
}.render().unwrap())
|
||||
} else {
|
||||
|
|
@ -192,19 +258,35 @@ async fn get_route(state: Data<Arc<AppState>>, path: web::Path<(String)>) -> imp
|
|||
}
|
||||
}
|
||||
|
||||
#[get("/api/route/{route_id}")]
|
||||
async fn api_get_route(state: Data<Arc<AppState>>, path: web::Path<(String)>) -> impl Responder {
|
||||
|
||||
async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> {
|
||||
let route = get_route_by_id(route_id.clone(), state.clone()).await?;
|
||||
let directions = get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
||||
let mut trips = get_schedule_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
||||
|
||||
state.trip_tracking_service.annotate_trips(&mut trips);
|
||||
|
||||
Ok(RouteResponse{
|
||||
route,
|
||||
directions,
|
||||
schedule: trips
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[get("/route/{route_id}.json")]
|
||||
async fn api_get_route(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
let route_id = path.into_inner();
|
||||
let route_r = get_route_by_id(route_id, state).await;
|
||||
if let Ok(route) = route_r {
|
||||
HttpResponse::Ok().json(route)
|
||||
let route_info_r = get_route_info(route_id, state).await;
|
||||
if let Ok(route_info) = route_info_r {
|
||||
HttpResponse::Ok().json(route_info)
|
||||
} else {
|
||||
HttpResponse::InternalServerError().body("Error")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/route/{route_id}/schedule")]
|
||||
async fn api_get_schedule(state: Data<Arc<AppState>>, path: web::Path<(String)>) -> impl Responder {
|
||||
async fn api_get_schedule(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
let route_id = path.into_inner();
|
||||
let route_r = get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await;
|
||||
if let Ok(route) = route_r {
|
||||
|
|
@ -215,7 +297,7 @@ async fn api_get_schedule(state: Data<Arc<AppState>>, path: web::Path<(String)>)
|
|||
}
|
||||
|
||||
#[get("/api/stop/{stop_id}/nta")]
|
||||
async fn api_get_nta(state: Data<Arc<AppState>>, path: web::Path<(String)>) -> impl Responder {
|
||||
async fn api_get_nta(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
let route_id = path.into_inner().split(',') .map(|s| s.parse::<i64>())
|
||||
.collect::<Result<Vec<i64>, _>>().unwrap();
|
||||
let route_r = get_nta_by_stop_id(route_id, chrono::Utc::now(), chrono::Utc::now() + TimeDelta::minutes(30), &mut state.database.begin().await.unwrap()).await;
|
||||
|
|
@ -244,10 +326,15 @@ async fn main() -> ::anyhow::Result<()> {
|
|||
.connect(&connection_string)
|
||||
.await?;
|
||||
|
||||
let tt_service = trip_tracking::TripTrackingService::new();
|
||||
tt_service.start();
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
database: pool
|
||||
database: pool,
|
||||
trip_tracking_service: tt_service
|
||||
});
|
||||
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(actix_cors::Cors::permissive())
|
||||
|
|
@ -256,7 +343,8 @@ async fn main() -> ::anyhow::Result<()> {
|
|||
.service(api_get_schedule)
|
||||
.service(api_get_nta)
|
||||
.service(get_route)
|
||||
.service(get_routes)
|
||||
.service(get_routes_json)
|
||||
.service(get_routes_html)
|
||||
.service(get_index)
|
||||
.service(actix_files::Files::new("/assets", "./assets"))
|
||||
})
|
||||
|
|
|
|||
1
api/src/services/mod.rs
Normal file
1
api/src/services/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod trip_tracking;
|
||||
161
api/src/services/trip_tracking.rs
Normal file
161
api/src/services/trip_tracking.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
use serde_json::Value;
|
||||
use serde::de;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use log::{error, info};
|
||||
use serde::{Serialize, Deserialize, Deserializer};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TripTracking {
|
||||
Tracked(LiveTrip),
|
||||
Untracked,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LiveTrip {
|
||||
pub delay: f64,
|
||||
pub next_stop_id: Option<String>,
|
||||
pub timestamp: i64,
|
||||
pub vehicle_id: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LiveTripJson {
|
||||
pub route_id: String,
|
||||
pub trip_id: String,
|
||||
pub service_id: Option<String>,
|
||||
pub trip_headsign: String,
|
||||
pub direction_id: i64,
|
||||
#[serde(deserialize_with = "de_numstr")]
|
||||
pub block_id: String,
|
||||
pub start_time: Option<String>,
|
||||
pub end_time: Option<String>,
|
||||
pub delay: f64,
|
||||
pub status: String,
|
||||
pub lat: Option<String>,
|
||||
pub lon: Option<String>,
|
||||
#[serde(deserialize_with = "de_numstrflo")]
|
||||
pub heading: Option<String>,
|
||||
#[serde(deserialize_with = "de_numstro")]
|
||||
pub next_stop_id: Option<String>,
|
||||
pub next_stop_name: Option<String>,
|
||||
pub next_stop_sequence: Option<i64>,
|
||||
pub seat_availability: Option<String>,
|
||||
pub vehicle_id: Option<String>,
|
||||
pub timestamp: i64
|
||||
}
|
||||
|
||||
const HOST: &str = "https://www3.septa.org";
|
||||
|
||||
struct TripTrackingServiceState {
|
||||
pub tracking_data: HashMap::<String, TripTracking>
|
||||
}
|
||||
|
||||
pub struct TripTrackingService {
|
||||
state: Arc<Mutex<TripTrackingServiceState>>
|
||||
}
|
||||
|
||||
impl TripTrackingService {
|
||||
const UPDATE_SECONDS: u64 = 75;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(TripTrackingServiceState{ tracking_data: HashMap::new()}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
let cloned_state = Arc::clone(&self.state);
|
||||
thread::spawn( move || {
|
||||
loop {
|
||||
info!("started");
|
||||
|
||||
let clonedx_state = Arc::clone(&cloned_state);
|
||||
let res = Self::update_live_trips(clonedx_state);
|
||||
|
||||
match res {
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(Self::UPDATE_SECONDS));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn annotate_trips(&self, trips: &mut Vec<crate::database::Trip>) {
|
||||
for trip in trips {
|
||||
trip.tracking_data = match self.state.lock().unwrap().tracking_data.get(&trip.trip_id.clone()){
|
||||
Some(x) => x.clone(),
|
||||
None => TripTracking::Untracked
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn update_live_trips(service: Arc<Mutex<TripTrackingServiceState>>) -> anyhow::Result<()> {
|
||||
let mut new_map: HashMap<String, TripTracking> = HashMap::new();
|
||||
let live_tracks = reqwest::blocking::get(format!("{}/api/v2/trips/", HOST))?.json::<Vec<LiveTripJson>>()?;
|
||||
|
||||
for live_track in live_tracks {
|
||||
let track: TripTracking = {
|
||||
if live_track.status == "NO GPS" {
|
||||
TripTracking::Untracked
|
||||
} else if live_track.status == "CANCELED" {
|
||||
TripTracking::Cancelled
|
||||
} else {
|
||||
TripTracking::Tracked(
|
||||
LiveTrip {
|
||||
delay: live_track.delay,
|
||||
next_stop_id: live_track.next_stop_id,
|
||||
timestamp: live_track.timestamp,
|
||||
vehicle_id: live_track.vehicle_id
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let TripTracking::Cancelled = track {
|
||||
}
|
||||
|
||||
new_map.insert(
|
||||
live_track.trip_id.clone(),
|
||||
track
|
||||
);
|
||||
}
|
||||
|
||||
info!("populated tracking data with {} entries", new_map.len());
|
||||
(service.lock().unwrap()).tracking_data = new_map;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn de_numstr<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
|
||||
Ok(match Value::deserialize(deserializer)? {
|
||||
Value::String(s) => s,
|
||||
Value::Number(num) => num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string(),
|
||||
_ => return Err(de::Error::custom("wrong type"))
|
||||
})
|
||||
}
|
||||
|
||||
fn de_numstro<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
|
||||
Ok(match Value::deserialize(deserializer)? {
|
||||
Value::String(s) => Some(s),
|
||||
Value::Number(num) => Some(num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string()),
|
||||
_ => None
|
||||
})
|
||||
}
|
||||
|
||||
fn de_numstrflo<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
|
||||
Ok(match Value::deserialize(deserializer)? {
|
||||
Value::String(s) => Some(s),
|
||||
Value::Number(num) => Some(num.as_f64().ok_or(de::Error::custom("Invalid number"))?.to_string()),
|
||||
_ => None
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +27,10 @@
|
|||
</style>
|
||||
<body>
|
||||
<div class="body">
|
||||
<div style="background-color: #ff0000; color: #ffffff; font-size: .7em; padding: 5px; margin-bottom: 10px; margin-top: 10px;">
|
||||
This website is not run by SEPTA. Data may be inaccurate.
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -68,18 +68,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
});
|
||||
</script>
|
||||
|
||||
<div style="background-color: #ff0000; color: #ffffff; padding: 15px; margin-bottom: 15px; margin-top: 15px;">
|
||||
This website is not run by SEPTA. As such, schedules may not be
|
||||
completely accurate.
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center;">
|
||||
{% call scope::route_symbol(route) %}
|
||||
<h1 style="margin-left: 15px;">{{ route.name }}</h1>
|
||||
</div>
|
||||
|
||||
{% for timetable in timetables %}
|
||||
<h2>{{ timetable.direction.direction | capitalize }} to {{ timetable.direction.direction_destination }}</h2>
|
||||
<details style="margin-top: 15px;">
|
||||
<summary>
|
||||
<div style="display: inline-block;">
|
||||
<h3>{{ timetable.direction.direction | capitalize }} to</h3>
|
||||
<h2>{{ timetable.direction.direction_destination }}</h2>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="tscroll">
|
||||
<table class="train-direction-table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
|
|
@ -92,20 +93,37 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for row in timetable.rows %}
|
||||
{% if let Some(filter_stop_v) = filter_stops %}
|
||||
{% if !filter_stop_v.contains(&row.stop_id) %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ row.stop_name }}</td>
|
||||
{% for time in row.times %}
|
||||
<td>
|
||||
|
||||
|
||||
{% if let Some(t) = time %}
|
||||
{{ t | format_time }}
|
||||
{% let live_o = timetable.tracking_data[loop.index0] %}
|
||||
{% if let Tracked(live) = live_o %}
|
||||
{% let time = (t + (live.delay * 60.0) as i64) %}
|
||||
<td style="background-color: #003300">
|
||||
<span style="color: #22bb22"> {{ time | format_time }} </span>
|
||||
</td>
|
||||
{% elif let TripTracking::Cancelled = live_o %}
|
||||
<td style="color: #ff0000"><s>{{ t | format_time }}</s></td>
|
||||
{% else %}
|
||||
<td>{{ t | format_time }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
<td>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -5,67 +5,44 @@
|
|||
<fieldset>
|
||||
<legend><h2>Regional Rail</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For infrequent rail service to suburban locations</p>
|
||||
<!--<h3 class="line-link">[ Pennsy ]</h3>-->
|
||||
<p class="line-link"><a href="/route/TRE">[ <b>TRE:</b> Trenton (NJT to New York) ]</a></p>
|
||||
<p class="line-link"><a href="/route/PAO">[ <b>PAO:</b> Paoli/Thorndale ]</a></p>
|
||||
<p class="line-link"><a href="/route/CYN">[ <b>CYN:</b> Cynwyd ]</a></p>
|
||||
<p class="line-link"><a href="/route/AIR">[ <b>AIR:</b> Airport ]</a></p>
|
||||
<p class="line-link"><a href="/route/WIL">[ <b>WIL:</b> Wilmington/Newark ]</a></p>
|
||||
<p class="line-link"><a href="/route/CHW">[ <b>CHW:</b> Chestnut Hill West ]</a></p>
|
||||
<p class="line-link"><a href="/route/WAW">[ <b>WAW:</b> Media/Wawa ]</a></p>
|
||||
|
||||
<!--<h3 class="line-link">[ Reading ]</h3>-->
|
||||
<p class="line-link"><a href="/route/LAN">[ <b>LAN:</b> Lansdale/Doylestown ]</a></p>
|
||||
<p class="line-link"><a href="/route/NOR">[ <b>NOR:</b> Manayunk/Norristown ]</a></p>
|
||||
<p class="line-link"><a href="/route/CHE">[ <b>CHE:</b> Chestnut Hill East ]</a></p>
|
||||
<p class="line-link"><a href="/route/FOX">[ <b>FOX:</b> Fox Chase ]</a></p>
|
||||
<p class="line-link"><a href="/route/WTF">[ <b>WTR:</b> West Trenton ]</a></p>
|
||||
<p class="line-link"><a href="/route/WAR">[ <b>WAR:</b> Warminster ]</a></p>
|
||||
{% for route in rr_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><h2>Metro</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For frequent rail service within Philadelphia and suburban locations</p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Subway/Elevated ]</p>
|
||||
<p class="line-link"><a href="/route/B1">[ <b>B1:</b> Broad Street Line Local ]</a></p>
|
||||
<p class="line-link"><a href="/route/B2">[ <b>B2:</b> Broad Street Line Express ]</a></p>
|
||||
<p class="line-link"><a href="/route/B3">[ <b>B3:</b> Broad Ridge Spur ]</a></p>
|
||||
<p class="line-link"><a href="/route/L1">[ <b>L1:</b> Market-Frankford Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/S1">[ <b>S1:</b> Norristown-Airport Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/S2">[ <b>S2:</b> Media-Chestnut Hill Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/S3">[ <b>S3:</b> Paoli-Fox Chase Line ]</a></p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Urban Trolley ]</p>
|
||||
<p class="line-link"><a href="/route/T1">[ <b>T1:</b> Lancaster Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T2">[ <b>T2:</b> Baltimore Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T3">[ <b>T3:</b> Chester Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T4">[ <b>T4:</b> Woodland Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T5">[ <b>T5:</b> Elmwood Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/G1">[ <b>G1:</b> Girard Avenue Trolley ]</a></p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Suburban Trolley ]</p>
|
||||
<p class="line-link"><a href="/route/D1">[ <b>D1:</b> Media Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/D2">[ <b>D2:</b> Sharon Hill Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/M1">[ <b>M1:</b> Norristown High Speed Line ]</a></p>
|
||||
<div class="lines-label" style="font-weight: bold; width: 100%; display: flex; justify-content: space-between;">
|
||||
<p>[ Subway/Elevated </p><p>]</p>
|
||||
</div>
|
||||
{% for route in subway_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<div class="lines-label" style="font-weight: bold; width: 100%; display: flex; justify-content: space-between;">
|
||||
<p>[ Trolleys </p><p>]</p>
|
||||
</div>
|
||||
|
||||
{% for route in trolley_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><h2>Bus</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For service of varying frequency within SEPTA's entire service area</p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Subway/Elevated ]</p>
|
||||
<p class="line-link"><a href="/route/B1">[ <b>B1:</b> Broad Street Line Local ]</a></p>
|
||||
<p class="line-link"><a href="/route/B2">[ <b>B2:</b> Broad Street Line Express ]</a></p>
|
||||
<p class="line-link"><a href="/route/B3">[ <b>B3:</b> Broad Ridge Spur ]</a></p>
|
||||
<p class="line-link"><a href="/route/L1">[ <b>L1:</b> Market-Frankford Line ]</a></p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Urban Trolley ]</p>
|
||||
<p class="line-link"><a href="/route/T1">[ <b>T1:</b> Lancaster Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T2">[ <b>T2:</b> Baltimore Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T3">[ <b>T3:</b> Chester Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T4">[ <b>T4:</b> Woodland Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/T5">[ <b>T5:</b> Elmwood Avenue Trolley ]</a></p>
|
||||
<p class="line-link"><a href="/route/G1">[ <b>G1:</b> Girard Avenue Trolley ]</a></p>
|
||||
<p class="lines-label" style="font-weight: bold;">[ Suburban Trolley ]</p>
|
||||
<p class="line-link"><a href="/route/D1">[ <b>D1:</b> Media Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/D2">[ <b>D2:</b> Sharon Hill Line ]</a></p>
|
||||
<p class="line-link"><a href="/route/M1">[ <b>M1:</b> Norristown High Speed Line ]</a></p>
|
||||
{% for route in bus_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue