live tracking and filter
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled

This commit is contained in:
Nicholas Orlowsky 2025-09-19 21:38:57 -04:00
parent 534c36b0f7
commit f5e0a31bb7
No known key found for this signature in database
GPG key ID: A9F3BA4C0AA7A70B
16 changed files with 414 additions and 115 deletions

View file

@ -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"))
})