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
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ CLAUDE.md
.claude/

# Unused files
ignored/
ignored/

# Preprocessed per-trip stop-times shards.
# These are generated by the Nandi preprocessor and copied into assets/ at
# build time so they get baked into the GIMS Docker image. They are NOT
# source — individual shards exceed GitHub's 100 MB push limit, so they must
# never enter git. The Sync GIMS Repo step relies on this rule to keep the
# branch under the limit while the shards still reach every built image.
assets/trip_stoptimes/
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name = "gtfs-routes-service"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "gtfs-routes-service"
path = "src/main.rs"

[dependencies]
# Web framework
actix-web = "4.3.1"
Expand Down
14 changes: 14 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ pipeline {
sh "docker tag ${env.IMAGE_NAME}:latest ${env.ACCOUNT_ID2}.dkr.ecr.ap-south-1.amazonaws.com/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"
sh "docker push ${env.ACCOUNT_ID2}.dkr.ecr.ap-south-1.amazonaws.com/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"

// Push to GCP Artifact Registry — master (ny-sandbox)
withCredentials([file(credentialsId: 'gcp-sa-key', variable: 'GCP_KEY_FILE')]) {
sh 'cat $GCP_KEY_FILE | docker login -u _json_key --password-stdin https://asia-south1-docker.pkg.dev'
sh "docker tag ${env.IMAGE_NAME}:latest asia-south1-docker.pkg.dev/ny-sandbox/${env.IMAGE_NAME}/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"
sh "docker push asia-south1-docker.pkg.dev/ny-sandbox/${env.IMAGE_NAME}/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"
}

// Push to GCP Artifact Registry — prod (ny-prod)
withCredentials([file(credentialsId: 'gcp-sa-key-prod', variable: 'GCP_KEY_FILE_PROD')]) {
sh 'cat $GCP_KEY_FILE_PROD | docker login -u _json_key --password-stdin https://asia-south1-docker.pkg.dev'
sh "docker tag ${env.IMAGE_NAME}:latest asia-south1-docker.pkg.dev/ny-prod/${env.IMAGE_NAME}/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"
sh "docker push asia-south1-docker.pkg.dev/ny-prod/${env.IMAGE_NAME}/${env.IMAGE_NAME}:${env.LAST_COMMIT_HASH}"
}

}
}
}
Expand Down
170 changes: 170 additions & 0 deletions assets/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{
"version": "60c162a080f3",
"build_commit": "5ad145",
"generated_at": "2026-06-05T19:44:13Z",
"gtfs_feeds": [
"bangalore_metro",
"bhubaneshwar_bus",
"chennai_bus",
"chennai_metro",
"chennai_suburban",
"delhi_bus_nammayatri_application_mock",
"delhi_metro",
"kochi_metro",
"kolkata_bus",
"kolkata_metro",
"mumbai_metro",
"mumbai_suburban",
"sambalpur_bus"
],
"files": {
"routes.json": {
"sha256": "4985f6d10c1ef7fc6c3181b07dd4a92ee8f0d1a17e0f9c7fe58f9d9b1fc5dc3e",
"count": 5643
},
"stops.json": {
"sha256": "074bcec44ac8d4ddc7eae05e86470b6faaaa4141f3a3df2c59bdbd25b3ffe53c",
"count": 24998
},
"patterns.json": {
"sha256": "efd57d7bff3fecbb1dc4e29dc11b5515665c05989fbc03b58b5e7b85c33fa7e7",
"count": 5755
},
"route_stops.json": {
"sha256": "7a1a7077970d8aae04ea9f910fe3b4e0cfcd495bdceffc41de26967839fb3893",
"count": 109280
},
"trip_stoptimes/bangalore_metro.json": {
"sha256": "e639d9a639c089d51cfd85efadcda52c2b5a277e8c6a77b159d5b4c4ea8a17f4",
"count": 2722
},
"trip_stoptimes/bhubaneshwar_bus.json": {
"sha256": "70816626318d547ab9cd801f62369af29c8c878d6a1dc3409dd4d6bb194ff47e",
"count": 4199
},
"trip_stoptimes/chennai_bus.json": {
"sha256": "5ab26b1ea65ef894f0d7f22f6db60d4be28215703178bca1677d4f47fe63f029",
"count": 51512
},
"trip_stoptimes/chennai_metro.json": {
"sha256": "19433454474303f58dc3428ccd33aa8c32239d9b2956a4299a539dc28e34a9a1",
"count": 3738
},
"trip_stoptimes/chennai_suburban.json": {
"sha256": "981ea7728f23fdf991e83329fd6988090f63a56f3955ef87ce1f11bd5f20ef9a",
"count": 807
},
"trip_stoptimes/delhi_bus_nammayatri_application_mock.json": {
"sha256": "da9b10ea10dd6dcd3c9ca2bf9cc33d26c874c03ba0f78dda9cbb89bf4b320b60",
"count": 24
},
"trip_stoptimes/delhi_metro.json": {
"sha256": "ae75e590e6a4149e9f1355ddbc76a105a0104aa9ad9a1cd1842ac7a3a1587c8c",
"count": 21825
},
"trip_stoptimes/kochi_metro.json": {
"sha256": "13c6cb2d264d3f10e50351801f7d3cebd0b3fd68e7460d5797b2e1223fcbb928",
"count": 450
},
"trip_stoptimes/kolkata_bus.json": {
"sha256": "b2d61d80185de6db39c61f3a475ff61065fe3794a2eeb73f9d0002983655816f",
"count": 17227
},
"trip_stoptimes/kolkata_metro.json": {
"sha256": "d84a6561c5b6415fb9d63ab1738ba502ff0fed7fcaf75572f38c6d2fa3ec0d19",
"count": 1652
},
"trip_stoptimes/mumbai_metro.json": {
"sha256": "4b04febcf317c01a7a3344232538e1f76677a8055947f2f4acb5fff549f9e145",
"count": 278
},
"trip_stoptimes/mumbai_suburban.json": {
"sha256": "8a374cba7e8b57f9cb8e9c43300984dd7014e2a51e3cc210c7a41fdfa928d2d7",
"count": 134
},
"trip_stoptimes/sambalpur_bus.json": {
"sha256": "11196eed2c41b0aa60e8f4268aa5eeb7aa04de108bcb046ade0800fa8f22f496",
"count": 405
}
},
"stats": {
"bangalore_metro": {
"routes": 6,
"stops": 472,
"patterns": 58,
"route_stop_mappings": 170
},
"bhubaneshwar_bus": {
"routes": 493,
"stops": 2916,
"patterns": 493,
"route_stop_mappings": 16875
},
"chennai_bus": {
"routes": 4138,
"stops": 17909,
"patterns": 4138,
"route_stop_mappings": 71269
},
"chennai_metro": {
"routes": 44,
"stops": 313,
"patterns": 50,
"route_stop_mappings": 593
},
"chennai_suburban": {
"routes": 804,
"stops": 722,
"patterns": 807,
"route_stop_mappings": 16814
},
"delhi_bus_nammayatri_application_mock": {
"routes": 1,
"stops": 11,
"patterns": 1,
"route_stop_mappings": 11
},
"delhi_metro": {
"routes": 22,
"stops": 1126,
"patterns": 57,
"route_stop_mappings": 561
},
"kochi_metro": {
"routes": 2,
"stops": 25,
"patterns": 8,
"route_stop_mappings": 50
},
"kolkata_bus": {
"routes": 88,
"stops": 693,
"patterns": 88,
"route_stop_mappings": 1773
},
"kolkata_metro": {
"routes": 10,
"stops": 182,
"patterns": 20,
"route_stop_mappings": 114
},
"mumbai_metro": {
"routes": 2,
"stops": 12,
"patterns": 2,
"route_stop_mappings": 24
},
"mumbai_suburban": {
"routes": 2,
"stops": 37,
"patterns": 2,
"route_stop_mappings": 74
},
"sambalpur_bus": {
"routes": 31,
"stops": 580,
"patterns": 31,
"route_stop_mappings": 952
}
}
}
1 change: 1 addition & 0 deletions assets/patterns.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/route_stops.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/routes.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/stops.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions dhall-configs/dev/gtfs_in_memory_server_rust.dhall
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
let LogLevel = < TRACE | DEBUG | INFO | WARN | ERROR | OFF >
clet LogLevel = < TRACE | DEBUG | INFO | WARN | ERROR | OFF >

