Skip to content

Commit

Permalink
Airports and flights
Browse files Browse the repository at this point in the history
  • Loading branch information
Celeo committed Mar 12, 2024
1 parent 12600a6 commit fda9b46
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 11 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ reqwest = { version = "0.11.24", features = ["json"] }
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite", "chrono", "uuid"] }
thousands = "0.2.0"
tokio = { version = "1.36.0", features = ["full"] }
toml = "0.8.10"
tower = "0.4.13"
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/homepage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async fn snippet_flights(State(state): State<Arc<AppState>>) -> Result<Html<Stri
}

// cache this endpoint's returned data for 60 seconds
let cache_key = "ONLINE_FLIGHTS";
let cache_key = "ONLINE_FLIGHTS_HOMEPAGE";
if let Some(cached) = state.cache.get(&cache_key) {
let elapsed = Instant::now() - cached.inserted;
if elapsed.as_secs() < 60 {
Expand Down
6 changes: 3 additions & 3 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! HTTP endpoints.
use crate::shared::{AppState, UserInfo, SESSION_USER_INFO_KEY};
use crate::shared::{AppError, AppState, UserInfo, SESSION_USER_INFO_KEY};
use anyhow::Result;
use axum::{extract::State, http::StatusCode, response::Html, routing::get, Router};
use axum::{extract::State, response::Html, routing::get, Router};
use minijinja::{context, Environment};
use std::sync::Arc;
use tower_sessions::Session;
Expand All @@ -17,7 +17,7 @@ pub mod pilots;
async fn handler_404(
State(state): State<Arc<AppState>>,
session: Session,
) -> Result<Html<String>, StatusCode> {
) -> Result<Html<String>, AppError> {
let user_info: Option<UserInfo> = session.get(SESSION_USER_INFO_KEY).await.unwrap();
let template = state.templates.get_template("404").unwrap();
let rendered = template.render(context! { user_info }).unwrap();
Expand Down
107 changes: 103 additions & 4 deletions src/endpoints/pilots.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::{
shared::{sql::INSERT_FEEDBACK, AppError, AppState, UserInfo, SESSION_USER_INFO_KEY},
utils::flashed_messages,
shared::{
sql::INSERT_FEEDBACK, AppError, AppState, CacheEntry, UserInfo, SESSION_USER_INFO_KEY,
},
utils::{flashed_messages, simaware_data},
};
use axum::{
extract::State,
Expand All @@ -9,9 +11,11 @@ use axum::{
Form, Router,
};
use minijinja::{context, Environment};
use serde::Deserialize;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use thousands::Separable;
use tower_sessions::Session;
use vatsim_utils::live_api::Vatsim;

/// View the feedback form.
///
Expand Down Expand Up @@ -72,6 +76,93 @@ async fn post_feedback_form(
Ok(Redirect::to("/pilots/feedback"))
}

/// Table of all the airspace's airports.
async fn handler_airports(
State(state): State<Arc<AppState>>,
session: Session,
) -> Result<Html<String>, AppError> {
let user_info: Option<UserInfo> = session.get(SESSION_USER_INFO_KEY).await.unwrap();
let template = state.templates.get_template("airports").unwrap();
let airports = &state.config.airports.all;
let rendered = template.render(context! { user_info, airports }).unwrap();
Ok(Html(rendered))
}

/// Table of all airspace-relevant flights.
async fn handler_flights(
State(state): State<Arc<AppState>>,
session: Session,
) -> Result<Html<String>, AppError> {
#[derive(Serialize, Default)]
struct OnlineFlight<'a> {
pilot_name: &'a str,
pilot_cid: u64,
callsign: &'a str,
departure: &'a str,
arrival: &'a str,
altitude: String,
speed: String,
simaware_id: &'a str,
}

// cache this endpoint's returned data for 60 seconds
let cache_key = "ONLINE_FLIGHTS_FULL";
if let Some(cached) = state.cache.get(&cache_key) {
let elapsed = Instant::now() - cached.inserted;
if elapsed.as_secs() < 60 {
return Ok(Html(cached.data));
}
state.cache.invalidate(&cache_key);
}

let artcc_fields: Vec<_> = state
.config
.airports
.all
.iter()
.map(|airport| &airport.code)
.collect();
let vatsim_data = Vatsim::new().await?.get_v3_data().await?;
let simaware_data = simaware_data().await?;
let flights: Vec<OnlineFlight> = vatsim_data
.pilots
.iter()
.flat_map(|flight| {
if let Some(plan) = &flight.flight_plan {
let from = artcc_fields.contains(&&plan.departure);
let to = artcc_fields.contains(&&plan.arrival);
if from || to {
Some(OnlineFlight {
pilot_name: &flight.name,
pilot_cid: flight.cid,
callsign: &flight.callsign,
departure: &plan.departure,
arrival: &plan.arrival,
altitude: flight.altitude.separate_with_commas(),
speed: flight.groundspeed.separate_with_commas(),
simaware_id: match simaware_data.get(&flight.cid) {
Some(id) => id,
None => "",
},
})
} else {
None
}
} else {
None
}
})
.collect();

let user_info: Option<UserInfo> = session.get(SESSION_USER_INFO_KEY).await.unwrap();
let template = state.templates.get_template("flights")?;
let rendered = template.render(context! { user_info, flights })?;
state
.cache
.insert(cache_key, CacheEntry::new(rendered.clone()));
Ok(Html(rendered))
}

/// This file's routes and templates.
pub fn router(templates: &mut Environment) -> Router<Arc<AppState>> {
templates
Expand All @@ -80,8 +171,16 @@ pub fn router(templates: &mut Environment) -> Router<Arc<AppState>> {
include_str!("../../templates/pilot_feedback.jinja"),
)
.unwrap();
templates
.add_template("airports", include_str!("../../templates/airports.jinja"))
.unwrap();
templates
.add_template("flights", include_str!("../../templates/flights.jinja"))
.unwrap();

Router::new()
.route("/pilots/feedback", get(page_feedback_form))
.route("/pilots/feedback", post(post_feedback_form))
.route("/pilots/airports", get(handler_airports))
.route("/pilots/flights", get(handler_flights))
}
4 changes: 4 additions & 0 deletions src/shared/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ mod config;
pub use config::{Config, DEFAULT_CONFIG_FILE_NAME};
pub mod sql;

/// Wrapper around `anyhow`'s `Error` type, which is itself a wrapper
/// around the stdlib's `Error` type.
pub struct AppError(anyhow::Error);

impl IntoResponse for AppError {
Expand All @@ -36,13 +38,15 @@ where
}
}

/// Data wrapper for items in the server-side cache.
#[derive(Clone)]
pub struct CacheEntry {
pub inserted: Instant,
pub data: String,
}

impl CacheEntry {
/// Wrap the data with a timestamp.
pub fn new(data: String) -> Self {
Self {
inserted: Instant::now(),
Expand Down
35 changes: 34 additions & 1 deletion src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::collections::HashMap;

use anyhow::{anyhow, Result};
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use serde::Serialize;
use reqwest::ClientBuilder;
use serde::{Deserialize, Serialize};

pub mod auth;
pub mod flashed_messages;
Expand Down Expand Up @@ -81,6 +84,36 @@ pub fn parse_metar(line: &str) -> Result<AirportWeather> {
})
}

/// Query the SimAware data endpoint for its data on active pilot sessions.
///
/// This endpoint should be cached so as to not hit the SimAware server too frequently.
pub async fn simaware_data() -> Result<HashMap<u64, String>> {
#[derive(Deserialize)]
struct Pilot {
cid: u64,
}

#[derive(Deserialize)]
struct TopLevel {
pilots: HashMap<String, Pilot>,
}

let mut mapping = HashMap::new();
let client = ClientBuilder::new()
.user_agent("github.com/celeo/vzdv")
.build()?;
let data: TopLevel = client
.get("https://r2.simaware.ca/api/livedata/data.json")
.send()
.await?
.json()
.await?;
for (id, pilot) in data.pilots {
mapping.insert(pilot.cid, id);
}
Ok(mapping)
}

#[cfg(test)]
pub mod tests {
use super::{parse_metar, parse_vatsim_timestamp, WeatherConditions};
Expand Down
6 changes: 4 additions & 2 deletions templates/404.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

{% block body %}

<h2>Something went wrong</h2>
<h4>That page couldn't be found.</h4>
<div class="text-center">
<h3>That page couldn't be found.</h3>
</div>

{% endblock %}
40 changes: 40 additions & 0 deletions templates/airports.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{% extends "layout" %}

{% block title %}Airports | {{ super() }}{% endblock %}

{% block body %}

<h2>Airports</h2>

<table class="table table-striped table-hover">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Location</th>
<th>Towered</th>
<th>SkyVector</th>
</tr>
</thead>
<tbody>
{% for airport in airports %}
<tr>
<td>{{ airport.code }}</td>
<td>{{ airport.name }}</td>
<td>{{ airport.location }}</td>
<td>
{% if airport.towered %}
Yes (Class {{ airport.class }})
{% else %}
No
{% endif %}
</td>
<td>
<a href="https://skyvector.com/api/airportSearch?query={{ airport.code }}" target="_blank">Link</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

{% endblock %}
36 changes: 36 additions & 0 deletions templates/flights.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% extends "layout" %}

{% block title %}Flights | {{ super() }}{% endblock %}

{% block body %}

<h2>Flights</h2>

<table class="table table-striped table-hover">
<thead>
<tr>
<th>Pilot</th>
<th>Callsign</th>
<th>Departure</th>
<th>Arrival</th>
<th>Altitude</th>
<th>Speed</th>
</tr>
</thead>
{% for flight in flights %}
<tr>
<td>
<a href="https://stats.vatsim.net/stats/{{ flight.pilot_cid }}" target="_blank">{{ flight.pilot_name }}</a>
</td>
<td>
<a href="https://simaware.ca/?uid={{ flight.simaware_id }}" target="_blank">{{ flight.callsign }}</a>
</td>
<td>{{ flight.departure }}</td>
<td>{{ flight.arrival }}</td>
<td>{{ flight.altitude }}</td>
<td>{{ flight.speed }}</td>
</tr>
{% endfor %}
</table>

{% endblock %}

0 comments on commit fda9b46

Please sign in to comment.