diff --git a/Cargo.toml b/Cargo.toml index 15f60ee..7562955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ zip = "8.3" csv = "1.4" [profile.dev] -opt-level = 0 +opt-level = 3 [profile.release] opt-level = 3 diff --git a/resources/gtfs.rs b/resources/gtfs.rs new file mode 100644 index 0000000..6d4db39 --- /dev/null +++ b/resources/gtfs.rs @@ -0,0 +1,132 @@ +use gtfs_structures::{Agency, Calendar, CalendarDate, Route, Stop}; +use serde::{de::DeserializeOwned}; +use std::{collections::HashMap, fs::File}; +use zip::ZipArchive; + +// The main GTFS struct. This is similar to (but not exactly) gtfs-structures::Gtfs because we don't need everything +#[derive(Debug)] +pub struct Gtfs { + /// All agencies. They can not be read by `agency_id`, as it is not a required field + pub agencies: Vec, + /// All Calendar by `service_id` + pub calendar: HashMap, + /// All calendar dates grouped by service_id + pub calendar_dates: HashMap>, + /// All routes by `route_id` + pub routes: HashMap, + /// All stop by `stop_id`. + pub stops: HashMap, +} + +// Utility function to load all records in a dataset +fn load_all(_: &V) -> bool { true } + +// Loads a vector of the selected type +fn load_vector( + destination: &mut Vec, + zip_reader: &mut ZipArchive, + table_name: &str, +) { + let file_reader = zip_reader.by_name(table_name).unwrap(); + let mut rdr = csv::Reader::from_reader(file_reader); + + for row in rdr.deserialize() { + let record: T = row.unwrap(); + destination.push(record); + } +} + +// Loads a HashMap of the selected type, using the provided index function as the key +fn load_map<'a, V: DeserializeOwned>( + destination: &mut HashMap, + zip_reader: &mut ZipArchive, + table_name: &str, + index: fn(&V) -> String, + accept: fn(&V) -> bool, +) { + let file_reader = zip_reader.by_name(table_name).unwrap(); + let mut rdr = csv::Reader::from_reader(file_reader); + + for row in rdr.deserialize() { + let record: V = row.unwrap(); + if accept(&record) { + let idx: String = index(&record); + destination.insert(idx, record); + } + } +} + +// Loads a HashMap of a vector of the selected type, using the provided index function as the key +// And a predicate as a filter +fn load_vector_map<'a, V: DeserializeOwned + Clone>( + destination: &mut HashMap>, + zip_reader: &mut ZipArchive, + table_name: &str, + index: fn(&V) -> String, + accept: fn(&V) -> bool, +) { + let file_reader = zip_reader.by_name(table_name).unwrap(); + let mut rdr = csv::Reader::from_reader(file_reader); + + for row in rdr.deserialize() { + let record: V = row.unwrap(); + if accept(&record) { + let idx: String = index(&record); + destination.entry(idx).or_insert_with(Vec::new).push(record); + } + } +} + +pub fn init(src_file: &str, routes: Vec) -> Gtfs { + // Open zip file + let mut zip_reader = zip::ZipArchive::new(File::open(src_file).unwrap()).unwrap(); + + let mut gtfs: Gtfs = Gtfs { + agencies: Vec::new(), + calendar: HashMap::new(), + calendar_dates: HashMap::new(), + routes: HashMap::new(), + stops: HashMap::new(), + }; + + // Agencies + load_vector(&mut gtfs.agencies, &mut zip_reader, "agency.txt"); + + // Calendars + load_map( + &mut gtfs.calendar, + &mut zip_reader, + "calendar.txt", + |c: &Calendar| String::from(&c.id), + load_all + ); + + // Calendar Dates + load_vector_map( + &mut gtfs.calendar_dates, + &mut zip_reader, + "calendar_dates.txt", + |d: &CalendarDate| String::from(&d.service_id), + load_all + ); + + // Stops + load_map(&mut gtfs.stops, + &mut zip_reader, + "stops.txt", + |s: &Stop| {String::from(&s.id)}, + load_all + ); + + // Routes + let accept_filter: fn(&Route) -> bool = (| rs: Vec, r: &Route | {rs.contains(&r.short_name.unwrap())}).curry(routes); + load_map( + &mut gtfs.routes, + &mut zip_reader, + "routes.txt", + |r: &Route| String::from(&r.id), + accept_filter + ); + + return gtfs; +} diff --git a/src/gtfs/loader.rs b/src/gtfs/loader.rs index b3298d5..94f2761 100644 --- a/src/gtfs/loader.rs +++ b/src/gtfs/loader.rs @@ -1,6 +1,6 @@ use log::debug; use gtfs_structures::{Calendar, CalendarDate, RawStopTime, RawTrip, Route, Stop}; -use serde::de::{DeserializeOwned, value::MapAccessDeserializer}; +use serde::de::{DeserializeOwned}; use std::{ collections::{HashMap, HashSet}, fs::File, diff --git a/src/gtfs/mod.rs b/src/gtfs/mod.rs index 6dce226..a6f8149 100644 --- a/src/gtfs/mod.rs +++ b/src/gtfs/mod.rs @@ -2,7 +2,7 @@ mod arrival; mod loader; mod utils; pub mod structs; -use chrono::{DateTime, Local, NaiveTime, Timelike}; +use chrono::{DateTime, Local, Timelike}; use log::{debug}; use std::{ collections::{HashMap, HashSet}, fs::File, io::Error diff --git a/src/gtfs/structs.rs b/src/gtfs/structs.rs index 330c310..5842b5e 100644 --- a/src/gtfs/structs.rs +++ b/src/gtfs/structs.rs @@ -1,6 +1,5 @@ use std::collections::{HashMap, HashSet}; -use chrono::NaiveTime; use gtfs_structures::{Agency, Calendar, CalendarDate, RawStopTime, RawTrip, Route, Stop}; // This is to store the preferences for the GTFS(-R) side of the code. @@ -32,7 +31,7 @@ pub struct Gtfs { #[derive(Debug)] pub struct Arrival<'a> { - pub departure_time: i64, + pub departure_time: u32, pub route: &'a Route, pub stop: &'a Stop, pub stop_time: &'a RawStopTime, diff --git a/src/gtfs/utils.rs b/src/gtfs/utils.rs index 3ee86df..8fac982 100644 --- a/src/gtfs/utils.rs +++ b/src/gtfs/utils.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use crate::gtfs::Gtfs; -pub fn stop_ids_from_codes(gtfs: &Gtfs, stop_codes: &HashSet) -> HashSet { +pub(crate) fn stop_ids_from_codes(gtfs: &Gtfs, stop_codes: &HashSet) -> HashSet { let mut ids: HashSet = HashSet::new(); for stop in >fs.stops { @@ -13,7 +13,7 @@ pub fn stop_ids_from_codes(gtfs: &Gtfs, stop_codes: &HashSet) -> HashSet return ids; } -pub fn route_ids_from_numbers(gtfs: &Gtfs, route_numbers: &HashSet) -> HashSet { +pub(crate) fn route_ids_from_numbers(gtfs: &Gtfs, route_numbers: &HashSet) -> HashSet { let mut ids: HashSet = HashSet::new(); for route in >fs.routes { diff --git a/src/main.rs b/src/main.rs index 172c5e8..f146571 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ mod gtfs; mod renderer; -use std::{collections::HashSet, ops::Add, os::unix::process::ExitStatusExt, process, thread::Builder, time::SystemTime}; -use chrono::{DateTime, Duration, Local, NaiveTime, Timelike}; -use log::{Metadata, Record, debug, error, info}; +use std::{collections::HashSet, ops::Add, process, thread::Builder, time::SystemTime}; +use chrono::{DateTime, Duration, Local, NaiveTime}; +use log::{Metadata, Record, error, info}; use sdl3::event::Event; use crate::{gtfs::structs::{Arrival, Gtfs}, renderer::structs::{DisplayData, DisplayEntry, Screen}}; const SRC_FILE: &str = "/home/nahuel/Downloads/GTFS_Realtime.zip"; const NUM_ARRIVALS: usize = 4; +const UPDATE_INTERVAL_SECONDS: u64 = 62; + // Custom Event to signal data refresh #[derive(Debug)] @@ -31,8 +33,6 @@ fn refresh_schedule<'a>(gtfs: &'a Gtfs, screen : &mut Screen<'a>) -> Option::new(), status: None @@ -46,7 +46,7 @@ fn refresh_schedule<'a>(gtfs: &'a Gtfs, screen : &mut Screen<'a>) -> Option { pub fn get_context(&self) -> &Sdl { @@ -35,25 +31,36 @@ impl Screen<'_> { return COLOR_LCD_RED; } + fn format_due_for(&self, due_in: i32, departure_time: u32) -> String { + if due_in < 60 { + return String::from("due"); + } + if due_in < 3600 { + return ((due_in / 60) as i32).to_string() + "min"; + } + return format!("{:02}:{:02}", (departure_time / 3600) as i32, ((departure_time % 3600) / 60) as i32); + } + pub fn update_information(&mut self, display_data: &DisplayData) { self.do_clear(); // TODO: Add header - let num_arrivals: i32 = min(if display_data.status.is_some() {LINE_COUNT - 1} else {LINE_COUNT}, display_data.lines.len().try_into().unwrap()); - for line in 0..num_arrivals { + let num_arrivals: i32 = min(LINE_COUNT, display_data.lines.len() as i32); + for index in 0..num_arrivals { + let line: u32 = ((num_arrivals - 1) - index) as u32; // Compose a line of text with all the information let entry = display_data.lines.get(line as usize).unwrap(); - let line: u32 = (line + 1).try_into().unwrap(); - let due_in_mins = entry.due_in as i32; - let arrival_color: Color = self.color_for(due_in_mins); + let due_in_mins = (entry.departure_time / 60) as i32; + let due_color: Color = self.color_for(due_in_mins); + let due_text = self.format_due_for(due_in_mins, entry.departure_time); self.color = COLOR_LCD_AMBER; self.do_print_at(line, &entry.route, XOFFSET_ROUTE); self.do_print_at(line, &entry.destination, XOFFSET_DESTINATION); - self.color = arrival_color; - self.do_print_at(line, &due_in_mins.to_string(), XOFFSEET_TIME_LEFT); + self.color = due_color; + self.do_print_at(line, &due_text, XOFFSEET_TIME_LEFT); }; if display_data.status.is_some() { @@ -63,14 +70,14 @@ impl Screen<'_> { } - pub fn print(&mut self, line: u32, text: &str) { + pub fn _print(&mut self, line: u32, text: &str) { self.do_print_at(line, text, 0); self.do_update(); } fn do_print_at(&mut self, line: u32, text: &str, left: u32) -> u32 { - let rendered_text = self.font.render(text).solid(self.color).unwrap(); + let rendered_text = self.font.render(text).lcd(self.color, COLOR_BACKGROUND).unwrap(); let texture_creator = self.canvas.texture_creator(); let texture = rendered_text.as_texture(&texture_creator).unwrap(); let _= self.canvas.copy(&texture, @@ -92,7 +99,7 @@ impl Screen<'_> { fn do_clear(&mut self) { - self.canvas.set_draw_color(Color::BLACK); + self.canvas.set_draw_color(COLOR_BACKGROUND); self.canvas.clear(); } @@ -112,8 +119,8 @@ impl Screen<'_> { // Load font let ttf_context = sdl3::ttf::init().unwrap(); - let font = ttf_context.load_font(&prefs.font_path, 128.0).unwrap(); - + let font = ttf_context.load_font(&prefs.font_path, TEXT_SIZE).unwrap(); + let mut screen: Screen = Screen { canvas: Box::new(window.into_canvas()), font: Box::new(font), diff --git a/src/renderer/structs.rs b/src/renderer/structs.rs index 5132c32..6e96573 100644 --- a/src/renderer/structs.rs +++ b/src/renderer/structs.rs @@ -18,7 +18,7 @@ pub struct Prefs { pub struct DisplayEntry { pub route: String, pub destination: String, - pub due_in: i64, + pub departure_time: u32, } pub struct DisplayData {