let logger_cfg = {
level = LogLevel.INFO,
Expand Down Expand Up @@ -65,5 +65,9 @@ in {
osrtc_username = secrets.osrtc_username,
osrtc_secret_key = secrets.osrtc_secret_key,
osrtc_station_refresh_interval_hours = 1,
osrtc_feed_key = Some "odisha_osrtc"
osrtc_feed_key = Some "odisha_osrtc",

-- Preprocessed data configuration
use_preprocessed_data = False,
preprocessed_data_dir = "./assets"
}
73 changes: 73 additions & 0 deletions src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::services::{
},
fleet_operator::{DBFleetOperatorService, FleetOperatorService, MockFleetOperatorService},
gtfs_service::GTFSService,
metro_graph::MetroGraph,
operator::{DBOperatorService, MockOperatorService, OperatorService},
osrtc_station_cache::OsrtcStationCache,
trip_service::TripService,
Expand Down Expand Up @@ -70,6 +71,15 @@ pub struct AppConfig {
pub ignored_trip_ids: Vec<String>,
pub bhubaneswar_cache_update_interval: u64,
pub bhubaneswar_external_auth: Option<String>,
/// When true, load GTFS data from preprocessed JSON files (produced by
/// Nandi preprocessor) instead of calling Nandi/OTP APIs.
#[serde(default)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

pub use_preprocessed_data: bool,
/// Directory containing preprocessed JSON files (routes.json, stops.json,
/// patterns.json, route_stops.json, metadata.json).
/// Defaults to "./assets" if not set.
#[serde(default = "default_preprocessed_data_dir")]
pub preprocessed_data_dir: String,
pub phone_number_hash_key: String,
/// Enable schedule-based active trip reconciliation (default: false)
pub enable_schedule_reconciliation: bool,
Expand All @@ -80,6 +90,10 @@ pub struct AppConfig {
pub osrtc_feed_key: Option<String>,
}

fn default_preprocessed_data_dir() -> String {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

"./assets".to_string()
}

impl OtpConfig {
pub fn find_instance_by_gtfs_id(&self, gtfs_id: &str) -> Option<&OtpInstance> {
self.gtfs_id_based_instances
Expand Down Expand Up @@ -148,6 +162,8 @@ pub struct AppState {
pub bus_registration_mapping: Arc<HashMap<String, HashMap<String, String>>>,
pub fleet_list: Arc<HashMap<String, HashMap<String, Vec<String>>>>,
pub vehicle_service_sub_types: Arc<HashMap<String, HashMap<String, Vec<String>>>>,
/// Metro transit graphs loaded from preprocessed JSON (gtfs_id -> MetroGraph)
pub metro_graphs: Arc<HashMap<String, MetroGraph>>,
pub depot_manager_details: Arc<HashMap<String, crate::models::DepotManagerDetails>>,
pub fleet_tag_list: Arc<HashMap<String, HashMap<String, String>>>,
pub chennai_service_type_cache: Arc<RwLock<HashMap<String, (Instant, Option<String>)>>>,
Expand Down Expand Up @@ -354,6 +370,7 @@ impl AppState {

// Load bus registration mapping from CSV
let bus_registration_mapping = Arc::new(Self::load_bus_registration_mapping().await?);
let metro_graphs = Arc::new(Self::load_metro_graphs(&app_config));

// Load depot manager details from CSV
let depot_manager_details = Arc::new(Self::load_depot_manager_details().await?);
Expand All @@ -372,6 +389,7 @@ impl AppState {
bus_registration_mapping,
fleet_list: Arc::new(Self::load_fleet_list().await?),
vehicle_service_sub_types: Arc::new(Self::load_vehicle_service_sub_types().await?),
metro_graphs,
depot_manager_details,
fleet_tag_list: Arc::new(Self::load_fleet_tag_list().await?),
chennai_service_type_cache: Arc::new(RwLock::new(HashMap::new())),
Expand Down Expand Up @@ -604,6 +622,61 @@ impl AppState {
Ok(sub_types_map)
}

/// Load metro transit graphs from preprocessed JSON files in the
/// preprocessed data directory.
///
/// Looks for `metro_graph.json` which contains a map of gtfs_id -> MetroGraph.
/// If the file doesn't exist, returns an empty map (metro routing won't
/// be available but all other APIs work fine).
fn load_metro_graphs(config: &AppConfig) -> HashMap<String, MetroGraph> {
let graph_file = std::path::Path::new(&config.preprocessed_data_dir)
.join("metro_graph.json");

if !graph_file.exists() {
info!(
"No metro_graph.json found in {}, metro routing will not be available",
config.preprocessed_data_dir
);
return HashMap::new();
}

info!(
"Loading metro graphs from {}",
graph_file.display()
);

match std::fs::read_to_string(&graph_file) {
Ok(json) => match serde_json::from_str::<HashMap<String, MetroGraph>>(&json) {
Ok(graphs) => {
for (gtfs_id, graph) in &graphs {
info!(
"Loaded metro graph for {}: {} nodes, {} routes",
gtfs_id,
graph.nodes.len(),
graph.route_stop_sequences.len()
);
}
info!("Loaded {} metro transit graphs", graphs.len());
graphs
}
Err(e) => {
error!(
"Failed to deserialize metro_graph.json: {}. Metro routing will not be available.",
e
);
HashMap::new()
}
},
Err(e) => {
error!(
"Failed to read metro_graph.json: {}. Metro routing will not be available.",
e
);
HashMap::new()
}
}
}

async fn load_depot_manager_details(
) -> Result<HashMap<String, crate::models::DepotManagerDetails>> {
use crate::models::DepotManagerDetails;
Expand Down
10 changes: 9 additions & 1 deletion src/graphql/responses/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::models::serialize_option_as_empty_string;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TripApiResponse {
#[serde(rename = "tripId")]
pub trip_id: String,
#[serde(rename = "routeId")]
pub route_id: String,
#[serde(rename = "routeName")]
// Backend's `TripInfoResponse.routeName :: Text` is non-Maybe — emit "" not
// null when the route lookup misses (e.g. the trip's route is missing from
// the routes feed) so the Aeson decoder doesn't fail.
#[serde(
rename = "routeName",
serialize_with = "serialize_option_as_empty_string"
)]
pub route_name: Option<String>,
#[serde(rename = "direction")]
pub direction: Option<i32>,
Expand Down
Loading