add preliminary nta support
This commit is contained in:
parent
a7d323056a
commit
1d66553398
21 changed files with 3318 additions and 257 deletions
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"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
|
|
@ -646,16 +640,16 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1606,7 +1600,7 @@ dependencies = [
|
|||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.1",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
|
|
@ -1908,6 +1902,8 @@ dependencies = [
|
|||
name = "libseptastic"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ img {
|
|||
.rr-container {
|
||||
background-color: #4c748c;
|
||||
color: #ffffff;
|
||||
font-size: 1.5em;
|
||||
font-size: 1.2em;
|
||||
padding: 0.3em;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
|
|
@ -81,6 +81,35 @@ img {
|
|||
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 {
|
||||
background-color: #FF661C;
|
||||
color: #ffffff;
|
||||
|
|
@ -121,6 +150,8 @@ img {
|
|||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
aspect-ratio: 3/2;
|
||||
line-height: 1;
|
||||
}
|
||||
.tscroll {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,38 @@
|
|||
gtfs_zips:
|
||||
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
||||
subzip: "google_rail.zip"
|
||||
prefix: "SEPTARAIL"
|
||||
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
||||
prefix: "SEPTABUS"
|
||||
subzip: "google_bus.zip"
|
||||
# - uri: "https://www.njtransit.com/rail_data.zip"
|
||||
# - uri: "https://www.njtransit.com/bus_data.zip"
|
||||
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:
|
||||
- id: 'NYC'
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
pub mod route;
|
||||
pub mod stop;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder};
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant};
|
||||
use libseptastic::{direction, route::RouteType, stop_schedule::Trip};
|
||||
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")]
|
||||
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();
|
||||
|
||||
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.")),
|
||||
widescreen: false,
|
||||
content: crate::templates::RoutesTemplate {
|
||||
|
|
@ -75,106 +76,37 @@ async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyho
|
|||
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();
|
||||
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
|
||||
}
|
||||
|
||||
enum RoutingNodeType {
|
||||
Origin,
|
||||
Destination,
|
||||
Midpoint
|
||||
}
|
||||
|
||||
struct RoutingNodePointer {
|
||||
pub stop_id: String,
|
||||
pub route_id: String,
|
||||
pub stop_sequence: u64,
|
||||
pub direction: u64
|
||||
}
|
||||
|
||||
struct RoutingNode {
|
||||
pub node_type: RoutingNodeType,
|
||||
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("/directions")]
|
||||
//async fn get_directions(state: Data<Arc<AppState>>) -> impl Responder {
|
||||
// let near_thresh = 0.45;
|
||||
//
|
||||
// let sig_cds = routing::Coordinates {
|
||||
// lat: 40.008420,
|
||||
// lng: -75.213439
|
||||
// };
|
||||
//
|
||||
// let home_cds = routing::Coordinates {
|
||||
// lat: 39.957210,
|
||||
// lng: -75.166214
|
||||
// };
|
||||
//
|
||||
// let all_stops = state.gtfs_service.get_all_stops();
|
||||
//
|
||||
//
|
||||
//
|
||||
// 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);
|
||||
//
|
||||
// let mut graph = construct_graph(sig_cds, &all_stops, &state.gtfs_service);
|
||||
//
|
||||
// let mut response = String::new();
|
||||
// for stop in &origin_stops {
|
||||
// response += bfs_rts(&stop, &mut graph, &dest_stops).as_str();
|
||||
// }
|
||||
//
|
||||
// return response;
|
||||
//}
|
||||
|
||||
#[get("/route/{route_id}")]
|
||||
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 statex = state.clone();
|
||||
async move {
|
||||
let mut filters: Option<Vec<i64>> = None;
|
||||
let mut filters: Option<Vec<String>> = None;
|
||||
if let Some (stops_v) = infox.stops.clone() {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for sid in stops_v.split(",") {
|
||||
items.push(sid.parse::<i64>().unwrap());
|
||||
items.push(String::from(sid));
|
||||
}
|
||||
filters = Some(items);
|
||||
}
|
||||
|
|
@ -201,7 +133,7 @@ async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Quer
|
|||
|
||||
Ok(crate::templates::ContentTemplate {
|
||||
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())),
|
||||
content: crate::templates::RouteTemplate {
|
||||
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 controllers;
|
||||
mod templates;
|
||||
//mod routing;
|
||||
|
||||
pub struct AppState {
|
||||
gtfs_service: services::gtfs_pull::GtfsPullService,
|
||||
|
|
@ -51,7 +52,8 @@ where T: Template,
|
|||
.cookie(cookie)
|
||||
.body(y.render().unwrap())
|
||||
},
|
||||
Err(_) => {
|
||||
Err(err) => {
|
||||
error!("Returned error b/c {:?}", err);
|
||||
HttpResponse::InternalServerError().body("Error")
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +107,9 @@ async fn main() -> ::anyhow::Result<()> {
|
|||
.service(controllers::route::get_route)
|
||||
.service(controllers::route::get_routes_json)
|
||||
.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(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 libseptastic::{stop::Platform, stop_schedule::CalendarDay};
|
||||
use log::{info, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zip::ZipArchive;
|
||||
|
||||
|
||||
macro_rules! make_global_id {
|
||||
($prefix: expr, $id: expr) => (format!("{}_{}", $prefix, $id))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||
struct GtfsSource {
|
||||
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)]
|
||||
pub struct Config {
|
||||
pub gtfs_zips: Vec<GtfsSource>
|
||||
pub gtfs_zips: Vec<GtfsSource>,
|
||||
pub annotations: Annotations
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -23,17 +43,24 @@ struct GtfsFile {
|
|||
}
|
||||
|
||||
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 trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
|
||||
pub stops: HashMap<String, libseptastic::stop::Stop>,
|
||||
pub route_id_by_stops: HashMap<String, HashSet<String>>
|
||||
pub stops: HashMap<String, Arc<libseptastic::stop::Stop>>,
|
||||
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 {
|
||||
pub gtfs_files: Vec<GtfsFile>,
|
||||
pub tmp_dir: PathBuf,
|
||||
pub ready: bool,
|
||||
pub annotations: Annotations,
|
||||
pub transit_data: TransitData
|
||||
}
|
||||
|
||||
|
|
@ -43,12 +70,13 @@ pub struct GtfsPullService {
|
|||
|
||||
impl TransitData {
|
||||
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 {
|
||||
const UPDATE_SECONDS: u64 = 3600*24;
|
||||
const READYSTATE_CHECK_MILLISECONDS: u64 = 500;
|
||||
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
|
|
@ -56,6 +84,7 @@ impl GtfsPullService {
|
|||
GtfsPullServiceState {
|
||||
gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(),
|
||||
tmp_dir: env::temp_dir(),
|
||||
annotations: config.annotations.clone(),
|
||||
ready: false,
|
||||
transit_data: TransitData::new()
|
||||
}
|
||||
|
|
@ -65,7 +94,9 @@ impl GtfsPullService {
|
|||
|
||||
pub fn wait_for_ready(&self) {
|
||||
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> {
|
||||
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> {
|
||||
let l_state = self.state.lock().unwrap();
|
||||
if let Some(route) = l_state.transit_data.routes.get(&route_id) {
|
||||
Ok(route.clone())
|
||||
Ok(libseptastic::route::Route::clone(route))
|
||||
} else {
|
||||
Err(anyhow!(""))
|
||||
}
|
||||
|
|
@ -104,10 +135,10 @@ impl GtfsPullService {
|
|||
|
||||
pub fn get_all_routes(&self) -> HashMap<String, libseptastic::route::Route> {
|
||||
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();
|
||||
l_state.transit_data.stops.clone()
|
||||
}
|
||||
|
|
@ -117,9 +148,22 @@ impl GtfsPullService {
|
|||
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();
|
||||
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>> {
|
||||
|
|
@ -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<()> {
|
||||
let mut l_state = state.lock().unwrap();
|
||||
let files = l_state.gtfs_files.clone();
|
||||
l_state.transit_data = TransitData::new();
|
||||
|
||||
let mut gtfses = Vec::new();
|
||||
|
||||
for gtfs_file in files.iter() {
|
||||
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();
|
||||
file_path.push(subzip.clone());
|
||||
|
||||
info!("Downloaded, parsing");
|
||||
|
||||
gtfs_structures::Gtfs::new(file_path.to_str().unwrap())?
|
||||
} else {
|
||||
|
|
@ -152,115 +344,35 @@ impl GtfsPullService {
|
|||
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 {
|
||||
l_state.transit_data.agencies.insert(a_id.clone(), libseptastic::agency::Agency{
|
||||
id: a_id.clone(),
|
||||
name: agency.name.clone()
|
||||
});
|
||||
hack_agency = Some(libseptastic::agency::Agency{
|
||||
id: a_id.clone(),
|
||||
name: agency.name.clone()
|
||||
});
|
||||
}
|
||||
|
||||
info!("Data loaded, processing...");
|
||||
|
||||
for (gtfs, prefix) in >fses {
|
||||
GtfsPullService::populate_routes(&mut l_state, &prefix, >fs)?;
|
||||
GtfsPullService::populate_stops(&mut l_state, &prefix, >fs)?;
|
||||
for calendar in >fs.calendar {
|
||||
l_state.transit_data.calendar_days.insert(calendar.1.id.clone(), Arc::new(CalendarDay{
|
||||
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 {
|
||||
let agency = route.1.agency_id.as_ref()
|
||||
.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());
|
||||
for (gtfs, prefix) in >fses {
|
||||
GtfsPullService::populate_trips(&mut l_state, &prefix, >fs)?;
|
||||
}
|
||||
|
||||
l_state.ready = true;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub struct ContentTemplate<T: askama::Template> {
|
|||
pub struct RouteTemplate {
|
||||
pub route: libseptastic::route::Route,
|
||||
pub timetables: Vec<TimetableDirection>,
|
||||
pub filter_stops: Option<Vec<i64>>
|
||||
pub filter_stops: Option<Vec<String>>
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
|
|
@ -32,6 +32,12 @@ pub struct RoutesTemplate {
|
|||
pub bus_routes: Vec<libseptastic::route::Route>
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "stops.html")]
|
||||
pub struct StopsTemplate {
|
||||
pub tc_stops: Vec<libseptastic::stop::Stop>,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct IndexTemplate {
|
||||
|
|
@ -39,7 +45,7 @@ pub struct IndexTemplate {
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TimetableStopRow {
|
||||
pub stop_id: i64,
|
||||
pub stop_id: String,
|
||||
pub stop_name: String,
|
||||
pub stop_sequence: i64,
|
||||
pub times: Vec<Option<i64>>
|
||||
|
|
@ -55,6 +61,21 @@ pub struct TimetableDirection {
|
|||
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(
|
||||
directions: Vec<Direction>,
|
||||
|
|
@ -120,7 +141,7 @@ pub fn build_timetables(
|
|||
let mut rows: Vec<TimetableStopRow> = stop_map
|
||||
.into_iter()
|
||||
.map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
|
||||
stop_id: stop_id.parse().unwrap(),
|
||||
stop_id,
|
||||
stop_sequence,
|
||||
stop_name,
|
||||
times,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
|
||||
{% if let Some(title) = page_title %}
|
||||
<title>{{ title }}</title>
|
||||
<title>{{ title }} | SEPTASTIC</title>
|
||||
{% else %}
|
||||
<title>SEPTASTIC</title>
|
||||
{% endif %}
|
||||
|
|
@ -58,6 +58,7 @@ window.onload = function () {
|
|||
<div>
|
||||
<a class="nav-link" href="/">[ Home ]</a>
|
||||
<a class="nav-link" href="/routes">[ Routes ]</a>
|
||||
<a class="nav-link" href="/stops">[ Stops ]</a>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
.train-direction-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: sans-serif;
|
||||
font-family: mono;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
|
@ -148,11 +148,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
{% let live_o = timetable.tracking_data[loop.index0] %}
|
||||
{% if let Tracked(live) = live_o %}
|
||||
{% 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">
|
||||
{% endif %}
|
||||
<span style="color: #22bb22"> {{ time | format_time }} </span>
|
||||
</td>
|
||||
{% 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue