diff --git a/assets/premium_bus_schedules.csv b/assets/premium_bus_schedules.csv new file mode 100644 index 0000000..f41d9b2 --- /dev/null +++ b/assets/premium_bus_schedules.csv @@ -0,0 +1,161 @@ +sequence_number,route_number_id,route_short_name,bus_stop_name,timings,ETA_in_mins +1,543001,P570,M.G.R.KOYAMBEDU B.T,7.00,0 +2,543001,P570,MMDA COLONY ROAD JUNCTION,7.07,7 +3,543001,P570,VADAPALANI THIRU NAGAR,7.12,5 +4,543001,P570,VADAPALANI S.I.M.S HOSPITAL,7.15,3 +5,543001,P570,VADAPALANI TEMPLE,7.17,2 +6,543001,P570,ASHOK PILLAR,7.24,7 +7,543001,P570,KASI THEATRE,7.27,3 +8,543001,P570,KALAIMAGAL NAGAR,7.30,3 +9,543001,P570,EKKATTUTHANGAL,7.33,3 +10,543001,P570,GUINDY CIPET,7.36,3 +11,543001,P570,TELEPHONE EXCHANGE GUINDY,7.38,2 +12,543001,P570,GUINDY B.T,7.40,2 +13,543001,P570,CHELLAMMAL COLLEGE,7.43,3 +14,543001,P570,GUINDY HALDA,7.48,5 +15,543001,P570,KANNIGAPURAM,7.50,2 +16,543001,P570,VELACHERY CHECK POST,7.52,2 +17,543001,P570,GURU NANAK COLLEGE,7.54,2 +18,543001,P570,VELACHERY GANDHI ROAD,7.56,2 +19,543001,P570,DHANDEESWARAN NAGAR,7.58,2 +20,543001,P570,VELACHERY,8.00,2 +21,543001,P570,VELACHERY TANSI NAGAR,8.02,2 +22,543001,P570,VELACHERY BABY NAGAR,8.04,2 +23,543001,P570,THARAMANI BHARATHI NAGAR,8.06,2 +24,543001,P570,THARAMANI PILLAIYAR TEMPLE,8.08,2 +25,543001,P570,I.R.T. RD.JN,8.10,2 +26,543001,P570,PERUNGUDI S.R.P TOOLS,8.13,3 +27,543001,P570,PERUNGUDI I G P,8.16,3 +28,543001,P570,KANDANCHAVADI,8.20,4 +29,543001,P570,PERUNGUDI,8.25,5 +30,543001,P570,SEEVARAM,8.27,2 +31,543001,P570,D B JAIN COLLEGE,8.29,2 +32,543001,P570,METTUKUPPAM,8.31,2 +33,543001,P570,PTC QUARTERS,8.33,2 +34,543001,P570,OKKIYAMPET,8.35,2 +35,543001,P570,KARAPAKKAM KCG COLLEGE,8.37,2 +36,543001,P570,KARAPAKKAM,8.39,2 +37,543001,P570,SHOLINGANALLUR ACCENTURE,8.41,2 +38,543001,P570,P U OFFICE SHOLINGANALLUR,8.43,2 +39,543001,P570,GOVERNMENT SCHOOL SHOLINGANALLUR,8.44,1 +40,543001,P570,SHOLINGANALLUR PONNIAMMAN TEMPLE,8.45,1 +41,543001,P570,SHOLINGANALLUR KUMARAN NAGAR,8.47,2 +42,543001,P570,SATYABHAMA UNIVERSITY,8.48,1 +43,543001,P570,SEMMANCHERI,8.50,2 +44,543001,P570,NAVALUR AGS COMPLEX,8.51,1 +45,543001,P570,NAVALUR,8.53,2 +46,543001,P570,EGATTUR,8.54,1 +47,543001,P570,SIRUSERI SIPCOT,8.55,1 +48,543001,P570,SIRUSERI TCS GATE 1,8.58,3 +49,543001,P570,SIRUSERI IT PARK,9.00,2 +1,546001,P570,SIRUSERI IT PARK,7.00,0 +2,546001,P570,SIRUSERI TCS GATE 1,7.02,2 +3,546001,P570,SIRUSERI SIPCOT,7.05,3 +4,546001,P570,EGATTUR,7.07,2 +5,546001,P570,NAVALUR,7.09,2 +6,546001,P570,NAVALUR AGS COMPLEX,7.10,1 +7,546001,P570,SEMMANCHERI,7.12,2 +8,546001,P570,SATYABHAMA UNIVERSITY,7.14,2 +9,546001,P570,SHOLINGANALLUR KUMARAN NAGAR,7.15,1 +10,546001,P570,SHOLINGANALLUR PONNIAMMAN TEMPLE,7.17,2 +11,546001,P570,GOVERNMENT SCHOOL SHOLINGANALLUR,7.18,1 +12,546001,P570,P U OFFICE SHOLINGANALLUR,7.20,2 +13,546001,P570,SHOLINGANALLUR ACCENTURE,7.22,2 +14,546001,P570,KARAPAKKAM,7.25,3 +15,546001,P570,KARAPAKKAM KCG COLLEGE,7.27,2 +16,546001,P570,OKKIYAMPET,7.29,2 +17,546001,P570,PTC QUARTERS OR MOOTAKARAN CHAVADI,7.32,3 +18,546001,P570,METTUKUPPAM,7.35,3 +19,546001,P570,D B JAIN COLLEGE,7.37,2 +20,546001,P570,SEEVARAM,7.39,2 +21,546001,P570,PERUNGUDI,7.41,2 +22,546001,P570,KANDANCHAVADI,7.43,2 +23,546001,P570,PERUNGUDI I G P,7.45,2 +24,546001,P570,THARAMANI S R P TOOLS,7.47,2 +25,546001,P570,THARAMANI,7.50,3 +26,546001,P570,THARAMANI PILLAIYAR TEMPLE,7.52,2 +27,546001,P570,THARAMANI BHARATHI NAGAR,7.54,2 +28,546001,P570,VELACHERY BABY NAGAR,7.56,2 +29,546001,P570,VELACHERY TANSI NAGAR,7.58,2 +30,546001,P570,VELACHERY,8.00,2 +31,546001,P570,DHANDEESWARAN NAGAR,8.03,3 +32,546001,P570,VELACHERY GANDHI ROAD,8.06,3 +33,546001,P570,GURU NANAK COLLEGE,8.09,3 +34,546001,P570,VELACHERY CHECK POST,8.14,5 +35,546001,P570,GUINDY PADAVETTAMMAN KOVIL,8.16,2 +36,546001,P570,MADUVANKARAI,8.18,2 +37,546001,P570,GUINDY M K N SALAI,8.20,2 +38,546001,P570,GUINDY CIPET,8.25,5 +39,546001,P570,EKKATTUTHANGAL,8.28,3 +40,546001,P570,KALAIMAGAL NAGAR,8.31,3 +41,546001,P570,KK NAGAR TELEPHONE EXCHANGE JAFFARKHANPET,8.35,4 +42,546001,P570,UDHYAM THEATER OR ASHOK PILLAR,8.38,3 +43,546001,P570,VADAPALANI,8.45,7 +44,546001,P570,VADAPALANI S.I.M.S HOSPITAL,8.48,3 +45,546001,P570,VADAPALANI THIRU NAGAR,8.50,2 +46,546001,P570,MMDA COLONY ROAD JUNCTION,8.55,5 +47,546001,P570,M.G.R.KOYAMBEDU B.T,9.00,5 +1,598001,P91,KILAMBAKKAM BUS TERMINUS,7.00,0 +2,598001,P91,VANDALUR ZOO,7.10,10 +3,598001,P91,VANDALUR GATE,7.12,2 +4,598001,P91,PERUNGALATHUR IRANIYAMMAN TEMPLE,7.13,1 +5,598001,P91,PERUNGALATTUR,7.15,2 +6,598001,P91,PERUNGALATGUR LAKE VIEW STOP,7.17,2 +7,598001,P91,IRUMBULIYUR,7.19,2 +8,598001,P91,TAMBARAM WEST BUS STAND,7.20,1 +9,598001,P91,TAMBARAM SANATORIUM B.T,7.23,3 +10,598001,P91,TAMBARAM TB HOSPITAL,7.24,1 +11,598001,P91,CHROMEPET MIT GATE,7.25,1 +12,598001,P91,CHROMEPET,7.27,2 +13,598001,P91,SARAVANA STORE CHROMEPET,7.29,2 +14,598001,P91,PALLAVARAM NEW BRIDGE,7.35,6 +15,598001,P91,VELS UNIVERSITY,7.40,5 +16,598001,P91,PALLAVA GARDEN BUS STOP OR DJ MET SCHOOL,7.43,3 +17,598001,P91,KOVILAMBAKKAM PERUMAL NAGAR,7.46,3 +18,598001,P91,EACHANGADU JUNCTION,7.49,3 +19,598001,P91,EACHANGADU KAVERI HOSPITAL,7.53,4 +20,598001,P91,SUNNAMBU KOLATHUR VINAYAGAPURAM,7.58,5 +21,598001,P91,SUNNAMBU KOLATHUR,8.01,3 +22,598001,P91,PALLIKARANAI KAMACHI HOSPITAL,8.05,4 +23,598001,P91,CHENNAI ONE IT SEZ,8.07,2 +24,598001,P91,SEEVARAM,8.11,4 +25,598001,P91,PERUNGUDI,8.15,4 +26,598001,P91,KANDANCHAVADI,8.17,2 +27,598001,P91,PERUNGUDI I G P,8.19,2 +28,598001,P91,PERUNGUDI S.R.P TOOLS,8.21,2 +29,598001,P91,TIDEL PARK,8.25,4 +30,598001,P91,JAYANTHI THEATRE,8.28,3 +31,598001,P91,THIRUVANMIYUR B.T,8.30,2 +1,918001,P91,THIRUVANMIYUR B.T,7.00,0 +2,918001,P91,JAYANTHI THEATRE,7.02,2 +3,918001,P91,TIDEL PARK,7.05,3 +4,918001,P91,PERUNGUDI S.R.P TOOLS,7.07,2 +5,918001,P91,PERUNGUDI I G P,7.10,3 +6,918001,P91,KANDANCHAVADI,7.13,3 +7,918001,P91,PERUNGUDI,7.15,2 +8,918001,P91,SEEVARAM,7.17,2 +9,918001,P91,THORAIPAKKAM NAGATHAMMAN TEMPLE,7.19,2 +10,918001,P91,CHENNAI ONE IT SEZ,7.21,2 +11,918001,P91,KAMATCHI HOSPITAL,7.24,3 +12,918001,P91,SUNNAMBU KOLATHUR,7.27,3 +13,918001,P91,SUNNAMBU KOLATHUR VINAYAGAPURAM,7.31,4 +14,918001,P91,EACHANGADU KAVERI HOSPITAL,7.34,3 +15,918001,P91,EACHANGADU JUNCTION,7.37,3 +16,918001,P91,KOVILAMBAKKAM PERUMAL NAGAR,7.40,3 +17,918001,P91,PALLAVA GARDEN BUS STOP OR DJ MET SCHOOL,7.44,4 +18,918001,P91,VELS UNIVERSITY,7.48,4 +19,918001,P91,PALLAVARAM NEW BRIDGE,7.51,3 +20,918001,P91,SARAVANA STORE CHROMEPET,7.54,3 +21,918001,P91,CHROMEPET,7.57,3 +22,918001,P91,CHROMEPET MIT GATE,8.00,3 +23,918001,P91,TAMBARAM TB HOSPITAL,8.03,3 +24,918001,P91,TAMBARAM SANATORIUM BUS STAND,8.05,2 +25,918001,P91,TAMBARAM WEST BUS STAND,8.10,5 +26,918001,P91,IRUMBULIYUR,8.13,3 +27,918001,P91,PERUNGALATGUR LAKE VIEW STOP,8.16,3 +28,918001,P91,PERUNGALATHUR,8.18,2 +29,918001,P91,NEW PERUNGALATHUR SRIRAM GATE,8.20,2 +30,918001,P91,PERUNGALATHUR IRANIYAMMAN TEMPLE,8.22,2 +31,918001,P91,VANDALUR GATE,8.25,3 +32,918001,P91,VANDALUR ZOO,8.28,3 +33,918001,P91,KILAMBAKKAM BUS TERMINUS,8.30,2 \ No newline at end of file diff --git a/src/environment.rs b/src/environment.rs index e108de8..13719a3 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -128,6 +128,7 @@ pub struct AppState { pub vehicle_service_sub_types: Arc>>>, pub conductor_details: Arc>, pub depot_manager_details: Arc>, + pub premium_bus_schedules: Arc>>, } impl AppState { @@ -195,6 +196,9 @@ impl AppState { // Load depot manager details from CSV let depot_manager_details = Arc::new(Self::load_depot_manager_details().await?); + // Load premium bus schedules from CSV + let premium_bus_schedules = Arc::new(Self::load_premium_bus_schedules().await?); + let app_state = AppState { gtfs_service, db_vehicle_reader, @@ -207,11 +211,78 @@ impl AppState { vehicle_service_sub_types: Arc::new(Self::load_vehicle_service_sub_types().await?), conductor_details, depot_manager_details, + premium_bus_schedules, }; Ok(app_state) } + async fn load_premium_bus_schedules() -> Result>> { + let file_path = "./assets/premium_bus_schedules.csv"; + + let mut file = match File::open(file_path).await { + Ok(file) => file, + Err(_) => { + info!("premium_bus_schedules.csv file not found, proceeding without premium bus schedule data"); + return Ok(HashMap::new()); + } + }; + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .await + .map_err(|e| anyhow::anyhow!("Failed to read premium_bus_schedules.csv: {}", e))?; + + let mut reader = ReaderBuilder::new() + .has_headers(true) + .from_reader(contents.as_bytes()); + + // route_number_id -> Vec ordered by sequence_number + let mut schedules_map: HashMap> = HashMap::new(); + + for result in reader.records() { + match result { + Ok(record) => { + // CSV headers: sequence_number,route_number_id,route_short_name,bus_stop_name,timings,ETA_in_mins + let (Some(seq_num_str), Some(route_id), Some(eta_str)) = + (record.get(0), record.get(1), record.get(5)) + else { + continue; + }; + + let seq_num = seq_num_str.trim().parse::().unwrap_or(0); + let eta = eta_str.trim().parse::().unwrap_or(0); + + schedules_map + .entry(route_id.trim().to_string()) + .or_default() + .insert(seq_num, eta); + } + Err(e) => { + error!("Error parsing premium_bus_schedules CSV row: {}", e); + } + } + } + + let schedules_map_len = schedules_map.len(); + + // Convert the BTreeMap (which inherently sorts by sequence_number) into a flat Vec + let final_map = schedules_map + .into_iter() + .map(|(route_id, eta_map)| { + let etas: Vec = eta_map.into_values().collect(); + (route_id, etas) + }) + .collect(); + + info!( + "Loaded premium bus schedules for {} routes", + schedules_map_len + ); + + Ok(final_map) + } + async fn load_fleet_list() -> Result>>> { let file_path = "./assets/fleet_list.csv"; diff --git a/src/handlers/routes.rs b/src/handlers/routes.rs index d708c4d..6dd4485 100644 --- a/src/handlers/routes.rs +++ b/src/handlers/routes.rs @@ -1097,6 +1097,61 @@ async fn get_trip_data( Ok(HttpResponse::Ok().json(trip_data)) } +fn calculate_eta_from_csv( + route_stop_mappings: &[std::sync::Arc], + trip_start_time: Option, + csv_etas: &[i32], +) -> Vec { + let mut bus_stop_etas: Vec = Vec::new(); + let now = chrono::Utc::now(); + + // Calculate cumulative travel time to each stop + let mut cumulative_time_seconds: f64 = 0.0; + + for (idx, mapping) in route_stop_mappings.iter().enumerate() { + if idx > 0 { + // Get ETA for this sequence from CSV + // If the CSV array is shorter than mapping, we fallback to 2 mins as safe default + let eta_mins = if idx < csv_etas.len() { + csv_etas[idx] as f64 + } else { + 2.0 + }; + cumulative_time_seconds += eta_mins * 60.0; + } + + // Calculate arrival time + let arrival_time = if let Some(start_epoch_millis) = trip_start_time { + // Calculate arrival time from trip start time + // Subtract 10 minutes for premium buses to show arrival 10 mins prior to schedule + let start_utc = + chrono::DateTime::::from_timestamp_millis(start_epoch_millis).unwrap(); + start_utc + chrono::Duration::seconds(cumulative_time_seconds as i64) - chrono::Duration::minutes(10) + } else { + // If no start time, use current time + cumulative time + now + chrono::Duration::seconds(cumulative_time_seconds as i64) + }; + + let arrival_epoch = arrival_time.timestamp(); + + // Calculate ETA in seconds (time until arrival) + let eta_seconds = if arrival_time > now { + Some((arrival_time - now).num_seconds()) + } else { + None + }; + + bus_stop_etas.push(crate::models::BusStopETA { + stop_code: mapping.stop_code.clone(), + arrival_time: arrival_epoch, + eta_seconds, + stop_name: Some(mapping.stop_name.clone()), + }); + } + + bus_stop_etas +} + /// Calculate arrival time and ETA based on haversine distance between stops /// Assumes constant speed of 25 km/hr @@ -1296,14 +1351,29 @@ async fn get_bus_route_schedule( trip.start_time.as_ref().and_then(|s| s.parse::().ok()) }); - // Calculate ETAs using haversine distance function - bus_stop_etas = - calculate_eta_from_haversine_distance(&route_stop_mappings, trip_start_time); + // Calculate ETAs + if waybill.service_type.eq_ignore_ascii_case("PREMIUM") { + if let Some(csv_etas) = app_state.premium_bus_schedules.get(&route_id) { + bus_stop_etas = calculate_eta_from_csv(&route_stop_mappings, trip_start_time, csv_etas); + } else { + bus_stop_etas = calculate_eta_from_haversine_distance(&route_stop_mappings, trip_start_time); + } + } else { + bus_stop_etas = calculate_eta_from_haversine_distance(&route_stop_mappings, trip_start_time); + } } - // If no trip details, calculate ETAs using haversine distance without start time + // If no trip details, calculate ETAs without start time if bus_stop_etas.is_empty() { - bus_stop_etas = calculate_eta_from_haversine_distance(&route_stop_mappings, None); + if waybill.service_type.eq_ignore_ascii_case("PREMIUM") { + if let Some(csv_etas) = app_state.premium_bus_schedules.get(&route_id) { + bus_stop_etas = calculate_eta_from_csv(&route_stop_mappings, None, csv_etas); + } else { + bus_stop_etas = calculate_eta_from_haversine_distance(&route_stop_mappings, None); + } + } else { + bus_stop_etas = calculate_eta_from_haversine_distance(&route_stop_mappings, None); + } } schedule_details.push(crate::models::BusScheduleDetail {