commit 774d4e4c8005ee5ff10657f8974ff49c4ba536cb Author: Nahuel Lofeudo Date: Sat Mar 28 08:02:32 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff0d847 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/.vscode +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1ff9fbf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rs-dublinbus" +version = "0.1.0" +edition = "2024" +host = "x86_64-unknown-linux-gnu" + +[dependencies] +sdl3 = "0.17.*" +sdl3-ttf-sys = "0.6.*" +serde = "*" +gtfs-structures = "0.47.*" +zip = "8.3.*" +csv = "1.4.*" + +[profile.dev] +opt-level = 0 + +[profile.release] +opt-level = 3 diff --git a/install.md b/install.md new file mode 100644 index 0000000..cb16117 --- /dev/null +++ b/install.md @@ -0,0 +1,20 @@ +## Install ARM toolchains: + +``` +cargo install cross +rustup target add aarch64-unknown-linux-gnu # 64-bit Pi OS +rustup target add armv7-unknown-linux-gnueabihf # 32-bit Pi 2/3/4 OS +rustup target add arm-unknown-linux-gnueabihf # ARMv6 (Pi Zero/1) + +sudo apt-get update +sudo apt-get install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf +``` + +## Cross-compile + +``` +# 2) Build +~/.cargo/bin/cross build --target aarch64-unknown-linux-gnu --release +~/.cargo/bin/cross build --target armv7-unknown-linux-gnueabihf --release +~/.cargo/bin/cross build --target arm-unknown-linux-gnueabihf --release +``` \ No newline at end of file diff --git a/src/gtfs.rs b/src/gtfs.rs new file mode 100644 index 0000000..47b0963 --- /dev/null +++ b/src/gtfs.rs @@ -0,0 +1,255 @@ +use gtfs_structures::{Agency, Calendar, CalendarDate, RawStopTime, RawTrip, Route, Stop}; +use serde::{self, de::DeserializeOwned}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + hash::Hash, +}; + +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, + /// All trips by trip_id + pub trips: HashMap, + /// Stop times for the chosen stops and the chosen routes + pub stop_times: HashMap<(String, u32), RawStopTime>, +} + +trait Filter { + fn accept(&self, v: &T) -> bool; +} + +// No filter on loaded records +struct LoadAll {} +impl Filter for LoadAll { + fn accept(&self, _: &T) -> bool { + return true; + } +} + +struct LoadRoutes<'a> { + routes: &'a HashSet, +} +impl Filter for LoadRoutes<'_> { + fn accept(&self, r: &Route) -> bool { + let short_name = &r.short_name; + return short_name.is_some() && self.routes.contains(short_name.as_ref().unwrap()); + } +} + +struct LoadStops<'a> { + stops: &'a HashSet, +} +impl Filter for LoadStops<'_> { + fn accept(&self, s: &Stop) -> bool { + let stop_code = &s.code; + return stop_code.is_some() && self.stops.contains(s.code.as_ref().unwrap()); + } +} + +struct LoadTrips<'a> { + route_ids: &'a HashSet, +} +impl Filter for LoadTrips<'_> { + fn accept(&self, t: &RawTrip) -> bool { + let route_id = &t.route_id; + return self.route_ids.contains(route_id); + } +} + +struct LoadStopTimes<'a> { + trip_ids: &'a HashSet, + stop_ids: &'a HashSet, +} +impl Filter for LoadStopTimes<'_> { + fn accept(&self, st: &RawStopTime) -> bool { + return self.stop_ids.contains(&st.stop_id) && self.trip_ids.contains(&st.trip_id); + } +} + +// 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( + destination: &mut HashMap, + zip_reader: &mut ZipArchive, + table_name: &str, + index: IndexFn, + filter: FilterT, +) where + K: Eq + Hash, + V: DeserializeOwned, + IndexFn: Fn(&V) -> K, + FilterT: Filter, + { + let file_reader = zip_reader.by_name(table_name).unwrap(); + let mut rdr = csv::Reader::from_reader(file_reader); + + for row in rdr.deserialize() { + if row.is_ok() { + let record: V = row.unwrap(); + if filter.accept(&record) { + let idx: K = index(&record); + destination.insert(idx, record); + } + } else { + print!("Row failed to deserialize row {:#?}", row.err()); + panic!(); + } + } +} + +// 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, + filter: impl Filter, +) { + 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 filter.accept(&record) { + let idx: String = index(&record); + destination.entry(idx).or_insert_with(Vec::new).push(record); + } + } +} + +fn stop_ids_from_codes(gtfs: &Gtfs, stop_codes: &HashSet) -> HashSet { + let mut ids: HashSet = HashSet::new(); + + for stop in >fs.stops { + let stop_number = stop.1.code.as_ref(); + if stop_number.is_some() && stop_codes.contains(stop_number.unwrap().as_str()) { + ids.insert(stop.0.clone()); + } + } + return ids; +} + +fn route_ids_from_numbers(gtfs: &Gtfs, route_numbers: &HashSet) -> HashSet { + let mut ids: HashSet = HashSet::new(); + + for route in >fs.routes { + let route_number = route.1.short_name.as_ref(); + if route_number.is_some() && route_numbers.contains(route_number.unwrap().as_str()) { + ids.insert(route.0.clone()); + } + } + return ids; +} + +pub fn init(src_file: &str, route_numbers: HashSet, stop_codes: HashSet) -> 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(), + trips: HashMap::new(), + stop_times: 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), + LoadAll {}, + ); + + // Calendar Dates + load_vector_map( + &mut gtfs.calendar_dates, + &mut zip_reader, + "calendar_dates.txt", + |d: &CalendarDate| String::from(&d.service_id), + LoadAll {}, + ); + + // Routes + load_map( + &mut gtfs.routes, + &mut zip_reader, + "routes.txt", + |r: &Route| String::from(&r.id), + LoadRoutes { + routes: &route_numbers, + }, + ); + + // Stops + load_map( + &mut gtfs.stops, + &mut zip_reader, + "stops.txt", + |s: &Stop| String::from(&s.id), + LoadStops { stops: &stop_codes }, + ); + + let route_ids = route_ids_from_numbers(>fs, &route_numbers); + // Trips + load_map( + &mut gtfs.trips, + &mut zip_reader, + "trips.txt", + |t: &RawTrip| String::from(&t.id), + LoadTrips { + route_ids: &route_ids, + }, + ); + + // Load stop times for the chosen routes and stops + let stop_ids = stop_ids_from_codes(>fs, &stop_codes); + let trip_ids = HashSet::::from_iter(gtfs.trips.keys().cloned()); + load_map( + &mut gtfs.stop_times, + &mut zip_reader, + "stop_times.txt", + |st: &RawStopTime| (st.trip_id.clone(), st.stop_sequence), + LoadStopTimes { + trip_ids: &trip_ids, + stop_ids: &stop_ids, + }, + ); + + return gtfs; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1a13ce0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,18 @@ +use std::{collections::HashSet, time::Instant}; + +mod gtfs; + +const SRC_FILE: &str = "/home/nahuel/Downloads/GTFS_Realtime.zip"; + +fn main() { + // Init GTFS static info + for _ in 0..1000 { + let start_gtfs = Instant::now(); + println!("Loading GTFS data..."); + let gtfs = gtfs::init(SRC_FILE, + HashSet::from([String::from("15A"), String::from("F1"), String::from("F2"), String::from("F3")]), + HashSet::from([String::from("1117")])); + println!("Loaded records in {:#?}. Data size: {:#?}", start_gtfs.elapsed(), ::std::mem::size_of_val(>fs)) + } + +}