clean up + update dfile
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 6m0s
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 6m0s
This commit is contained in:
parent
7904c80642
commit
8fd41b1090
8 changed files with 314 additions and 335 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
FROM rust:1.90-bullseye AS build
|
FROM rust:1.90-slim-trixie AS build
|
||||||
|
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
ENV SCCACHE_DIR=/build-cache
|
||||||
|
ENV RUSTC_WRAPPER=sccache
|
||||||
|
|
||||||
WORKDIR .
|
WORKDIR .
|
||||||
COPY ./api ./api
|
COPY ./api ./api
|
||||||
|
|
@ -9,9 +11,10 @@ COPY ./api/assets ./assets
|
||||||
COPY ./api/templates ./templates
|
COPY ./api/templates ./templates
|
||||||
|
|
||||||
|
|
||||||
|
RUN apt -y update && apt install -y libssl-dev libc-dev sccache build-essential pkg-config
|
||||||
RUN cd api && cargo build --release
|
RUN cd api && cargo build --release
|
||||||
|
|
||||||
FROM debian:bullseye-slim
|
FROM debian:trixie-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
@ -19,7 +22,7 @@ COPY --from=build /api/target/release/septastic_api /app/septastic_api
|
||||||
COPY api/assets /app/assets
|
COPY api/assets /app/assets
|
||||||
COPY api/templates /app/templates
|
COPY api/templates /app/templates
|
||||||
|
|
||||||
RUN apt update && apt install curl
|
RUN apt -y update && apt install -y curl
|
||||||
|
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
ENV EXPOSE_PORT=8080
|
ENV EXPOSE_PORT=8080
|
||||||
|
|
|
||||||
1
api/src/controllers/mod.rs
Normal file
1
api/src/controllers/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod route;
|
||||||
126
api/src/controllers/route.rs
Normal file
126
api/src/controllers/route.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
use actix_web::{get, web::{Data, self}, HttpResponse, Responder};
|
||||||
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
|
use libseptastic::{route::RouteType, stop_schedule::Trip};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::database;
|
||||||
|
|
||||||
|
#[get("/routes")]
|
||||||
|
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(crate::templates::ContentTemplate {
|
||||||
|
page_title: None,
|
||||||
|
page_desc: None,
|
||||||
|
content: crate::templates::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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct MyQueryParams {
|
||||||
|
#[serde(default)] // Optional: handle missing parameters with a default value
|
||||||
|
stops: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_route_by_id(id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<libseptastic::route::Route> {
|
||||||
|
Ok(database::get_route_by_id(id, &mut state.database.begin().await?).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RouteResponse {
|
||||||
|
pub route: libseptastic::route::Route,
|
||||||
|
pub directions: Vec<libseptastic::direction::Direction>,
|
||||||
|
pub schedule: Vec<Trip>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = database::get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
||||||
|
let mut trips = database::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}")]
|
||||||
|
async fn get_route(state: Data<Arc<AppState>>, info: web::Query<MyQueryParams>, path: web::Path<String>) -> impl Responder {
|
||||||
|
let mut filters: 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());
|
||||||
|
}
|
||||||
|
filters = Some(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
let route_id = path.into_inner();
|
||||||
|
let route_info_r = get_route_info(route_id, state).await;
|
||||||
|
if let Ok(route_info) = route_info_r {
|
||||||
|
HttpResponse::Ok().body(crate::templates::ContentTemplate {
|
||||||
|
page_title: None,
|
||||||
|
page_desc: None,
|
||||||
|
content: crate::templates::RouteTemplate {
|
||||||
|
route: route_info.route,
|
||||||
|
directions: route_info.directions.clone(),
|
||||||
|
timetables: crate::templates::build_timetables(route_info.directions.as_slice(), route_info.schedule.as_slice()),
|
||||||
|
filter_stops: filters.clone()
|
||||||
|
}
|
||||||
|
}.render().unwrap())
|
||||||
|
} else {
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_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 {
|
||||||
|
let route_id = path.into_inner();
|
||||||
|
let route_r = database::get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await;
|
||||||
|
|
||||||
|
if let Ok(route) = route_r {
|
||||||
|
HttpResponse::Ok().json(route)
|
||||||
|
} else {
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -2,8 +2,7 @@ use std::collections::HashMap;
|
||||||
use libseptastic::{direction::CardinalDirection, route::RouteType};
|
use libseptastic::{direction::CardinalDirection, route::RouteType};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Postgres, Transaction};
|
use sqlx::{Postgres, Transaction};
|
||||||
|
use libseptastic::{stop_schedule::{Trip, TripTracking, StopSchedule}};
|
||||||
use crate::services::trip_tracking::TripTracking;
|
|
||||||
|
|
||||||
pub async fn get_route_by_id(
|
pub async fn get_route_by_id(
|
||||||
id: String,
|
id: String,
|
||||||
|
|
@ -104,29 +103,6 @@ pub async fn get_direction_by_route_id(
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct StopSchedule {
|
|
||||||
pub route_id: String,
|
|
||||||
pub stop_name: String,
|
|
||||||
pub trip_id: String,
|
|
||||||
pub service_id: String,
|
|
||||||
pub direction_id: i64,
|
|
||||||
pub arrival_time: i64,
|
|
||||||
pub stop_id: i64,
|
|
||||||
pub stop_sequence: i64
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
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(
|
pub async fn get_schedule_by_route_id(
|
||||||
id: String,
|
id: String,
|
||||||
transaction: &mut Transaction<'_, Postgres>,
|
transaction: &mut Transaction<'_, Postgres>,
|
||||||
|
|
|
||||||
295
api/src/main.rs
295
api/src/main.rs
|
|
@ -1,301 +1,32 @@
|
||||||
use actix_web::{get, web::{self, Data}, App, HttpResponse, HttpServer, Responder};
|
use actix_web::{get, web::{self, Data}, App, HttpResponse, HttpServer, Responder};
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use database::{get_direction_by_route_id, get_nta_by_stop_id, get_schedule_by_route_id};
|
use database::get_nta_by_stop_id;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use libseptastic::{direction::Direction, route::RouteType};
|
|
||||||
use database::{Trip};
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use services::trip_tracking::{self, TripTracking};
|
use services::trip_tracking::{self};
|
||||||
use std::{cmp::Ordering, collections::BTreeMap, sync::Arc};
|
use std::sync::Arc;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use crate::TripTracking::Tracked;
|
|
||||||
mod database;
|
mod database;
|
||||||
mod services;
|
mod services;
|
||||||
|
mod controllers;
|
||||||
|
mod templates;
|
||||||
|
|
||||||
struct AppState {
|
pub struct AppState {
|
||||||
database: ::sqlx::postgres::PgPool,
|
database: ::sqlx::postgres::PgPool,
|
||||||
trip_tracking_service: services::trip_tracking::TripTrackingService
|
trip_tracking_service: services::trip_tracking::TripTrackingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_route_by_id(id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<libseptastic::route::Route> {
|
|
||||||
Ok(database::get_route_by_id(id, &mut state.database.begin().await?).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct TimetableStopRow {
|
|
||||||
pub stop_id: i64,
|
|
||||||
pub stop_name: String,
|
|
||||||
pub stop_sequence: i64,
|
|
||||||
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 tracking_data: Vec<TripTracking>,
|
|
||||||
pub rows: Vec<TimetableStopRow>
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_timetables(
|
|
||||||
directions: &[Direction],
|
|
||||||
trips: &[Trip],
|
|
||||||
) -> Vec<TimetableDirection> {
|
|
||||||
let mut results = Vec::new();
|
|
||||||
|
|
||||||
for direction in directions {
|
|
||||||
let mut direction_trips: Vec<&Trip> = trips
|
|
||||||
.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)
|
|
||||||
});
|
|
||||||
|
|
||||||
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<i64, (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)
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(trip_ids.len() == live_trips.len());
|
|
||||||
for row in &rows {
|
|
||||||
assert!(row.times.len() == live_trips.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
results.push(TimetableDirection {
|
|
||||||
direction: direction.clone(),
|
|
||||||
trip_ids,
|
|
||||||
rows,
|
|
||||||
tracking_data: live_trips
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
mod filters {
|
|
||||||
pub fn format_time(
|
|
||||||
seconds: &i64,
|
|
||||||
_: &dyn askama::Values,
|
|
||||||
) -> askama::Result<String> {
|
|
||||||
let total_minutes = seconds / 60;
|
|
||||||
let (hours, ampm) = {
|
|
||||||
let hrs = total_minutes / 60;
|
|
||||||
if hrs > 12 {
|
|
||||||
(hrs - 12, "PM")
|
|
||||||
} else {
|
|
||||||
(hrs, "AM")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let minutes = total_minutes % 60;
|
|
||||||
Ok(format!("{}:{:02} {}", hours, minutes, ampm))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "layout.html")]
|
|
||||||
struct ContentTemplate<T: askama::Template> {
|
|
||||||
content: T,
|
|
||||||
page_title: Option<String>,
|
|
||||||
page_desc: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "route.html")]
|
|
||||||
struct RouteTemplate {
|
|
||||||
route: libseptastic::route::Route,
|
|
||||||
directions: Vec<libseptastic::direction::Direction>,
|
|
||||||
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)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
struct IndexTemplate {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/routes")]
|
|
||||||
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 {
|
|
||||||
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("/")]
|
#[get("/")]
|
||||||
async fn get_index() -> impl Responder {
|
async fn get_index() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body(templates::ContentTemplate {
|
||||||
HttpResponse::Ok().body(ContentTemplate {
|
|
||||||
page_title: None,
|
page_title: None,
|
||||||
page_desc: None,
|
page_desc: None,
|
||||||
content: IndexTemplate {}
|
content: templates::IndexTemplate {}
|
||||||
}.render().unwrap())
|
}.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>>, 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_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: 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 {
|
|
||||||
HttpResponse::InternalServerError().body("Error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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_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 {
|
|
||||||
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 {
|
|
||||||
HttpResponse::Ok().json(route)
|
|
||||||
} else {
|
|
||||||
HttpResponse::InternalServerError().body("Error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/api/stop/{stop_id}/nta")]
|
#[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>())
|
let route_id = path.into_inner().split(',') .map(|s| s.parse::<i64>())
|
||||||
|
|
@ -339,12 +70,12 @@ async fn main() -> ::anyhow::Result<()> {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(actix_cors::Cors::permissive())
|
.wrap(actix_cors::Cors::permissive())
|
||||||
.app_data(Data::new(state.clone()))
|
.app_data(Data::new(state.clone()))
|
||||||
.service(api_get_route)
|
|
||||||
.service(api_get_schedule)
|
|
||||||
.service(api_get_nta)
|
.service(api_get_nta)
|
||||||
.service(get_route)
|
.service(controllers::route::api_get_route)
|
||||||
.service(get_routes_json)
|
.service(controllers::route::api_get_schedule)
|
||||||
.service(get_routes_html)
|
.service(controllers::route::get_route)
|
||||||
|
.service(controllers::route::get_routes_json)
|
||||||
|
.service(controllers::route::get_routes_html)
|
||||||
.service(get_index)
|
.service(get_index)
|
||||||
.service(actix_files::Files::new("/assets", "./assets"))
|
.service(actix_files::Files::new("/assets", "./assets"))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,7 @@ use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use serde::{Serialize, Deserialize, Deserializer};
|
use serde::{Serialize, Deserialize, Deserializer};
|
||||||
|
use libseptastic::stop_schedule::{LiveTrip, TripTracking};
|
||||||
#[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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LiveTripJson {
|
pub struct LiveTripJson {
|
||||||
|
|
@ -71,8 +57,6 @@ impl TripTrackingService {
|
||||||
let cloned_state = Arc::clone(&self.state);
|
let cloned_state = Arc::clone(&self.state);
|
||||||
thread::spawn( move || {
|
thread::spawn( move || {
|
||||||
loop {
|
loop {
|
||||||
info!("started");
|
|
||||||
|
|
||||||
let clonedx_state = Arc::clone(&cloned_state);
|
let clonedx_state = Arc::clone(&cloned_state);
|
||||||
let res = Self::update_live_trips(clonedx_state);
|
let res = Self::update_live_trips(clonedx_state);
|
||||||
|
|
||||||
|
|
@ -88,7 +72,7 @@ impl TripTrackingService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn annotate_trips(&self, trips: &mut Vec<crate::database::Trip>) {
|
pub fn annotate_trips(&self, trips: &mut Vec<libseptastic::stop_schedule::Trip>) {
|
||||||
for trip in trips {
|
for trip in trips {
|
||||||
trip.tracking_data = match self.state.lock().unwrap().tracking_data.get(&trip.trip_id.clone()){
|
trip.tracking_data = match self.state.lock().unwrap().tracking_data.get(&trip.trip_id.clone()){
|
||||||
Some(x) => x.clone(),
|
Some(x) => x.clone(),
|
||||||
|
|
@ -128,7 +112,6 @@ impl TripTrackingService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("populated tracking data with {} entries", new_map.len());
|
|
||||||
(service.lock().unwrap()).tracking_data = new_map;
|
(service.lock().unwrap()).tracking_data = new_map;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
152
api/src/templates.rs
Normal file
152
api/src/templates.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
use libseptastic::{direction::Direction, stop_schedule::{Trip, TripTracking}};
|
||||||
|
use std::{cmp::Ordering, collections::BTreeMap};
|
||||||
|
use serde::{Serialize};
|
||||||
|
use libseptastic::stop_schedule::TripTracking::Tracked;
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "route.html")]
|
||||||
|
pub struct RouteTemplate {
|
||||||
|
pub route: libseptastic::route::Route,
|
||||||
|
pub directions: Vec<libseptastic::direction::Direction>,
|
||||||
|
pub timetables: Vec<TimetableDirection>,
|
||||||
|
pub filter_stops: Option<Vec<i64>>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = "index.html")]
|
||||||
|
pub struct IndexTemplate {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct TimetableStopRow {
|
||||||
|
pub stop_id: i64,
|
||||||
|
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 fn build_timetables(
|
||||||
|
directions: &[Direction],
|
||||||
|
trips: &[Trip],
|
||||||
|
) -> Vec<TimetableDirection> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for direction in directions {
|
||||||
|
let mut direction_trips: Vec<&Trip> = trips
|
||||||
|
.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)
|
||||||
|
});
|
||||||
|
|
||||||
|
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<i64, (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)
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(trip_ids.len() == live_trips.len());
|
||||||
|
for row in &rows {
|
||||||
|
assert!(row.times.len() == live_trips.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
results.push(TimetableDirection {
|
||||||
|
direction: direction.clone(),
|
||||||
|
trip_ids,
|
||||||
|
rows,
|
||||||
|
tracking_data: live_trips
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
mod filters {
|
||||||
|
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 {
|
||||||
|
(hrs, "AM")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let minutes = total_minutes % 60;
|
||||||
|
Ok(format!("{}:{:02} {}", hours, minutes, ampm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StopSchedule {
|
pub struct StopSchedule {
|
||||||
pub route_id: String,
|
pub route_id: String,
|
||||||
|
pub stop_name: String,
|
||||||
pub trip_id: String,
|
pub trip_id: String,
|
||||||
pub service_id: String,
|
pub service_id: String,
|
||||||
pub direction_id: i64,
|
pub direction_id: i64,
|
||||||
|
|
@ -16,15 +17,21 @@ pub struct Trip {
|
||||||
pub route_id: String,
|
pub route_id: String,
|
||||||
pub trip_id: String,
|
pub trip_id: String,
|
||||||
pub direction_id: i64,
|
pub direction_id: i64,
|
||||||
pub live_trip_data: Option<LiveTrip>,
|
pub tracking_data: TripTracking,
|
||||||
pub schedule: Vec<StopSchedule>
|
pub schedule: Vec<StopSchedule>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LiveTrip {
|
pub enum TripTracking {
|
||||||
pub delay: i64,
|
Tracked(LiveTrip),
|
||||||
pub next_stop_id: i64,
|
Untracked,
|
||||||
pub timestamp: i64,
|
Cancelled
|
||||||
pub vehicle_id: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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>
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue