Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions assets/premium_bus_schedules.csv
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ pub struct AppState {
pub vehicle_service_sub_types: Arc<HashMap<String, HashMap<String, Vec<String>>>>,
pub conductor_details: Arc<HashMap<String, crate::models::MinimalEmployee>>,
pub depot_manager_details: Arc<HashMap<String, crate::models::DepotManagerDetails>>,
pub premium_bus_schedules: Arc<HashMap<String, Vec<i32>>>,
}

impl AppState {
Expand Down Expand Up @@ -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,
Expand All @@ -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<HashMap<String, Vec<i32>>> {
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<eta_in_mins> ordered by sequence_number
let mut schedules_map: HashMap<String, std::collections::BTreeMap<i32, i32>> = 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::<i32>().unwrap_or(0);
let eta = eta_str.trim().parse::<i32>().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<i32> = 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<HashMap<String, HashMap<String, Vec<String>>>> {
let file_path = "./assets/fleet_list.csv";

Expand Down
80 changes: 75 additions & 5 deletions src/handlers/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteStopMapping>],
trip_start_time: Option<i64>,
csv_etas: &[i32],
) -> Vec<crate::models::BusStopETA> {
let mut bus_stop_etas: Vec<crate::models::BusStopETA> = 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::<chrono::Utc>::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
Expand Down Expand Up @@ -1296,14 +1351,29 @@ async fn get_bus_route_schedule(
trip.start_time.as_ref().and_then(|s| s.parse::<i64>().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 {
Expand Down