add preliminary nta support
This commit is contained in:
parent
a7d323056a
commit
1d66553398
21 changed files with 3318 additions and 257 deletions
19
.direnv/bin/nix-direnv-reload
Executable file
19
.direnv/bin/nix-direnv-reload
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ ! -d "/home/nickorlow/programming/septastic/api" ]]; then
|
||||||
|
echo "Cannot find source directory; Did you move it?"
|
||||||
|
echo "(Looking for "/home/nickorlow/programming/septastic/api")"
|
||||||
|
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rebuild the cache forcefully
|
||||||
|
_nix_direnv_force_reload=1 direnv exec "/home/nickorlow/programming/septastic/api" true
|
||||||
|
|
||||||
|
# Update the mtime for .envrc.
|
||||||
|
# This will cause direnv to reload again - but without re-building.
|
||||||
|
touch "/home/nickorlow/programming/septastic/api/.envrc"
|
||||||
|
|
||||||
|
# Also update the timestamp of whatever profile_rc we have.
|
||||||
|
# This makes sure that we know we are up to date.
|
||||||
|
touch -r "/home/nickorlow/programming/septastic/api/.envrc" "/home/nickorlow/programming/septastic/api/.direnv"/*.rc
|
||||||
1
.direnv/nix-profile-25.11-sizirny50f893gx0
Symbolic link
1
.direnv/nix-profile-25.11-sizirny50f893gx0
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/jyg5pzxlxkbvzy1wb808kc5idmbij4r6-env-env
|
||||||
2127
.direnv/nix-profile-25.11-sizirny50f893gx0.rc
Normal file
2127
.direnv/nix-profile-25.11-sizirny50f893gx0.rc
Normal file
File diff suppressed because it is too large
Load diff
18
api/Cargo.lock
generated
18
api/Cargo.lock
generated
|
|
@ -311,12 +311,6 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android-tzdata"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -646,16 +640,16 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link 0.1.3",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1606,7 +1600,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.5.10",
|
"socket2 0.6.1",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
|
@ -1908,6 +1902,8 @@ dependencies = [
|
||||||
name = "libseptastic"
|
name = "libseptastic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ img {
|
||||||
.rr-container {
|
.rr-container {
|
||||||
background-color: #4c748c;
|
background-color: #4c748c;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 1.5em;
|
font-size: 1.2em;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -81,6 +81,35 @@ img {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.train-direction-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: mono;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.train-direction-table th,
|
||||||
|
.train-direction-table td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.train-direction-table th {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-row td,
|
||||||
|
.highlight-row th {
|
||||||
|
background-color: #d0ebff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-col {
|
||||||
|
background-color: #d0ebff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.bg-B1, .bg-B2, .bg-B3 {
|
.bg-B1, .bg-B2, .bg-B3 {
|
||||||
background-color: #FF661C;
|
background-color: #FF661C;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|
@ -121,6 +150,8 @@ img {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
aspect-ratio: 3/2;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.tscroll {
|
.tscroll {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,38 @@
|
||||||
gtfs_zips:
|
gtfs_zips:
|
||||||
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
||||||
subzip: "google_rail.zip"
|
subzip: "google_rail.zip"
|
||||||
|
prefix: "SEPTARAIL"
|
||||||
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
||||||
|
prefix: "SEPTABUS"
|
||||||
subzip: "google_bus.zip"
|
subzip: "google_bus.zip"
|
||||||
# - uri: "https://www.njtransit.com/rail_data.zip"
|
# - uri: "https://www.njtransit.com/rail_data.zip"
|
||||||
# - uri: "https://www.njtransit.com/bus_data.zip"
|
# - uri: "https://www.njtransit.com/bus_data.zip"
|
||||||
annotations:
|
annotations:
|
||||||
|
multiplatform_stops:
|
||||||
|
- id: 'WTC'
|
||||||
|
name: 'Wissahickon Transit Center'
|
||||||
|
platform_station_ids:
|
||||||
|
- 'SEPTABUS_2'
|
||||||
|
- 'SEPTABUS_31032'
|
||||||
|
- 'SEPTABUS_32980'
|
||||||
|
- 'SEPTABUS_32988'
|
||||||
|
- 'SEPTABUS_32989'
|
||||||
|
- 'SEPTABUS_32990'
|
||||||
|
- 'SEPTABUS_32992'
|
||||||
|
- 'SEPTABUS_32993'
|
||||||
|
- id: 'STC'
|
||||||
|
name: "Susquehanna Transit Center"
|
||||||
|
platform_station_ids:
|
||||||
|
- 'SEPTABUS_740'
|
||||||
|
- 'SEPTABUS_703'
|
||||||
|
- 'SEPTABUS_699'
|
||||||
|
- 'SEPTABUS_22302'
|
||||||
|
- id: 'CCC'
|
||||||
|
name: "Center City Combined"
|
||||||
|
platform_station_ids:
|
||||||
|
- 'SEPTABUS_3057'
|
||||||
|
- 'SEPTABUS_2687'
|
||||||
|
- 'SEPTABUS_18451'
|
||||||
|
- 'SEPTABUS_17170'
|
||||||
synthetic_routes:
|
synthetic_routes:
|
||||||
- id: 'NYC'
|
- id: 'NYC'
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub mod route;
|
pub mod route;
|
||||||
|
pub mod stop;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use log::info;
|
|
||||||
use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant};
|
use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant};
|
||||||
use libseptastic::{direction, route::RouteType, stop_schedule::Trip};
|
use libseptastic::{direction, route::RouteType, stop_schedule::Trip};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
//use crate::{routing::{bfs_rts, construct_graph, get_stops_near}, AppState};
|
||||||
|
//use crate::routing;
|
||||||
|
|
||||||
#[get("/routes")]
|
#[get("/routes")]
|
||||||
async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
|
async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
|
||||||
|
|
@ -21,7 +22,7 @@ async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl R
|
||||||
let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect();
|
let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect();
|
||||||
|
|
||||||
Ok(crate::templates::ContentTemplate {
|
Ok(crate::templates::ContentTemplate {
|
||||||
page_title: Some(String::from("SEPTASTIC | Routes")),
|
page_title: Some(String::from("Routes")),
|
||||||
page_desc: Some(String::from("All SEPTA routes.")),
|
page_desc: Some(String::from("All SEPTA routes.")),
|
||||||
widescreen: false,
|
widescreen: false,
|
||||||
content: crate::templates::RoutesTemplate {
|
content: crate::templates::RoutesTemplate {
|
||||||
|
|
@ -75,106 +76,37 @@ async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyho
|
||||||
schedule: trips
|
schedule: trips
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
|
|
||||||
let r = 6371.0; // Earth's radius in kilometers
|
|
||||||
|
|
||||||
let d_lat = (lat2 - lat1).to_radians();
|
//#[get("/directions")]
|
||||||
let d_lon = (lon2 - lon1).to_radians();
|
//async fn get_directions(state: Data<Arc<AppState>>) -> impl Responder {
|
||||||
|
// let near_thresh = 0.45;
|
||||||
let lat1_rad = lat1.to_radians();
|
//
|
||||||
let lat2_rad = lat2.to_radians();
|
// let sig_cds = routing::Coordinates {
|
||||||
|
// lat: 40.008420,
|
||||||
let a = (d_lat / 2.0).sin().powi(2)
|
// lng: -75.213439
|
||||||
+ lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
|
// };
|
||||||
|
//
|
||||||
let c = 2.0 * a.sqrt().asin();
|
// let home_cds = routing::Coordinates {
|
||||||
|
// lat: 39.957210,
|
||||||
r * c
|
// lng: -75.166214
|
||||||
}
|
// };
|
||||||
|
//
|
||||||
enum RoutingNodeType {
|
// let all_stops = state.gtfs_service.get_all_stops();
|
||||||
Origin,
|
//
|
||||||
Destination,
|
//
|
||||||
Midpoint
|
//
|
||||||
}
|
// let origin_stops: HashSet<String> = get_stops_near(home_cds, &all_stops);
|
||||||
|
// let dest_stops: HashSet<String> = get_stops_near(sig_cds.clone(), &all_stops);
|
||||||
struct RoutingNodePointer {
|
//
|
||||||
pub stop_id: String,
|
// let mut graph = construct_graph(sig_cds, &all_stops, &state.gtfs_service);
|
||||||
pub route_id: String,
|
//
|
||||||
pub stop_sequence: u64,
|
// let mut response = String::new();
|
||||||
pub direction: u64
|
// for stop in &origin_stops {
|
||||||
}
|
// response += bfs_rts(&stop, &mut graph, &dest_stops).as_str();
|
||||||
|
// }
|
||||||
struct RoutingNode {
|
//
|
||||||
pub node_type: RoutingNodeType,
|
// return response;
|
||||||
pub stop_id: String,
|
//}
|
||||||
pub next_stops_per_routes: HashMap<String, HashSet<RoutingNodePointer>>,
|
|
||||||
pub visited: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TripState {
|
|
||||||
pub used_lines: HashSet<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Coordinates {
|
|
||||||
pub lat: f64,
|
|
||||||
pub lng: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/directions")]
|
|
||||||
async fn get_directions(state: Data<Arc<AppState>>) -> impl Responder {
|
|
||||||
let near_thresh = 0.45;
|
|
||||||
|
|
||||||
let sig_cds = Coordinates {
|
|
||||||
lat: 40.008420,
|
|
||||||
lng: -75.213439
|
|
||||||
};
|
|
||||||
|
|
||||||
let home_cds = Coordinates {
|
|
||||||
lat: 39.957210,
|
|
||||||
lng: -75.166214
|
|
||||||
};
|
|
||||||
|
|
||||||
let all_stops = state.gtfs_service.get_all_stops();
|
|
||||||
|
|
||||||
let mut response = String::new();
|
|
||||||
|
|
||||||
|
|
||||||
let mut origin_stops: HashSet<String> = HashSet::new();
|
|
||||||
let mut dest_stops: HashSet<String> = HashSet::new();
|
|
||||||
|
|
||||||
let mut graph = HashMap::<String, RoutingNode>::new();
|
|
||||||
|
|
||||||
for stop_p in &all_stops {
|
|
||||||
let stop = stop_p.1;
|
|
||||||
|
|
||||||
let dist = haversine_distance(sig_cds.lat, sig_cds.lng, stop.lat, stop.lng);
|
|
||||||
|
|
||||||
if dist.abs() < near_thresh {
|
|
||||||
origin_stops.insert(stop.id.clone());
|
|
||||||
graph.insert(stop.id.clone(), RoutingNode {
|
|
||||||
node_type: RoutingNodeType::Origin,
|
|
||||||
stop_id: stop.id.clone(),
|
|
||||||
next_stops_per_routes: ,
|
|
||||||
visited: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for stop_p in &all_stops {
|
|
||||||
let stop = stop_p.1;
|
|
||||||
|
|
||||||
let dist = haversine_distance(home_cds.lat, home_cds.lng, stop.lat, stop.lng);
|
|
||||||
|
|
||||||
if dist.abs() < near_thresh {
|
|
||||||
dest_stops.insert(stop.id.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/route/{route_id}")]
|
#[get("/route/{route_id}")]
|
||||||
async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> impl Responder {
|
async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> impl Responder {
|
||||||
|
|
@ -183,12 +115,12 @@ async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Quer
|
||||||
let infox = info.clone();
|
let infox = info.clone();
|
||||||
let statex = state.clone();
|
let statex = state.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut filters: Option<Vec<i64>> = None;
|
let mut filters: Option<Vec<String>> = None;
|
||||||
if let Some (stops_v) = infox.stops.clone() {
|
if let Some (stops_v) = infox.stops.clone() {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
||||||
for sid in stops_v.split(",") {
|
for sid in stops_v.split(",") {
|
||||||
items.push(sid.parse::<i64>().unwrap());
|
items.push(String::from(sid));
|
||||||
}
|
}
|
||||||
filters = Some(items);
|
filters = Some(items);
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +133,7 @@ async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Quer
|
||||||
|
|
||||||
Ok(crate::templates::ContentTemplate {
|
Ok(crate::templates::ContentTemplate {
|
||||||
widescreen: false,
|
widescreen: false,
|
||||||
page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())),
|
page_title: Some(format!("Schedules for {}", route_id.clone())),
|
||||||
page_desc: Some(format!("Schedule information for {}", route_id.clone())),
|
page_desc: Some(format!("Schedule information for {}", route_id.clone())),
|
||||||
content: crate::templates::RouteTemplate {
|
content: crate::templates::RouteTemplate {
|
||||||
route: route_info.route,
|
route: route_info.route,
|
||||||
|
|
|
||||||
145
api/src/controllers/stop.rs
Normal file
145
api/src/controllers/stop.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use actix_web::{get, web::{self, Data}, HttpRequest, Responder};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use chrono::Timelike;
|
||||||
|
use chrono_tz::America::New_York;
|
||||||
|
use libseptastic::stop_schedule::{self, Trip};
|
||||||
|
use log::info;
|
||||||
|
use std::{collections::HashSet, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use crate::{AppState, templates::TripPerspective};
|
||||||
|
|
||||||
|
#[get("/stops")]
|
||||||
|
async fn get_stops_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
|
||||||
|
crate::perform_action(req, move || {
|
||||||
|
let statex = state.clone();
|
||||||
|
async move {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
let stops = statex.gtfs_service.get_all_stops().iter().filter_map(|f| {
|
||||||
|
if f.1.id.contains("ANNOTATED") {
|
||||||
|
Some(libseptastic::stop::Stop::clone(f.1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(crate::ContentTemplate {
|
||||||
|
page_title: Some(String::from("Stops")),
|
||||||
|
page_desc: Some(String::from("Stops")),
|
||||||
|
widescreen: false,
|
||||||
|
content: crate::templates::StopsTemplate {
|
||||||
|
tc_stops: stops
|
||||||
|
},
|
||||||
|
load_time_ms: Some(start_time.elapsed().as_millis())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
#[get("/stop/{stop_id}")]
|
||||||
|
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||||
|
crate::perform_action(req, move || {
|
||||||
|
let statex = state.clone();
|
||||||
|
let pathx = path.clone();
|
||||||
|
async move {
|
||||||
|
let stop_id = pathx;
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
if let Some(stop) = statex.gtfs_service.get_stop_by_id(&stop_id) {
|
||||||
|
let routes: Vec<libseptastic::route::Route> = statex.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| {
|
||||||
|
match statex.gtfs_service.get_route(route.clone()) {
|
||||||
|
Ok(route) => Some(route),
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let route_ids: HashSet<String> = routes.iter().map(|route| route.id.clone()).collect();
|
||||||
|
|
||||||
|
let mut trips = statex.gtfs_service.get_all_trips().iter().filter_map(|trip| {
|
||||||
|
if route_ids.contains(trip.0) {
|
||||||
|
Some(trip.1.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).flatten().collect();
|
||||||
|
|
||||||
|
|
||||||
|
statex.trip_tracking_service.annotate_trips(&mut trips).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 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()) {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let mut est_arrival_time = 0;
|
||||||
|
let mut is_tracked = false;
|
||||||
|
let stop_sched : Vec<_> = trip.schedule.iter().filter(|stop_schedule| {
|
||||||
|
if stop_schedule.stop.id != stop_id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &trip.tracking_data {
|
||||||
|
libseptastic::stop_schedule::TripTracking::Tracked(live) => {
|
||||||
|
let actual_arrival_time = stop_schedule.arrival_time + (live.delay * 60 as f64) as i64;
|
||||||
|
est_arrival_time = stop_schedule.arrival_time;
|
||||||
|
is_tracked = true;
|
||||||
|
return
|
||||||
|
(actual_arrival_time - cur_time) > -(1 * 60)
|
||||||
|
&&
|
||||||
|
(actual_arrival_time - cur_time) < (60 * 60)
|
||||||
|
;
|
||||||
|
},
|
||||||
|
libseptastic::stop_schedule::TripTracking::Untracked => {
|
||||||
|
est_arrival_time = stop_schedule.arrival_time;
|
||||||
|
return
|
||||||
|
(stop_schedule.arrival_time - cur_time) > -(3 * 60)
|
||||||
|
&&
|
||||||
|
(stop_schedule.arrival_time - cur_time) < (60 * 60)
|
||||||
|
;
|
||||||
|
},
|
||||||
|
libseptastic::stop_schedule::TripTracking::Cancelled => {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).filter_map(|ss| Some(ss.clone())).collect();
|
||||||
|
|
||||||
|
if stop_sched.len() > 0 {
|
||||||
|
Some(TripPerspective {
|
||||||
|
est_arrival_time,
|
||||||
|
is_tracked,
|
||||||
|
perspective_stop: stop_sched.first().unwrap().clone(),
|
||||||
|
trip: trip.clone()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
filtered_trips.sort_by_key(|f| f.perspective_stop.arrival_time);
|
||||||
|
|
||||||
|
Ok(crate::templates::ContentTemplate {
|
||||||
|
page_title: Some(stop.name.clone()),
|
||||||
|
page_desc: Some(String::from("Stop information")),
|
||||||
|
widescreen: false,
|
||||||
|
content: crate::templates::StopTemplate {
|
||||||
|
stop: stop.clone(),
|
||||||
|
routes,
|
||||||
|
trips: filtered_trips,
|
||||||
|
current_time: cur_time
|
||||||
|
},
|
||||||
|
load_time_ms: Some(start_time.elapsed().as_millis())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Err(anyhow!("Stop not found!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ use askama::Template;
|
||||||
mod services;
|
mod services;
|
||||||
mod controllers;
|
mod controllers;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
//mod routing;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
gtfs_service: services::gtfs_pull::GtfsPullService,
|
gtfs_service: services::gtfs_pull::GtfsPullService,
|
||||||
|
|
@ -51,7 +52,8 @@ where T: Template,
|
||||||
.cookie(cookie)
|
.cookie(cookie)
|
||||||
.body(y.render().unwrap())
|
.body(y.render().unwrap())
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
|
error!("Returned error b/c {:?}", err);
|
||||||
HttpResponse::InternalServerError().body("Error")
|
HttpResponse::InternalServerError().body("Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +107,9 @@ async fn main() -> ::anyhow::Result<()> {
|
||||||
.service(controllers::route::get_route)
|
.service(controllers::route::get_route)
|
||||||
.service(controllers::route::get_routes_json)
|
.service(controllers::route::get_routes_json)
|
||||||
.service(controllers::route::get_routes_html)
|
.service(controllers::route::get_routes_html)
|
||||||
.service(controllers::route::get_directions)
|
// .service(controllers::route::get_directions)
|
||||||
|
.service(controllers::stop::get_stops_html)
|
||||||
|
.service(controllers::stop::get_stop_html)
|
||||||
.service(get_index)
|
.service(get_index)
|
||||||
.service(actix_files::Files::new("/assets", "./assets"))
|
.service(actix_files::Files::new("/assets", "./assets"))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
225
api/src/routing.rs
Normal file
225
api/src/routing.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
use std::{cmp::Ordering, collections::{BTreeSet, HashMap, HashSet}};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::services;
|
||||||
|
|
||||||
|
pub struct RoutingNodePointer {
|
||||||
|
pub stop_id: String,
|
||||||
|
pub route_id: String,
|
||||||
|
pub stop_sequence: u64,
|
||||||
|
pub direction: u64,
|
||||||
|
pub dest_dist: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RoutingNode {
|
||||||
|
pub stop_id: String,
|
||||||
|
pub stop_name: String,
|
||||||
|
pub next_stops_per_routes: HashMap<String, BTreeSet<RoutingNodePointer>>,
|
||||||
|
pub visited: bool,
|
||||||
|
pub scratch: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for RoutingNodePointer {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
if self.dest_dist > other.dest_dist {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
Ordering::Less
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for RoutingNodePointer {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
if self.dest_dist > other.dest_dist {
|
||||||
|
Some(Ordering::Greater)
|
||||||
|
} else {
|
||||||
|
Some(Ordering::Less)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for RoutingNodePointer {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.stop_id == other.stop_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Eq for RoutingNodePointer {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct TripState {
|
||||||
|
pub used_lines: HashSet<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Coordinates {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lng: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RoutingGraph = HashMap::<String, RoutingNode>;
|
||||||
|
|
||||||
|
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
|
||||||
|
let r = 6371.0; // Earth's radius in kilometers
|
||||||
|
|
||||||
|
let d_lat = (lat2 - lat1).to_radians();
|
||||||
|
let d_lon = (lon2 - lon1).to_radians();
|
||||||
|
|
||||||
|
let lat1_rad = lat1.to_radians();
|
||||||
|
let lat2_rad = lat2.to_radians();
|
||||||
|
|
||||||
|
let a = (d_lat / 2.0).sin().powi(2)
|
||||||
|
+ lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
|
||||||
|
|
||||||
|
let c = 2.0 * a.sqrt().asin();
|
||||||
|
|
||||||
|
r * c
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_stops_near(cds: Coordinates,
|
||||||
|
all_stops: &HashMap<String, libseptastic::stop::Stop>
|
||||||
|
) -> HashSet<String> {
|
||||||
|
let near_thresh_km = 0.45;
|
||||||
|
let mut stops: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
for stop_p in all_stops {
|
||||||
|
let stop = stop_p.1;
|
||||||
|
|
||||||
|
let dist = haversine_distance(cds.lat, cds.lng, stop.lat, stop.lng);
|
||||||
|
|
||||||
|
if dist.abs() < near_thresh_km {
|
||||||
|
stops.insert(stop.id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stops
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stop_as_node() {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct_graph(
|
||||||
|
dest: Coordinates,
|
||||||
|
all_stops: &HashMap<String, libseptastic::stop::Stop>,
|
||||||
|
gtfs_service: &services::gtfs_pull::GtfsPullService
|
||||||
|
) -> RoutingGraph {
|
||||||
|
let mut graph = RoutingGraph::new();
|
||||||
|
|
||||||
|
let limited_rts = vec!["44", "65", "27", "38", "124", "125", "1"];
|
||||||
|
|
||||||
|
for stop_p in all_stops {
|
||||||
|
let stop = stop_p.1;
|
||||||
|
|
||||||
|
let ras = gtfs_service.get_routes_at_stop(&stop.id);
|
||||||
|
|
||||||
|
let cont = {
|
||||||
|
let mut ret = false;
|
||||||
|
for l_rt in limited_rts.clone() {
|
||||||
|
if ras.contains(&String::from(l_rt)) {
|
||||||
|
ret = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cont {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.insert(stop.id.clone(), RoutingNode {
|
||||||
|
stop_id: stop.id.clone(),
|
||||||
|
stop_name: stop.name.clone(),
|
||||||
|
next_stops_per_routes: {
|
||||||
|
let routes = gtfs_service.get_routes_at_stop(&stop.id);
|
||||||
|
|
||||||
|
let mut other_stops = HashMap::<String, BTreeSet<RoutingNodePointer>>::new();
|
||||||
|
|
||||||
|
for route in &routes {
|
||||||
|
let mut stops = gtfs_service.get_stops_by_route(&route);
|
||||||
|
stops.remove(&stop.id);
|
||||||
|
let rnps = {
|
||||||
|
let mut ret = BTreeSet::new();
|
||||||
|
for stop in &stops {
|
||||||
|
let stp = all_stops.get(stop).unwrap();
|
||||||
|
ret.insert(RoutingNodePointer{
|
||||||
|
dest_dist: haversine_distance(dest.lat, dest.lng, stp.lat, stp.lng),
|
||||||
|
stop_id: stop.clone(),
|
||||||
|
route_id: route.clone(),
|
||||||
|
stop_sequence: 0,
|
||||||
|
direction: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
};
|
||||||
|
other_stops.insert(route.clone(), rnps);
|
||||||
|
}
|
||||||
|
|
||||||
|
other_stops
|
||||||
|
},
|
||||||
|
visited: false,
|
||||||
|
scratch: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
pub fn bfs_rts_int(route_id: &String, origin: &String, graph: &RoutingGraph, dests: &HashSet<String>, mut visited: HashSet<String>, max_legs: u8) -> Option<String> {
|
||||||
|
if max_legs == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut limited_rts = HashSet::new();
|
||||||
|
for item in vec!["44", "65", "27", "38", "124", "125", "1"] {
|
||||||
|
limited_rts.insert(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !limited_rts.contains(&route_id.as_str()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(origin_stop) = graph.get(origin) {
|
||||||
|
if dests.contains(origin) {
|
||||||
|
return Some(format!("[stop {} via rt {}] --> DEST", origin_stop.stop_name, route_id))
|
||||||
|
}
|
||||||
|
if visited.contains(origin) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.insert(origin.clone());
|
||||||
|
|
||||||
|
for items in &origin_stop.next_stops_per_routes {
|
||||||
|
if route_id == items.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for rnp in items.1 {
|
||||||
|
if let Some(rt) = bfs_rts_int(items.0, &rnp.stop_id, graph, dests, visited.clone(), max_legs - 1) {
|
||||||
|
return Some(format!("[stop {} via rt {}] >>[XFER]>> {}", origin_stop.stop_name, route_id, rt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bfs_rts(origin: &String, graph: &RoutingGraph, dests: &HashSet<String>) -> String {
|
||||||
|
let mut resp = String::new();
|
||||||
|
|
||||||
|
if let Some(origin_stop) = graph.get(origin) {
|
||||||
|
for items in &origin_stop.next_stops_per_routes {
|
||||||
|
let route_id = items.0;
|
||||||
|
|
||||||
|
for rnp in items.1 {
|
||||||
|
if let Some(rt) = bfs_rts_int(route_id, &rnp.stop_id, graph, dests, HashSet::new(), 3) {
|
||||||
|
resp += format!("ORIGIN --> [stop {} via rt {}] --> {}\n", origin_stop.stop_name, route_id, rt).as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,39 @@
|
||||||
use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex}, thread, time::Duration};
|
use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use libseptastic::{stop::Platform, stop_schedule::CalendarDay};
|
||||||
use log::{info, error};
|
use log::{info, error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
|
||||||
|
macro_rules! make_global_id {
|
||||||
|
($prefix: expr, $id: expr) => (format!("{}_{}", $prefix, $id))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||||
struct GtfsSource {
|
struct GtfsSource {
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub subzip: Option<String>
|
pub subzip: Option<String>,
|
||||||
|
pub prefix: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||||
|
struct MultiplatformStopConfig {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub platform_station_ids: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||||
|
struct Annotations {
|
||||||
|
pub multiplatform_stops: Vec<MultiplatformStopConfig>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub gtfs_zips: Vec<GtfsSource>
|
pub gtfs_zips: Vec<GtfsSource>,
|
||||||
|
pub annotations: Annotations
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -23,17 +43,24 @@ struct GtfsFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransitData {
|
struct TransitData {
|
||||||
pub routes: HashMap<String, libseptastic::route::Route>,
|
pub routes: HashMap<String, Arc<libseptastic::route::Route>>,
|
||||||
pub agencies: HashMap<String, libseptastic::agency::Agency>,
|
pub agencies: HashMap<String, libseptastic::agency::Agency>,
|
||||||
pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
|
pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
|
||||||
pub stops: HashMap<String, libseptastic::stop::Stop>,
|
pub stops: HashMap<String, Arc<libseptastic::stop::Stop>>,
|
||||||
pub route_id_by_stops: HashMap<String, HashSet<String>>
|
pub platforms: HashMap<String, Arc<libseptastic::stop::Platform>>,
|
||||||
|
pub calendar_days: HashMap<String, Arc<libseptastic::stop_schedule::CalendarDay>>,
|
||||||
|
|
||||||
|
// extended lookup methods
|
||||||
|
pub route_id_by_stops: HashMap<String, HashSet<String>>,
|
||||||
|
pub stops_by_route_id: HashMap<String, HashSet<String>>,
|
||||||
|
pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GtfsPullServiceState {
|
struct GtfsPullServiceState {
|
||||||
pub gtfs_files: Vec<GtfsFile>,
|
pub gtfs_files: Vec<GtfsFile>,
|
||||||
pub tmp_dir: PathBuf,
|
pub tmp_dir: PathBuf,
|
||||||
pub ready: bool,
|
pub ready: bool,
|
||||||
|
pub annotations: Annotations,
|
||||||
pub transit_data: TransitData
|
pub transit_data: TransitData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,12 +70,13 @@ pub struct GtfsPullService {
|
||||||
|
|
||||||
impl TransitData {
|
impl TransitData {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
return TransitData { routes: HashMap::new(), agencies: HashMap::new(), trips: HashMap::new(), stops: HashMap::new(), route_id_by_stops: 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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GtfsPullService {
|
impl GtfsPullService {
|
||||||
const UPDATE_SECONDS: u64 = 3600*24;
|
const UPDATE_SECONDS: u64 = 3600*24;
|
||||||
|
const READYSTATE_CHECK_MILLISECONDS: u64 = 500;
|
||||||
|
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -56,6 +84,7 @@ impl GtfsPullService {
|
||||||
GtfsPullServiceState {
|
GtfsPullServiceState {
|
||||||
gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(),
|
gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(),
|
||||||
tmp_dir: env::temp_dir(),
|
tmp_dir: env::temp_dir(),
|
||||||
|
annotations: config.annotations.clone(),
|
||||||
ready: false,
|
ready: false,
|
||||||
transit_data: TransitData::new()
|
transit_data: TransitData::new()
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +94,9 @@ impl GtfsPullService {
|
||||||
|
|
||||||
pub fn wait_for_ready(&self) {
|
pub fn wait_for_ready(&self) {
|
||||||
while !(self.state.lock().unwrap()).ready {
|
while !(self.state.lock().unwrap()).ready {
|
||||||
thread::sleep(Duration::from_millis(500));
|
thread::sleep(
|
||||||
|
Duration::from_millis(Self::READYSTATE_CHECK_MILLISECONDS)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,13 +121,13 @@ impl GtfsPullService {
|
||||||
|
|
||||||
pub fn get_routes(&self) -> Vec<libseptastic::route::Route> {
|
pub fn get_routes(&self) -> Vec<libseptastic::route::Route> {
|
||||||
let l_state = self.state.lock().unwrap();
|
let l_state = self.state.lock().unwrap();
|
||||||
l_state.transit_data.routes.iter().map(|r| r.1.clone()).collect()
|
l_state.transit_data.routes.iter().map(|r| libseptastic::route::Route::clone(r.1)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_route(&self, route_id: String) -> anyhow::Result<libseptastic::route::Route> {
|
pub fn get_route(&self, route_id: String) -> anyhow::Result<libseptastic::route::Route> {
|
||||||
let l_state = self.state.lock().unwrap();
|
let l_state = self.state.lock().unwrap();
|
||||||
if let Some(route) = l_state.transit_data.routes.get(&route_id) {
|
if let Some(route) = l_state.transit_data.routes.get(&route_id) {
|
||||||
Ok(route.clone())
|
Ok(libseptastic::route::Route::clone(route))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!(""))
|
Err(anyhow!(""))
|
||||||
}
|
}
|
||||||
|
|
@ -104,10 +135,10 @@ impl GtfsPullService {
|
||||||
|
|
||||||
pub fn get_all_routes(&self) -> HashMap<String, libseptastic::route::Route> {
|
pub fn get_all_routes(&self) -> HashMap<String, libseptastic::route::Route> {
|
||||||
let l_state = self.state.lock().unwrap();
|
let l_state = self.state.lock().unwrap();
|
||||||
l_state.transit_data.routes.clone()
|
l_state.transit_data.routes.iter().map(|r| (r.0.clone(), libseptastic::route::Route::clone(r.1))).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_stops(&self) -> HashMap<String, libseptastic::stop::Stop> {
|
pub fn get_all_stops(&self) -> HashMap<String, Arc<libseptastic::stop::Stop>> {
|
||||||
let l_state = self.state.lock().unwrap();
|
let l_state = self.state.lock().unwrap();
|
||||||
l_state.transit_data.stops.clone()
|
l_state.transit_data.stops.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -117,9 +148,22 @@ impl GtfsPullService {
|
||||||
l_state.transit_data.trips.clone()
|
l_state.transit_data.trips.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_routes_at_stop(&self, id: String) -> HashSet<String> {
|
pub fn get_routes_at_stop(&self, id: &String) -> HashSet<String> {
|
||||||
let l_state = self.state.lock().unwrap();
|
let l_state = self.state.lock().unwrap();
|
||||||
l_state.transit_data.route_id_by_stops.get(&id).unwrap_or(&HashSet::new()).clone()
|
l_state.transit_data.route_id_by_stops.get(id).unwrap_or(&HashSet::new()).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stops_by_route(&self, id: &String) -> HashSet<String> {
|
||||||
|
let l_state = self.state.lock().unwrap();
|
||||||
|
l_state.transit_data.stops_by_route_id.get(id).unwrap_or(&HashSet::new()).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stop_by_id(&self, id: &String) -> Option<libseptastic::stop::Stop> {
|
||||||
|
let l_state = self.state.lock().unwrap();
|
||||||
|
match l_state.transit_data.stops.get(id) {
|
||||||
|
Some(stop) => Some(libseptastic::stop::Stop::clone(stop)),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_schedule(&self, route_id: String) -> anyhow::Result<Vec<libseptastic::stop_schedule::Trip>> {
|
pub fn get_schedule(&self, route_id: String) -> anyhow::Result<Vec<libseptastic::stop_schedule::Trip>> {
|
||||||
|
|
@ -131,9 +175,155 @@ impl GtfsPullService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn postprocess_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>) -> anyhow::Result<()> {
|
||||||
|
for annotated_stop in state.annotations.multiplatform_stops.clone() {
|
||||||
|
let global_id = make_global_id!("ANNOTATED", annotated_stop.id.clone());
|
||||||
|
let stop = Arc::new(libseptastic::stop::Stop {
|
||||||
|
id: global_id.clone(),
|
||||||
|
name: annotated_stop.name.clone(),
|
||||||
|
platforms: libseptastic::stop::StopType::MultiPlatform(annotated_stop.platform_station_ids.iter().map(|platform_id| {
|
||||||
|
info!("Folding {} stop into stop {} as platform", platform_id.clone(), annotated_stop.id.clone());
|
||||||
|
let platform = match state.transit_data.stops.remove(platform_id).unwrap().platforms.clone() {
|
||||||
|
libseptastic::stop::StopType::SinglePlatform(plat) => Ok(plat),
|
||||||
|
_ => Err(anyhow!(""))
|
||||||
|
}.unwrap();
|
||||||
|
|
||||||
|
state.transit_data.stops_by_platform_id.remove(&platform.id).unwrap();
|
||||||
|
|
||||||
|
platform
|
||||||
|
}).collect())
|
||||||
|
});
|
||||||
|
|
||||||
|
state.transit_data.stops.insert(global_id.clone(), stop.clone());
|
||||||
|
match &stop.platforms {
|
||||||
|
libseptastic::stop::StopType::MultiPlatform(platforms) => {
|
||||||
|
for platform in platforms {
|
||||||
|
state.transit_data.stops_by_platform_id.insert(platform.id.clone(), stop.clone());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
_ => Err(anyhow!(""))
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> {
|
||||||
|
for stop in >fs.stops {
|
||||||
|
let global_id = make_global_id!(prefix, stop.1.id.clone());
|
||||||
|
let platform = Arc::new(Platform {
|
||||||
|
id : global_id.clone(),
|
||||||
|
name: stop.1.name.clone().unwrap(),
|
||||||
|
lat: stop.1.latitude.unwrap(),
|
||||||
|
lng: stop.1.longitude.unwrap(),
|
||||||
|
platform_location: libseptastic::stop::PlatformLocationType::Normal
|
||||||
|
});
|
||||||
|
|
||||||
|
let stop = Arc::new(libseptastic::stop::Stop {
|
||||||
|
id: global_id.clone(),
|
||||||
|
name: stop.1.name.clone().unwrap(),
|
||||||
|
platforms: libseptastic::stop::StopType::SinglePlatform(platform.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
state.transit_data.stops.insert(global_id.clone(), stop.clone());
|
||||||
|
state.transit_data.platforms.insert(global_id.clone(), platform.clone());
|
||||||
|
state.transit_data.stops_by_platform_id.insert(global_id.clone(), stop.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let rt_name = match route.1.long_name.clone() {
|
||||||
|
Some(x) => x,
|
||||||
|
_ => String::from("Unknown")
|
||||||
|
};
|
||||||
|
|
||||||
|
state.transit_data.routes.insert(global_rt_id.clone(), Arc::new(libseptastic::route::Route{
|
||||||
|
name: rt_name,
|
||||||
|
short_name: match route.1.short_name.clone() {
|
||||||
|
Some(x) => x,
|
||||||
|
_ => String::from("unknown")
|
||||||
|
},
|
||||||
|
color_hex: match route.1.color{
|
||||||
|
Some(x) => x.to_string(),
|
||||||
|
_ => String::from("unknown")
|
||||||
|
},
|
||||||
|
id: global_rt_id,
|
||||||
|
route_type: match route.1.route_type {
|
||||||
|
gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus,
|
||||||
|
gtfs_structures::RouteType::Rail => libseptastic::route::RouteType::RegionalRail,
|
||||||
|
gtfs_structures::RouteType::Subway => libseptastic::route::RouteType::SubwayElevated,
|
||||||
|
gtfs_structures::RouteType::Tramway => libseptastic::route::RouteType::Trolley,
|
||||||
|
_ => libseptastic::route::RouteType::TracklessTrolley
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let sched = trip.1.stop_times.iter().map(|s| {
|
||||||
|
let global_stop_id = make_global_id!(prefix, s.stop.id);
|
||||||
|
|
||||||
|
let stop = state.transit_data.stops_by_platform_id.get(&global_stop_id).unwrap().clone();
|
||||||
|
let platform = state.transit_data.platforms.get(&global_stop_id).unwrap().clone();
|
||||||
|
|
||||||
|
state.transit_data.route_id_by_stops.entry(stop.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone());
|
||||||
|
state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(stop.id.clone());
|
||||||
|
|
||||||
|
state.transit_data.route_id_by_stops.entry(platform.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone());
|
||||||
|
state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(platform.id.clone());
|
||||||
|
|
||||||
|
libseptastic::stop_schedule::StopSchedule{
|
||||||
|
arrival_time: i64::from(s.arrival_time.unwrap()),
|
||||||
|
stop_sequence: i64::from(s.stop_sequence),
|
||||||
|
stop,
|
||||||
|
platform
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
if let Some(calendar_day) = state.transit_data.calendar_days.get(&trip.1.service_id.clone()) {
|
||||||
|
let trip = libseptastic::stop_schedule::Trip{
|
||||||
|
trip_id: trip.1.id.clone(),
|
||||||
|
route: state.transit_data.routes.get(&make_global_id!(prefix, trip.1.route_id)).unwrap().clone(),
|
||||||
|
direction: 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()
|
||||||
|
},
|
||||||
|
tracking_data: libseptastic::stop_schedule::TripTracking::Untracked,
|
||||||
|
schedule: sched,
|
||||||
|
service_id: trip.1.service_id.clone(),
|
||||||
|
calendar_day: calendar_day.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(trip_arr) = state.transit_data.trips.get_mut(&global_rt_id) {
|
||||||
|
trip_arr.push(trip);
|
||||||
|
} else {
|
||||||
|
state.transit_data.trips.insert(global_rt_id, vec![trip]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_gtfs_data(state: Arc<Mutex<GtfsPullServiceState>>) -> anyhow::Result<()> {
|
pub fn update_gtfs_data(state: Arc<Mutex<GtfsPullServiceState>>) -> anyhow::Result<()> {
|
||||||
let mut l_state = state.lock().unwrap();
|
let mut l_state = state.lock().unwrap();
|
||||||
let files = l_state.gtfs_files.clone();
|
let files = l_state.gtfs_files.clone();
|
||||||
|
l_state.transit_data = TransitData::new();
|
||||||
|
|
||||||
|
let mut gtfses = Vec::new();
|
||||||
|
|
||||||
for gtfs_file in files.iter() {
|
for gtfs_file in files.iter() {
|
||||||
let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() {
|
let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() {
|
||||||
|
|
@ -145,6 +335,8 @@ impl GtfsPullService {
|
||||||
|
|
||||||
let mut file_path = l_state.tmp_dir.clone();
|
let mut file_path = l_state.tmp_dir.clone();
|
||||||
file_path.push(subzip.clone());
|
file_path.push(subzip.clone());
|
||||||
|
|
||||||
|
info!("Downloaded, parsing");
|
||||||
|
|
||||||
gtfs_structures::Gtfs::new(file_path.to_str().unwrap())?
|
gtfs_structures::Gtfs::new(file_path.to_str().unwrap())?
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -152,115 +344,35 @@ impl GtfsPullService {
|
||||||
gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())?
|
gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hack_agency = None;
|
gtfses.push((gtfs, gtfs_file.source.prefix.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
for agency in >fs.agencies {
|
|
||||||
if let Some(a_id) = &agency.id {
|
info!("Data loaded, processing...");
|
||||||
l_state.transit_data.agencies.insert(a_id.clone(), libseptastic::agency::Agency{
|
|
||||||
id: a_id.clone(),
|
for (gtfs, prefix) in >fses {
|
||||||
name: agency.name.clone()
|
GtfsPullService::populate_routes(&mut l_state, &prefix, >fs)?;
|
||||||
});
|
GtfsPullService::populate_stops(&mut l_state, &prefix, >fs)?;
|
||||||
hack_agency = Some(libseptastic::agency::Agency{
|
for calendar in >fs.calendar {
|
||||||
id: a_id.clone(),
|
l_state.transit_data.calendar_days.insert(calendar.1.id.clone(), Arc::new(CalendarDay{
|
||||||
name: agency.name.clone()
|
id: calendar.1.id.clone(),
|
||||||
});
|
monday: calendar.1.monday,
|
||||||
}
|
tuesday: calendar.1.tuesday,
|
||||||
|
wednesday: calendar.1.wednesday,
|
||||||
|
thursday: calendar.1.thursday,
|
||||||
|
friday: calendar.1.friday,
|
||||||
|
saturday: calendar.1.saturday,
|
||||||
|
sunday: calendar.1.sunday,
|
||||||
|
start_date: calendar.1.start_date,
|
||||||
|
end_date: calendar.1.end_date
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GtfsPullService::postprocess_stops(&mut l_state)?;
|
||||||
|
|
||||||
for route in >fs.routes {
|
for (gtfs, prefix) in >fses {
|
||||||
let agency = route.1.agency_id.as_ref()
|
GtfsPullService::populate_trips(&mut l_state, &prefix, >fs)?;
|
||||||
.and_then(|agency_id| l_state.transit_data.agencies.get(agency_id))
|
|
||||||
.map(|agency| agency.clone());
|
|
||||||
|
|
||||||
let global_rt_id = match &agency {
|
|
||||||
Some(a) => format!("{}_{}", a.id, route.0.clone()),
|
|
||||||
None => format!("{}", route.0.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let rt_name = match route.1.long_name.clone() {
|
|
||||||
Some(x) => x,
|
|
||||||
_ => match route.1.short_name.clone() {
|
|
||||||
Some(y) => match agency {
|
|
||||||
Some(z) => format!("{} {}", z.name, y),
|
|
||||||
None => y
|
|
||||||
},
|
|
||||||
None => String::from("unknown")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
l_state.transit_data.routes.insert(global_rt_id.clone(), libseptastic::route::Route{
|
|
||||||
name: rt_name,
|
|
||||||
short_name: match route.1.short_name.clone() {
|
|
||||||
Some(x) => x,
|
|
||||||
_ => String::from("unknown")
|
|
||||||
},
|
|
||||||
color_hex: match route.1.color{
|
|
||||||
Some(x) => x.to_string(),
|
|
||||||
_ => String::from("unknown")
|
|
||||||
},
|
|
||||||
id: global_rt_id,
|
|
||||||
route_type: match route.1.route_type {
|
|
||||||
gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus,
|
|
||||||
gtfs_structures::RouteType::Rail => libseptastic::route::RouteType::RegionalRail,
|
|
||||||
gtfs_structures::RouteType::Subway => libseptastic::route::RouteType::SubwayElevated,
|
|
||||||
gtfs_structures::RouteType::Tramway => libseptastic::route::RouteType::Trolley,
|
|
||||||
_ => libseptastic::route::RouteType::TracklessTrolley
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for stop in >fs.stops {
|
|
||||||
l_state.transit_data.stops.insert(stop.1.id.clone(), libseptastic::stop::Stop {
|
|
||||||
id: stop.1.id.clone(),
|
|
||||||
name: stop.1.name.clone().unwrap(),
|
|
||||||
lat: stop.1.latitude.unwrap(),
|
|
||||||
lng: stop.1.longitude.unwrap(),
|
|
||||||
stop_type: libseptastic::stop::StopType::Normal
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for trip in >fs.trips {
|
|
||||||
let global_rt_id = match &hack_agency {
|
|
||||||
Some(a) => format!("{}_{}", a.id, trip.1.route_id.clone()),
|
|
||||||
None => format!("{}", trip.1.route_id.clone())
|
|
||||||
};
|
|
||||||
let sched = trip.1.stop_times.iter().map(|s| {
|
|
||||||
|
|
||||||
l_state.transit_data.route_id_by_stops.entry(s.stop.id.clone()).or_insert(HashSet::new()).insert(trip.1.route_id.clone());
|
|
||||||
libseptastic::stop_schedule::StopSchedule{
|
|
||||||
arrival_time: i64::from(s.arrival_time.unwrap()),
|
|
||||||
stop_sequence: i64::from(s.stop_sequence),
|
|
||||||
stop: libseptastic::stop::Stop {
|
|
||||||
name: s.stop.name.clone().unwrap(),
|
|
||||||
lat: s.stop.latitude.unwrap(),
|
|
||||||
lng: s.stop.longitude.unwrap(),
|
|
||||||
id: s.stop.id.parse().unwrap(),
|
|
||||||
stop_type: libseptastic::stop::StopType::Normal
|
|
||||||
}}
|
|
||||||
}).collect();
|
|
||||||
|
|
||||||
let trip = libseptastic::stop_schedule::Trip{
|
|
||||||
trip_id: trip.1.id.clone(),
|
|
||||||
direction: 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()
|
|
||||||
},
|
|
||||||
tracking_data: libseptastic::stop_schedule::TripTracking::Untracked,
|
|
||||||
schedule: sched,
|
|
||||||
service_id: trip.1.service_id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(trip_arr) = l_state.transit_data.trips.get_mut(&global_rt_id) {
|
|
||||||
trip_arr.push(trip);
|
|
||||||
} else {
|
|
||||||
l_state.transit_data.trips.insert(global_rt_id, vec![trip]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Added {} routes", gtfs.routes.len());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l_state.ready = true;
|
l_state.ready = true;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pub struct ContentTemplate<T: askama::Template> {
|
||||||
pub struct RouteTemplate {
|
pub struct RouteTemplate {
|
||||||
pub route: libseptastic::route::Route,
|
pub route: libseptastic::route::Route,
|
||||||
pub timetables: Vec<TimetableDirection>,
|
pub timetables: Vec<TimetableDirection>,
|
||||||
pub filter_stops: Option<Vec<i64>>
|
pub filter_stops: Option<Vec<String>>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -32,6 +32,12 @@ pub struct RoutesTemplate {
|
||||||
pub bus_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)]
|
#[derive(askama::Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
pub struct IndexTemplate {
|
pub struct IndexTemplate {
|
||||||
|
|
@ -39,7 +45,7 @@ pub struct IndexTemplate {
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct TimetableStopRow {
|
pub struct TimetableStopRow {
|
||||||
pub stop_id: i64,
|
pub stop_id: String,
|
||||||
pub stop_name: String,
|
pub stop_name: String,
|
||||||
pub stop_sequence: i64,
|
pub stop_sequence: i64,
|
||||||
pub times: Vec<Option<i64>>
|
pub times: Vec<Option<i64>>
|
||||||
|
|
@ -55,6 +61,21 @@ pub struct TimetableDirection {
|
||||||
pub next_id: Option<String>
|
pub next_id: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TripPerspective {
|
||||||
|
pub trip:libseptastic::stop_schedule::Trip,
|
||||||
|
pub perspective_stop: libseptastic::stop_schedule::StopSchedule,
|
||||||
|
pub est_arrival_time: i64,
|
||||||
|
pub is_tracked: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "stop.html")]
|
||||||
|
pub struct StopTemplate {
|
||||||
|
pub stop: libseptastic::stop::Stop,
|
||||||
|
pub routes: Vec<libseptastic::route::Route>,
|
||||||
|
pub trips: Vec<TripPerspective>,
|
||||||
|
pub current_time: i64
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_timetables(
|
pub fn build_timetables(
|
||||||
directions: Vec<Direction>,
|
directions: Vec<Direction>,
|
||||||
|
|
@ -120,7 +141,7 @@ pub fn build_timetables(
|
||||||
let mut rows: Vec<TimetableStopRow> = stop_map
|
let mut rows: Vec<TimetableStopRow> = stop_map
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
|
.map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
|
||||||
stop_id: stop_id.parse().unwrap(),
|
stop_id,
|
||||||
stop_sequence,
|
stop_sequence,
|
||||||
stop_name,
|
stop_name,
|
||||||
times,
|
times,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
{% if let Some(title) = page_title %}
|
{% if let Some(title) = page_title %}
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }} | SEPTASTIC</title>
|
||||||
{% else %}
|
{% else %}
|
||||||
<title>SEPTASTIC</title>
|
<title>SEPTASTIC</title>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -58,6 +58,7 @@ window.onload = function () {
|
||||||
<div>
|
<div>
|
||||||
<a class="nav-link" href="/">[ Home ]</a>
|
<a class="nav-link" href="/">[ Home ]</a>
|
||||||
<a class="nav-link" href="/routes">[ Routes ]</a>
|
<a class="nav-link" href="/routes">[ Routes ]</a>
|
||||||
|
<a class="nav-link" href="/stops">[ Stops ]</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
.train-direction-table {
|
.train-direction-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-family: sans-serif;
|
font-family: mono;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,11 +148,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
{% let live_o = timetable.tracking_data[loop.index0] %}
|
{% let live_o = timetable.tracking_data[loop.index0] %}
|
||||||
{% if let Tracked(live) = live_o %}
|
{% if let Tracked(live) = live_o %}
|
||||||
{% let time = (t + (live.delay * 60.0) as i64) %}
|
{% let time = (t + (live.delay * 60.0) as i64) %}
|
||||||
{% if live.next_stop_id == Some(*row.stop_id) %}
|
|
||||||
<td style="background-color: #007700">
|
|
||||||
{% else %}
|
|
||||||
<td style="background-color: #003300">
|
<td style="background-color: #003300">
|
||||||
{% endif %}
|
|
||||||
<span style="color: #22bb22"> {{ time | format_time }} </span>
|
<span style="color: #22bb22"> {{ time | format_time }} </span>
|
||||||
</td>
|
</td>
|
||||||
{% elif let TripTracking::Cancelled = live_o %}
|
{% elif let TripTracking::Cancelled = live_o %}
|
||||||
|
|
|
||||||
71
api/templates/stop.html
Normal file
71
api/templates/stop.html
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
{%- import "route_symbol.html" as scope -%}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.trip-desc {
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<h1>{{ stop.name }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>With service available on:</p>
|
||||||
|
<div style="display: flex; justify-content: start; padding-top: 5px; padding-bottom: 5px; flex-wrap: wrap; gap: 5px;">
|
||||||
|
{% for route in routes %}
|
||||||
|
<div style="margin-right: 5px">
|
||||||
|
{% call scope::route_symbol(route) %}
|
||||||
|
</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 %}#}
|
||||||
|
|
||||||
|
<table class="train-direction-table">
|
||||||
|
<tr>
|
||||||
|
<th>ROUTE</th>
|
||||||
|
<th>DESTINATION</th>
|
||||||
|
<th>BOARDING AREA</th>
|
||||||
|
<th>TIME</th>
|
||||||
|
<th>VEHICLE</th>
|
||||||
|
</tr>
|
||||||
|
{% for trip in trips %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% call scope::route_symbol(trip.trip.route) %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p>{{ trip.trip.direction.direction_destination }}</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p>{{ trip.perspective_stop.platform.name }}</p>
|
||||||
|
</td>
|
||||||
|
{% if trip.is_tracked %}
|
||||||
|
<td style="color: #008800">
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
{% endif %}
|
||||||
|
<p style="font-size: small;">{{ trip.perspective_stop.arrival_time | format_time }}</p>
|
||||||
|
<p style="font-size: x-small; font-style: italic;">{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins</p>
|
||||||
|
</td>
|
||||||
|
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||||
|
<td>
|
||||||
|
{{ tracked_trip.vehicle_ids.join(", ") }}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
27
api/templates/stops.html
Normal file
27
api/templates/stops.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<h1>Stops</h1>
|
||||||
|
|
||||||
|
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend><h2>Transit Centers</h2></legend>
|
||||||
|
<p style="margin-top: 10px; margin-bottom: 10px;">Hubs to connect between different modes of transit</p>
|
||||||
|
{% for stop in tc_stops %}
|
||||||
|
<a href="/stop/{{ stop.id }}" style="display: flex; justify-content: space-between;">
|
||||||
|
<p class="line-link">[ {{ stop.name }} </p><p>]</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.line-link, .lines-label {
|
||||||
|
white-space: pre;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-label {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #000000;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
libseptastic/Cargo.lock
generated
237
libseptastic/Cargo.lock
generated
|
|
@ -8,6 +8,15 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -53,6 +62,12 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -65,12 +80,46 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.52"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -86,6 +135,12 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -212,6 +267,12 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -390,6 +451,30 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -513,6 +598,16 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.83"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -538,6 +633,8 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||||
name = "libseptastic"
|
name = "libseptastic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|
@ -688,6 +785,24 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -822,6 +937,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
|
|
@ -900,6 +1021,12 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -910,6 +1037,12 @@ dependencies = [
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
|
@ -1325,6 +1458,51 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -1335,6 +1513,65 @@ dependencies = [
|
||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.42", features = [ "serde" ] }
|
||||||
|
chrono-tz = "0.10.4"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = "0.8.6"
|
sqlx = "0.8.6"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,41 @@
|
||||||
|
use std::{hash::{Hash, Hasher}, sync::Arc};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(sqlx::Type, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
#[derive(sqlx::Type, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")]
|
#[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")]
|
||||||
pub enum StopType {
|
pub enum PlatformLocationType {
|
||||||
FarSide,
|
FarSide,
|
||||||
MiddleBlockNearSide,
|
MiddleBlockNearSide,
|
||||||
Normal
|
Normal
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Stop {
|
pub enum StopType {
|
||||||
pub id: String,
|
SinglePlatform(Arc<Platform>),
|
||||||
|
MultiPlatform(Vec<Arc<Platform>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Platform {
|
||||||
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub lat: f64,
|
pub lat: f64,
|
||||||
pub lng: f64,
|
pub lng: f64,
|
||||||
pub stop_type: StopType
|
pub platform_location: PlatformLocationType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Stop {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub platforms: StopType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Stop {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Stop {}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,51 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::{Datelike, Days, TimeZone, Weekday};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::direction::Direction;
|
use crate::{direction::Direction, route::Route, stop::Platform};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StopSchedule {
|
pub struct StopSchedule {
|
||||||
pub arrival_time: i64,
|
pub arrival_time: i64,
|
||||||
pub stop_sequence: i64,
|
pub stop_sequence: i64,
|
||||||
pub stop: crate::stop::Stop
|
pub stop: Arc<crate::stop::Stop>,
|
||||||
|
pub platform: Arc<Platform>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Trip {
|
pub struct Trip {
|
||||||
pub service_id: String,
|
pub service_id: String,
|
||||||
|
pub route: Arc<Route>,
|
||||||
pub trip_id: String,
|
pub trip_id: String,
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub tracking_data: TripTracking,
|
pub tracking_data: TripTracking,
|
||||||
pub schedule: Vec<StopSchedule>
|
pub schedule: Vec<StopSchedule>,
|
||||||
|
pub calendar_day: Arc<CalendarDay>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trip {
|
||||||
|
pub fn is_active_on(&self, datetime: &chrono::NaiveDateTime) -> bool {
|
||||||
|
if !self.calendar_day.is_calendar_active_for_date(&datetime.date()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_trip_start = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.first().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap();
|
||||||
|
let mut dt_trip_start = chrono::NaiveDateTime::new(datetime.date(), time_trip_start);
|
||||||
|
|
||||||
|
if self.schedule.first().unwrap().arrival_time > (60*60*24) {
|
||||||
|
dt_trip_start = dt_trip_start.checked_add_days(Days::new(1)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_trip_end = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.last().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap();
|
||||||
|
let mut dt_trip_end = chrono::NaiveDateTime::new(datetime.date(), time_trip_end);
|
||||||
|
|
||||||
|
if self.schedule.last().unwrap().arrival_time > (60*60*24) {
|
||||||
|
dt_trip_end = dt_trip_end.checked_add_days(Days::new(1)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return *datetime >= dt_trip_start && *datetime <= dt_trip_end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -25,6 +55,38 @@ pub enum TripTracking {
|
||||||
Cancelled
|
Cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CalendarDay {
|
||||||
|
pub id: String,
|
||||||
|
pub monday: bool,
|
||||||
|
pub tuesday: bool,
|
||||||
|
pub wednesday: bool,
|
||||||
|
pub thursday: bool,
|
||||||
|
pub friday: bool,
|
||||||
|
pub saturday: bool,
|
||||||
|
pub sunday: bool,
|
||||||
|
pub start_date: chrono::NaiveDate,
|
||||||
|
pub end_date: chrono::NaiveDate
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarDay {
|
||||||
|
pub fn is_calendar_active_for_date(&self, date: &chrono::NaiveDate) -> bool {
|
||||||
|
if *date < self.start_date || *date > self.end_date {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match date.weekday() {
|
||||||
|
Weekday::Mon => self.monday,
|
||||||
|
Weekday::Tue => self.tuesday,
|
||||||
|
Weekday::Wed => self.wednesday,
|
||||||
|
Weekday::Thu => self.thursday,
|
||||||
|
Weekday::Fri => self.friday,
|
||||||
|
Weekday::Sat => self.saturday,
|
||||||
|
Weekday::Sun => self.sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LiveTrip {
|
pub struct LiveTrip {
|
||||||
pub delay: f64,
|
pub delay: f64,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue