diff --git a/README.md b/README.md index 190e4af..a1af357 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,17 @@ As of today, the flag `--azure-sas-token` is only available when the code is exe from `main`, as writing to the blob storage must be done through a controlled code base that preserves data integrity. +### Examples: + +```bash +# Story about Danish private jets that flew to Davos between two dates +cargo run --example country -- --from=2024-01-13 --to=2024-01-21 --country=denmark --location=davos +# Story about Danish private jets that flew between two dates +cargo run --example country -- --from=2024-01-13 --to=2024-01-21 --country=denmark +# Story about Portuguese private jets that flew between two dates +cargo run --example country -- --from=2024-01-13 --to=2024-01-21 --country=portugal +``` + ## Methodology The methodology used to extract information is available at [`methodology.md`](./methodology.md). diff --git a/examples/country.md b/examples/country.md new file mode 100644 index 0000000..7ab1623 --- /dev/null +++ b/examples/country.md @@ -0,0 +1,27 @@ +# Use of {country.possessive} private jets{location} + +Between {from_date} and {to_date}, {country.possessive} private jets{location} emitted +{emissions_tons.claim} tons of CO2e[^1], or what **{citizen_years.claim} {country.plural}** emit in a year[^2]. +{number_of_private_jets.claim} private jets alone[^3] jeopardize the efforts of millions +of {country.plural} that every day act to reduce their emissions. + +Of these, +* {number_of_legs_less_300km} trips were less than 300 km[^4] and could have been replaced by + a limusine or first class train ticket, which would have emitted ~17x less CO2[^5]. +* {number_of_legs_more_300km} trips[^4] could have been replaced by + a first class ticket on a commercial aircraft, which would have emitted + ~{ratio_commercial_300km}x less CO2[^1]. + +The use of private jets by billionaires and large companies alike is an insult +to {country.plural} and {country.possessive} companies alike that are doing everything they can +to reduce emissions. + +> Ban private jets _now_ + +## References + +[^1]: {emissions_tons.source}, in {number_of_legs.claim} trips[^4] - retrieved on {emissions_tons.date} +[^2]: {citizen_years.source} - retrieved on {citizen_years.date} +[^3]: {number_of_private_jets.source} on {number_of_private_jets.date} +[^4]: {number_of_legs.source} - retrieved on {number_of_legs.date} +[^5]: 10x vs a commercial flight[^1] plus 7x vs a train, as per https://ourworldindata.org/travel-carbon-footprint (UK data, vary by country) - retrieved on 2024-01-20 diff --git a/examples/dk_jets.rs b/examples/country.rs similarity index 55% rename from examples/dk_jets.rs rename to examples/country.rs index 905c674..c63cdfb 100644 --- a/examples/dk_jets.rs +++ b/examples/country.rs @@ -4,13 +4,13 @@ use clap::Parser; use num_format::{Locale, ToFormattedString}; use simple_logger::SimpleLogger; -use flights::{emissions, load_aircraft_types, load_aircrafts, Class, Fact, Leg}; +use flights::{emissions, load_aircrafts, load_private_jet_types, Class, Fact, Leg, Position}; use time::Date; fn render(context: &Context) -> Result<(), Box> { - let path = "all_dk_jets.md"; + let path = format!("{}_story.md", context.country.name.to_lowercase()); - let template = std::fs::read_to_string("examples/dk_jets.md")?; + let template = std::fs::read_to_string("examples/country.md")?; let mut tt = tinytemplate::TinyTemplate::new(); tt.set_default_formatter(&tinytemplate::format_unescaped); @@ -23,14 +23,23 @@ fn render(context: &Context) -> Result<(), Box> { Ok(()) } +#[derive(serde::Serialize)] +pub struct CountryContext { + pub name: String, + pub plural: String, + pub possessive: String, +} + #[derive(serde::Serialize)] pub struct Context { + pub country: CountryContext, + pub location: String, pub from_date: String, pub to_date: String, pub number_of_private_jets: Fact, pub number_of_legs: Fact, pub emissions_tons: Fact, - pub dane_years: Fact, + pub citizen_years: Fact, pub number_of_legs_less_300km: String, pub number_of_legs_more_300km: String, pub ratio_commercial_300km: String, @@ -49,27 +58,119 @@ fn parse_date(arg: &str) -> Result { ) } +#[derive(clap::ValueEnum, Debug, Clone)] +enum Country { + Denmark, + Portugal, +} + +#[derive(clap::ValueEnum, Debug, Clone, Copy)] +enum Location { + Davos, +} + +impl Location { + fn name(&self) -> &'static str { + match self { + Self::Davos => "Davos airport (LSZR)", + } + } + + fn region(&self) -> [[f64; 2]; 2] { + match self { + Self::Davos => [[47.482, 9.538], [47.490, 9.568]], + } + } +} + +impl Country { + fn to_context(&self) -> CountryContext { + CountryContext { + name: self.name().to_string(), + plural: self.plural().to_string(), + possessive: self.possessive().to_string(), + } + } + + fn possessive(&self) -> &'static str { + match self { + Self::Denmark => "Danish", + Self::Portugal => "Portuguese", + } + } + + fn plural(&self) -> &'static str { + match self { + Self::Denmark => "Danes", + Self::Portugal => "Portugueses", + } + } + + fn tail_number(&self) -> &'static str { + match self { + Country::Denmark => "OY-", + Country::Portugal => "CS-", + } + } + + fn name(&self) -> &'static str { + match self { + Country::Denmark => "Denmark", + Country::Portugal => "Portugal", + } + } + + fn emissions(&self) -> Fact { + match self { + Country::Denmark => 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(), + }, + Country::Portugal => Fact { + claim: 4.1, + source: "A portuguese emitted 4.1 t CO2/person/year in 2022 according to [work bank data](https://ourworldindata.org/co2/country/denmark).".to_string(), + date: "2024-01-23".to_string(), + }, + } + } +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { /// The Azure token - #[arg(short, long)] + #[arg(long)] azure_sas_token: Option, - #[arg(short, long, value_enum, default_value_t=Backend::Azure)] + #[arg(long, value_enum, default_value_t=Backend::Azure)] backend: Backend, + /// Name of the country to compute on + #[arg(long)] + country: Country, + /// A date in format `yyyy-mm-dd` #[arg(long, value_parser = parse_date)] from: time::Date, /// Optional end date in format `yyyy-mm-dd` (else it is to today) #[arg(long, value_parser = parse_date)] to: Option, + + /// Optional location to restrict the search geographically. Currently only + #[arg(long)] + location: Option, +} + +pub fn in_box(position: &Position, region: [[f64; 2]; 2]) -> bool { + return (position.latitude() >= region[0][0] && position.latitude() < region[1][0]) + && (position.longitude() >= region[0][1] && position.longitude() < region[1][1]); } async fn legs( from: Date, to: Date, icao_number: &str, + location: Option, client: Option<&flights::fs_azure::ContainerClient>, ) -> Result, Box> { let positions = flights::aircraft_positions(from, to, icao_number, client).await?; @@ -81,7 +182,18 @@ async fn legs( positions.sort_unstable_by_key(|p| p.datetime()); log::info!("Computing legs {}", icao_number); - Ok(flights::legs(positions.into_iter())) + let legs = flights::legs(positions.into_iter()); + + // filter by location + if let Some(location) = location { + let region = location.region(); + Ok(legs + .into_iter() + .filter(|leg| leg.positions().iter().any(|p| in_box(p, region))) + .collect()) + } else { + Ok(legs) + } } #[tokio::main] @@ -109,33 +221,26 @@ async fn main() -> Result<(), Box> { // load datasets to memory let aircrafts = load_aircrafts(client.as_ref()).await?; - let types = load_aircraft_types()?; + let types = load_private_jet_types()?; let private_jets = aircrafts .into_iter() // is private jet .filter(|(_, a)| types.contains_key(&a.model)) - // is from DK - .filter(|(a, _)| a.starts_with("OY-")) + // from country + .filter(|(a, _)| a.starts_with(cli.country.tail_number())) .collect::>(); - let number_of_private_jets = Fact { - claim: private_jets.len().to_formatted_string(&Locale::en), - source: format!( - "All aircrafts in [adsbexchange.com](https://globe.adsbexchange.com) whose model is a private jet and tail number starts with \"OY-\"" - ), - date: "2023-11-06".to_string(), - }; - + let now = time::OffsetDateTime::now_utc().date(); let from = cli.from; - let to = cli.to.unwrap_or(time::OffsetDateTime::now_utc().date()); + let to = cli.to.unwrap_or(now); let from_date = from.to_string(); let to_date = to.to_string(); let client = client.as_ref(); let legs = private_jets.iter().map(|(_, aircraft)| async { - legs(from, to, &aircraft.icao_number, client) + legs(from, to, &aircraft.icao_number, cli.location, client) .await .map(|legs| (aircraft.icao_number.clone(), legs)) }); @@ -143,6 +248,14 @@ async fn main() -> Result<(), Box> { let legs = futures::future::join_all(legs).await; let legs = legs.into_iter().collect::, _>>()?; + let number_of_private_jets = Fact { + claim: legs.iter().filter(|x| x.1.len() > 0).count().to_formatted_string(&Locale::en), + source: format!( + "All aircrafts in [adsbexchange.com](https://globe.adsbexchange.com) whose model is a private jet, registered in {}, and with at least one leg - ", cli.country.name() + ), + date: "2023-11-06".to_string(), + }; + let number_of_legs = Fact { claim: legs .iter() @@ -152,7 +265,7 @@ async fn main() -> Result<(), Box> { source: format!( "[adsbexchange.com](https://globe.adsbexchange.com) between {from_date} and {to_date}" ), - date: to.to_string(), + date: now.to_string(), }; let commercial_emissions_tons = legs @@ -180,26 +293,27 @@ async fn main() -> Result<(), Box> { .map(|(_, legs)| legs.iter().filter(|leg| leg.distance() >= 300.0).count()) .sum::(); - 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 = (emissions_tons_value / dane_emissions_tons.claim) as usize; - let dane_years = Fact { - claim: dane_years.to_formatted_string(&Locale::en), - 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 citizen_emissions_tons = cli.country.emissions(); + + let citizen_years = (emissions_tons_value / citizen_emissions_tons.claim) as usize; + let citizen_years = Fact { + claim: citizen_years.to_formatted_string(&Locale::en), + source: citizen_emissions_tons.source, + date: citizen_emissions_tons.date, }; let context = Context { + country: cli.country.to_context(), + location: cli + .location + .map(|l| format!(" at {}", l.name())) + .unwrap_or_default(), from_date, to_date, number_of_private_jets, number_of_legs, emissions_tons, - dane_years, + citizen_years, number_of_legs_less_300km: short_legs.to_formatted_string(&Locale::en), number_of_legs_more_300km: long_legs.to_formatted_string(&Locale::en), ratio_commercial_300km: format!("{:.0}", commercial_to_private_ratio), diff --git a/examples/dk_jets.md b/examples/dk_jets.md deleted file mode 100644 index 7be322a..0000000 --- a/examples/dk_jets.md +++ /dev/null @@ -1,26 +0,0 @@ -# Use of Danish private jets - -{from_date} to {to_date}, all Danish private jets emitted -{emissions_tons.claim} tons of CO2e[^1], or what **{dane_years.claim} Danes** emit in a year[^2]. -{number_of_private_jets.claim} private jets alone[^3] jeopardize the efforts of millions -of Danes that every day act to reduce their emissions. - -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]. - -The use of private jets by billionaires and large companies alike is an insult -to Danes and Danish companies alike that are doing everything they can to -keep for our future. - -> Ban private jets _now_ - -## References - -[^1]: {emissions_tons.source}, in {number_of_legs.claim} trips[^4] - retrieved on {emissions_tons.date} -[^2]: {dane_years.source} - retrieved on {dane_years.date} -[^3]: {number_of_private_jets.source} on {number_of_private_jets.date} -[^4]: {number_of_legs.source} - retrieved on {number_of_legs.date} diff --git a/examples/export_private_jets.rs b/examples/export_private_jets.rs index 9898b63..db0ad5b 100644 --- a/examples/export_private_jets.rs +++ b/examples/export_private_jets.rs @@ -4,7 +4,7 @@ use clap::Parser; use simple_logger::SimpleLogger; use flights::BlobStorageProvider; -use flights::{load_aircraft_types, load_aircrafts}; +use flights::{load_aircrafts, load_private_jet_types}; #[derive(clap::ValueEnum, Debug, Clone)] enum Backend { @@ -52,7 +52,7 @@ async fn main() -> Result<(), Box> { // load datasets to memory let aircrafts = load_aircrafts(client.as_ref()).await?; - let types = load_aircraft_types()?; + let types = load_private_jet_types()?; let private_jets = aircrafts .values() diff --git a/src/aircraft_types.rs b/src/aircraft_types.rs index cac04d1..385eb29 100644 --- a/src/aircraft_types.rs +++ b/src/aircraft_types.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// A map of the aircraft type (e.g. `CL30`) to an [`AircraftType`]. pub type AircraftTypes = HashMap; -/// The in-memory representation of an aircraft type +/// In-memory representation of an aircraft type #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AircraftType { /// the type (e.g. `CL30`) @@ -18,9 +18,11 @@ pub struct AircraftType { pub date: String, } -/// Loads [`AircraftType`] from `src/types.csv` into memory. +/// Returns the set of all [`AircraftType`] in `src/types.csv`, +/// corresponding to aircraft types whose primary use is to be a private jet +/// according to the [methodology `M-2`](../methodology.md). /// # Error /// Errors if the file cannot be read -pub fn load_aircraft_types() -> Result> { +pub fn load_private_jet_types() -> Result> { super::csv::load("src/types.csv", |a: AircraftType| (a.icao.clone(), a)) }