add live updating table
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 9m20s
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 9m20s
This commit is contained in:
parent
2ca76548d6
commit
6773e6ae30
7 changed files with 210 additions and 124 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
use actix_web::{get, web::{self, Data}, HttpRequest, Responder};
|
use actix_web::{HttpRequest, HttpResponse, Responder, get, web::{self, Data}};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use chrono::Timelike;
|
use askama::Template;
|
||||||
|
use chrono::{NaiveTime, Timelike};
|
||||||
use chrono_tz::America::New_York;
|
use chrono_tz::America::New_York;
|
||||||
use libseptastic::stop_schedule::{self, LiveTrip, Trip, TripTracking};
|
use libseptastic::stop_schedule::{self, LiveTrip, Trip, TripTracking};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
@ -34,6 +35,118 @@ async fn get_stops_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Re
|
||||||
}
|
}
|
||||||
}).await
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_trip_perspective_for_stop(state: &Data<Arc<AppState>>,stop: &libseptastic::stop::Stop) -> Vec<TripPerspective> {
|
||||||
|
let routes: Vec<libseptastic::route::Route> = state.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| {
|
||||||
|
match state.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 = state.gtfs_service.get_all_trips().iter().filter_map(|trip| {
|
||||||
|
if route_ids.contains(trip.0) {
|
||||||
|
Some(trip.1.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).flatten().collect();
|
||||||
|
|
||||||
|
|
||||||
|
state.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.get_arrival_time(&live);
|
||||||
|
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|
|
||||||
|
match &f.trip.tracking_data {
|
||||||
|
TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live),
|
||||||
|
_ => f.perspective_stop.arrival_time
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered_trips
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/stop/{stop_id}/table")]
|
||||||
|
async fn get_stop_table_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||||
|
let stop_id = path;
|
||||||
|
|
||||||
|
if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) {
|
||||||
|
let filtered_trips = get_trip_perspective_for_stop(&state, &stop).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());
|
||||||
|
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(crate::templates::StopTableTemplate {
|
||||||
|
trips: filtered_trips,
|
||||||
|
current_time: cur_time
|
||||||
|
}.render().unwrap())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/stop/{stop_id}")]
|
#[get("/stop/{stop_id}")]
|
||||||
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||||
crate::perform_action(req, move || {
|
crate::perform_action(req, move || {
|
||||||
|
|
@ -44,6 +157,7 @@ async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
if let Some(stop) = statex.gtfs_service.get_stop_by_id(&stop_id) {
|
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| {
|
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()) {
|
match statex.gtfs_service.get_route(route.clone()) {
|
||||||
Ok(route) => Some(route),
|
Ok(route) => Some(route),
|
||||||
|
|
@ -51,83 +165,13 @@ async fn get_stop_html(req: HttpRequest, state: Data<Arc<AppState>>, path: web::
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
let route_ids: HashSet<String> = routes.iter().map(|route| route.id.clone()).collect();
|
let filtered_trips = get_trip_perspective_for_stop(&statex, &stop).await;
|
||||||
|
|
||||||
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_utc = chrono::Utc::now();
|
||||||
let now = now_utc.with_timezone(&New_York);
|
let now = now_utc.with_timezone(&New_York);
|
||||||
let naive_time = now.time();
|
let naive_time = now.time();
|
||||||
let cur_time = i64::from(naive_time.num_seconds_from_midnight());
|
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.get_arrival_time(&live);
|
|
||||||
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|
|
|
||||||
match &f.trip.tracking_data {
|
|
||||||
TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live),
|
|
||||||
_ => f.perspective_stop.arrival_time
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(crate::templates::ContentTemplate {
|
Ok(crate::templates::ContentTemplate {
|
||||||
page_title: Some(stop.name.clone()),
|
page_title: Some(stop.name.clone()),
|
||||||
page_desc: Some(String::from("Stop information")),
|
page_desc: Some(String::from("Stop information")),
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ async fn main() -> ::anyhow::Result<()> {
|
||||||
// .service(controllers::route::get_directions)
|
// .service(controllers::route::get_directions)
|
||||||
.service(controllers::stop::get_stops_html)
|
.service(controllers::stop::get_stops_html)
|
||||||
.service(controllers::stop::get_stop_html)
|
.service(controllers::stop::get_stop_html)
|
||||||
|
.service(controllers::stop::get_stop_table_html)
|
||||||
.service(get_index)
|
.service(get_index)
|
||||||
.service(actix_files::Files::new("/assets", "./assets"))
|
.service(actix_files::Files::new("/assets", "./assets"))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,13 @@ pub struct StopTemplate {
|
||||||
pub current_time: i64
|
pub current_time: i64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "stop_table_impl.html")]
|
||||||
|
pub struct StopTableTemplate {
|
||||||
|
pub trips: Vec<TripPerspective>,
|
||||||
|
pub current_time: i64
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_timetables(
|
pub fn build_timetables(
|
||||||
directions: Vec<Direction>,
|
directions: Vec<Direction>,
|
||||||
trips: Vec<Trip>,
|
trips: Vec<Trip>,
|
||||||
|
|
@ -183,6 +190,7 @@ mod filters {
|
||||||
return Ok(format!("{}ns", nanos));
|
return Ok(format!("{}ns", nanos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_time(
|
pub fn format_time(
|
||||||
seconds_since_midnight: &i64,
|
seconds_since_midnight: &i64,
|
||||||
_: &dyn askama::Values,
|
_: &dyn askama::Values,
|
||||||
|
|
@ -190,8 +198,10 @@ mod filters {
|
||||||
let total_minutes = seconds_since_midnight / 60;
|
let total_minutes = seconds_since_midnight / 60;
|
||||||
let (hours, ampm) = {
|
let (hours, ampm) = {
|
||||||
let hrs = total_minutes / 60;
|
let hrs = total_minutes / 60;
|
||||||
if hrs >= 12 {
|
if hrs > 12 {
|
||||||
(hrs - 12, "PM")
|
(hrs - 12, "PM")
|
||||||
|
} else if hrs == 12 {
|
||||||
|
(12, "PM")
|
||||||
} else if hrs > 0 {
|
} else if hrs > 0 {
|
||||||
(hrs, "AM")
|
(hrs, "AM")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -201,4 +211,26 @@ mod filters {
|
||||||
let minutes = total_minutes % 60;
|
let minutes = total_minutes % 60;
|
||||||
Ok(format!("{}:{:02} {}", hours, minutes, ampm))
|
Ok(format!("{}:{:02} {}", hours, minutes, ampm))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_time_with_seconds(
|
||||||
|
seconds_since_midnight: &i64,
|
||||||
|
_: &dyn askama::Values,
|
||||||
|
) -> askama::Result<String> {
|
||||||
|
let total_minutes = seconds_since_midnight / 60;
|
||||||
|
let (hours, ampm) = {
|
||||||
|
let hrs = total_minutes / 60;
|
||||||
|
if hrs > 12 {
|
||||||
|
(hrs - 12, "PM")
|
||||||
|
} else if hrs == 12 {
|
||||||
|
(12, "PM")
|
||||||
|
} else if hrs > 0 {
|
||||||
|
(hrs, "AM")
|
||||||
|
} else {
|
||||||
|
(12, "AM")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let minutes = total_minutes % 60;
|
||||||
|
let seconds = seconds_since_midnight % 60;
|
||||||
|
Ok(format!("{}:{:02}:{:02} {}", hours, minutes, seconds, ampm))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</head>
|
</head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
{%- import "route_symbol.html" as scope -%}
|
{%- import "route_symbol.html" as scope -%}
|
||||||
|
{%- import "stop_table.html" as stop_table -%}
|
||||||
<style>
|
|
||||||
.trip-desc {
|
|
||||||
display: flex;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<h1>{{ stop.name }}</h1>
|
<h1>{{ stop.name }}</h1>
|
||||||
|
|
@ -20,7 +14,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{#{% if let libseptastic::stop::StopType::MultiPlatform(platforms) = stop.platforms %}
|
{#{% if let libseptastic::stop::StopType::MultiPlatform(platforms) = stop.platforms %}
|
||||||
<div>
|
<div>
|
||||||
<p>Platforms at this station:</p>
|
<p>Platforms at this station:</p>
|
||||||
|
|
@ -30,45 +23,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}#}
|
{% endif %}#}
|
||||||
|
|
||||||
<table class="train-direction-table">
|
<div hx-get="/stop/{{ stop.id }}/table" hx-trigger="every 5s">
|
||||||
<tr>
|
{% call stop_table::stop_table(trips, current_time) %}
|
||||||
<th>ROUTE</th>
|
</div>
|
||||||
<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 let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
|
||||||
<td style="color: #008800">
|
|
||||||
<p style="font-size: small;">{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}</p>
|
|
||||||
<p style="font-size: x-small; font-style: italic;">{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins</p>
|
|
||||||
</td>
|
|
||||||
{% else %}
|
|
||||||
<td>
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
|
||||||
<td>
|
|
||||||
{{ tracked_trip.vehicle_ids.join(", ") }}
|
|
||||||
</td>
|
|
||||||
{% else %}
|
|
||||||
<td>
|
|
||||||
-
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
|
||||||
51
api/templates/stop_table.html
Normal file
51
api/templates/stop_table.html
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{%- import "route_symbol.html" as scope -%}
|
||||||
|
|
||||||
|
{% macro stop_table(trips, current_time) %}
|
||||||
|
<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 let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||||
|
<td style="color: #008800">
|
||||||
|
<p style="font-size: small;">{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}</p>
|
||||||
|
<p style="font-size: x-small; font-style: italic;">{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins</p>
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||||
|
<td>
|
||||||
|
{{ tracked_trip.vehicle_ids.join(", ") }}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
<p>Updated at: {{ current_time | format_time_with_seconds }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endmacro %}
|
||||||
3
api/templates/stop_table_impl.html
Normal file
3
api/templates/stop_table_impl.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{%- import "stop_table.html" as stop_table -%}
|
||||||
|
|
||||||
|
{% call stop_table::stop_table(trips, current_time) %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue