Skip to content

Commit

Permalink
Added example to create period-based stories (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgecardleitao authored Nov 19, 2023
1 parent 8d4c493 commit 188996e
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 56 deletions.
159 changes: 159 additions & 0 deletions examples/period.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use std::error::Error;

use flights::{
emissions, load_aircraft_owners, load_aircrafts, load_owners, Aircraft, Class, Company, Fact,
};

#[derive(serde::Serialize)]
pub struct Context {
pub owner: Fact<Company>,
pub aircraft: Aircraft,
pub from_date: String,
pub to_date: String,
pub number_of_legs: Fact<usize>,
pub emissions_tons: Fact<usize>,
pub dane_years: Fact<String>,
pub number_of_legs_less_300km: usize,
pub number_of_legs_more_300km: usize,
pub ratio_commercial_300km: String,
}

fn render(context: &Context) -> Result<(), Box<dyn Error>> {
let path = "story.md";

let template = std::fs::read_to_string("examples/template.md")?;

let mut tt = tinytemplate::TinyTemplate::new();
tt.set_default_formatter(&tinytemplate::format_unescaped);
tt.add_template("t", &template)?;

let rendered = tt.render("t", context)?;

println!("Story written to {path}");
std::fs::write(path, rendered)?;
Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
let owners = load_owners()?;
let aircraft_owners = load_aircraft_owners()?;
let aircrafts = load_aircrafts()?;

let to = time::OffsetDateTime::now_utc().date() - time::Duration::days(1);
let from = to - time::Duration::days(90);

let tail_number = "OY-GFS";
let aircraft = aircrafts
.get(tail_number)
.ok_or_else(|| Into::<Box<dyn Error>>::into("Aircraft ICAO number not found"))?
.clone();
let aircraft_owner = aircraft_owners
.get(tail_number)
.ok_or_else(|| Into::<Box<dyn Error>>::into("Owner of tail number not found"))?;
println!("Aircraft owner: {}", aircraft_owner.owner);
let company = owners
.get(&aircraft_owner.owner)
.ok_or_else(|| Into::<Box<dyn Error>>::into("Owner not found"))?;
println!("Owner information found");
let owner = Fact {
claim: company.clone(),
source: aircraft_owner.source.clone(),
date: aircraft_owner.date.clone(),
};

let icao = &aircraft.icao_number;
println!("ICAO number: {}", icao);

let iter = flights::DateIter {
from,
to,
increment: time::Duration::days(1),
};

let mut positions = vec![];
for date in iter {
positions.extend(flights::positions(icao, &date, 1000.0)?);
}

let legs = flights::legs(positions.into_iter());
let legs = legs
.into_iter()
// ignore legs that are too fast, as they are likely noise
.filter(|leg| leg.duration() > time::Duration::minutes(5))
// ignore legs that are too short, as they are likely noise
.filter(|leg| leg.distance() > 3.0)
// ignore legs that are too low, as they are likely noise
.filter(|leg| leg.maximum_altitude > 1000.0)
.collect::<Vec<_>>();
println!("number_of_legs: {}", legs.len());
for leg in &legs {
println!(
"{},{},{},{},{},{},{},{},{}",
leg.from.datetime(),
leg.from.latitude(),
leg.from.longitude(),
leg.from.altitude(),
leg.to.datetime(),
leg.to.latitude(),
leg.to.longitude(),
leg.to.altitude(),
leg.maximum_altitude
);
}

let commercial_to_private_ratio = 10.0;
let commercial_emissions_tons = legs
.iter()
.map(|leg| emissions(leg.from.pos(), leg.to.pos(), Class::First) / 1000.0)
.sum::<f64>();
let emissions_tons = Fact {
claim: (commercial_emissions_tons * commercial_to_private_ratio) as usize,
source: format!("Commercial flights would have emitted {commercial_emissions_tons:.1} tons of CO2e (based on [myclimate.org](https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/) - retrieved on 2023-10-19). Private jets emit 5-14x times. 10x was used based on [transportenvironment.org](https://www.transportenvironment.org/discover/private-jets-can-the-super-rich-supercharge-zero-emission-aviation/)"),
date: "2023-10-05, from 2021-05-27".to_string(),
};

let short_legs = legs.iter().filter(|leg| leg.distance() < 300.0);
let long_legs = legs.iter().filter(|leg| leg.distance() >= 300.0);

let dane_emissions_tons = Fact {
claim: 5.1,
source: "A dane emitted 5.1 t CO2/person/year in 2019 according to [work bank data](https://ourworldindata.org/co2/country/denmark).".to_string(),
date: "2023-10-08".to_string(),
};

let dane_years = format!(
"{:.0}",
emissions_tons.claim as f32 / dane_emissions_tons.claim as f32
);
let dane_years = Fact {
claim: dane_years,
source: "https://ourworldindata.org/co2/country/denmark Denmark emits 5.1 t CO2/person/year in 2019.".to_string(),
date: "2023-10-08".to_string(),
};

let from_date = from.to_string();
let to_date = to.to_string();

let number_of_legs = Fact {
claim: legs.len(),
source: format!("[adsbexchange.com](https://globe.adsbexchange.com/?icao={icao}) between {from_date} and {to_date}"),
date: to.to_string()
};

let context = Context {
owner,
aircraft,
from_date,
to_date,
number_of_legs,
emissions_tons,
dane_years,
number_of_legs_less_300km: short_legs.count(),
number_of_legs_more_300km: long_legs.count(),
ratio_commercial_300km: format!("{:.0}", commercial_to_private_ratio),
};

render(&context)?;

Ok(())
}
29 changes: 29 additions & 0 deletions examples/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# {owner.claim.name} use of private jets

{owner.claim.name} {owner.claim.statement.claim}[^1].

Yet, between {from_date} and {to_date}, their staff travelled **{number_of_legs.claim} times**[^2] on {owner.claim.name}'s own private jet, {aircraft.tail_number}[^3].

> These {number_of_legs.claim} trips emitted {emissions_tons.claim} tons of CO2. A Dane
takes **{dane_years.claim} years** to emit this amount of CO2[^4].

Of these,
* {number_of_legs_less_300km} trips were less than 300 km and could have been replaced by
a limusine or first class train ticket, which would have emitted ~30-50x less CO2.
* {number_of_legs_more_300km} trips could have been replaced by
a first class ticket on a commercial aircraft, which would have emitted
~{ratio_commercial_300km}x less CO2[^5].

Billionaires and companies alike are incapable of regulating their emissions,
recklessly destroying the common good.
Ban private jets now and until they emit what equivalent means of transportation would emit.

## References

[^1]: {owner.claim.statement.source} - retrieved on {owner.claim.statement.date}
[^2]: {number_of_legs.source} - retrieved on {number_of_legs.date}
[^3]: {owner.source} - retrieved on {owner.date}
[^4]: {dane_years.source} - retrieved on {dane_years.date}
[^5]: {emissions_tons.source} - retrieved on {emissions_tons.date}

Copyright Jorge Leitão, released under [CC0](https://creativecommons.org/public-domain/cc0/) - No Rights Reserved.
4 changes: 2 additions & 2 deletions src/aircraft_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use std::collections::HashMap;
use std::error::Error;

use reqwest;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json;

/// [`HashMap`] between tail number (e.g. "OY-TWM") and an [`Aircraft`]
pub type Aircrafts = HashMap<String, Aircraft>;

/// An in-memory representation of an aircraft data
#[derive(Deserialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Aircraft {
/// The ICAO number of the aircraft (e.g. `459CD3`)
pub icao_number: String,
Expand Down
2 changes: 1 addition & 1 deletion src/emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn distance_to_emissions(distance: f64, class: Class) -> f64 {
}
}

/// Returns emissions of a commercial flight flying `from` -> `to`, in kg of eCO2.
/// Returns emissions of a commercial flight flying `from` -> `to`, in kg of CO2e.
/// The exact calculation is described here: https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/
pub fn emissions(from: (f64, f64), to: (f64, f64), class: Class) -> f64 {
distance_to_emissions(super::distance(from, to), class)
Expand Down
52 changes: 2 additions & 50 deletions src/icao_to_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use reqwest::header;
use reqwest::{self, StatusCode};
use time::PrimitiveDateTime;

use super::Position;

fn last_2(icao: &str) -> &str {
let bytes = icao.as_bytes();
std::str::from_utf8(&bytes[bytes.len() - 2..]).unwrap()
Expand Down Expand Up @@ -127,56 +129,6 @@ pub fn trace_cached(
Ok(std::mem::take(trace))
}

/// A position of an aircraft
#[derive(Debug, Clone, Copy)]
pub enum Position {
/// Aircraft transponder declares the aircraft is grounded
Grounded {
datetime: PrimitiveDateTime,
latitude: f64,
longitude: f64,
},
/// Aircraft transponder declares the aircraft is flying at a given altitude
Flying {
datetime: PrimitiveDateTime,
latitude: f64,
longitude: f64,
altitude: f64,
},
}

impl Position {
pub fn latitude(&self) -> f64 {
match *self {
Position::Flying { latitude, .. } | Position::Grounded { latitude, .. } => latitude,
}
}

pub fn longitude(&self) -> f64 {
match *self {
Position::Flying { longitude, .. } | Position::Grounded { longitude, .. } => longitude,
}
}

pub fn pos(&self) -> (f64, f64) {
(self.latitude(), self.longitude())
}

pub fn altitude(&self) -> f64 {
match *self {
Position::Flying { altitude, .. } => altitude,
Position::Grounded { .. } => 0.0,
}
}

pub fn datetime(&self) -> PrimitiveDateTime {
match *self {
Position::Flying { datetime, .. } => datetime,
Position::Grounded { datetime, .. } => datetime,
}
}
}

/// Returns an iterator of [`Position`] over the trace of `icao` on day `date` assuming that
/// a flight below `threshold` feet is grounded.
pub fn positions(
Expand Down
25 changes: 22 additions & 3 deletions src/legs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ use crate::Position;
pub struct Leg {
pub from: Position,
pub to: Position,
/// in feet
pub maximum_altitude: f64,
}

impl Leg {
/// Leg geo distance in km
pub fn distance(&self) -> f64 {
self.from.distace(&self.to)
}

/// Leg duration
pub fn duration(&self) -> time::Duration {
self.to.datetime() - self.from.datetime()
}
}

/// Returns a set of [`Leg`]s from a sequence of [`Position`]s.
Expand All @@ -16,13 +30,16 @@ pub fn legs(mut positions: impl Iterator<Item = Position>) -> Vec<Leg> {

let first = prev_position;
let mut legs: Vec<Leg> = vec![];
let mut maximum_altitude = prev_position.altitude();
positions.for_each(|position| {
maximum_altitude = position.altitude().max(maximum_altitude);
match (prev_position, position) {
(Position::Grounded { .. }, Position::Flying { .. }) => {
// departed, still do not know to where
legs.push(Leg {
from: prev_position,
to: prev_position,
maximum_altitude,
});
}
(Position::Flying { .. }, Position::Grounded { .. }) => {
Expand All @@ -35,17 +52,19 @@ pub fn legs(mut positions: impl Iterator<Item = Position>) -> Vec<Leg> {
legs.push(Leg {
from: first,
to: position,
})
maximum_altitude,
});
maximum_altitude = 0.0
}
}
_ => {}
};
prev_position = position;
});

// if it is still flying, we leave the last leg as incomplete.
// if it is still flying, remove the incomplete leg
if matches!(prev_position, Position::Flying { .. }) && !legs.is_empty() {
legs.last_mut().unwrap().to = prev_position;
legs.pop();
}

legs
Expand Down
Loading

0 comments on commit 188996e

Please sign in to comment.