nictd_gtfs_rt/
lib.rs

1use chrono::Datelike;
2use chrono::{DateTime, NaiveDateTime, TimeZone};
3use chrono_tz::America::Chicago;
4use gtfs_realtime::alert::{Cause, Effect, SeverityLevel};
5use gtfs_realtime::translated_string::Translation;
6use core::time;
7use gtfs_realtime::trip_update::stop_time_update::StopTimeProperties;
8use gtfs_realtime::trip_update::{StopTimeEvent, StopTimeUpdate};
9use gtfs_realtime::{Alert, EntitySelector, FeedEntity, FeedMessage, TimeRange, TranslatedString, stop};
10use inline_colorization::*;
11use serde::Deserialize;
12use std::collections::HashMap;
13use std::collections::HashSet;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16pub fn capitalize(s: &str) -> String {
17    let mut c = s.chars();
18    match c.next() {
19        None => String::new(),
20        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
21    }
22}
23
24#[derive(Debug, Clone)]
25pub struct NICTDResults {
26    pub alerts: FeedMessage,
27}
28
29#[derive(Deserialize, Debug, Clone, Eq, PartialEq)]
30struct NICTDAlert {
31    created: String,
32    modified: String,
33    pin_index: i32,
34    active: bool,
35    auto_text: bool,
36    // TODO - I've never seen `train`, `delay`, `reason`, or `alert_heading` nonnull
37    train: Option<String>,
38    delay: Option<String>,
39    reason: Option<String>,
40    alert_heading: Option<String>,
41    alert_body: Option<String>,
42}
43
44fn timestamp_from_str_u64(timestamp: &str) -> Option<u64> {
45    let time = chrono::DateTime::parse_from_rfc3339(&timestamp).ok()?;
46
47    Some(time.timestamp() as u64)
48}
49
50fn english_only_translations(text: String) -> TranslatedString {
51    TranslatedString {
52            translation: vec![Translation {
53            text: text,
54            language: Some("en_US".to_string()),
55        }]
56    }
57}
58
59pub async fn train_feed(
60    client: &reqwest::Client,
61) -> Result<NICTDResults, Box<dyn std::error::Error + Sync + Send>> {
62    
63    let mut alerts: Vec<FeedEntity> = vec![];
64
65    // Query NICTD website for alerts
66    let response = client
67        .get("https://mysouthshoreline.com/service_updates.json")
68        .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0 CatenaryMaps/1.0")
69        .send()
70        .await;
71
72    if let Err(response) = &response {
73        println!(
74            "{color_magenta}{:#?}{color_reset}",
75            response.url().unwrap().as_str()
76        );
77    }
78
79    let response = response?;
80    let text = response.text().await?;
81
82    // println!("{}", text);
83
84    let alerts_data = serde_json::from_str::<Vec<NICTDAlert>>(text.as_str())?;
85
86    for alert in alerts_data {
87        let active_period: Vec<TimeRange> = vec![
88            TimeRange {
89                start: timestamp_from_str_u64(&alert.modified),
90                end: None,
91            }
92        ];
93
94        let informed_entity: Vec<EntitySelector> = vec![
95            EntitySelector {
96                agency_id: Some("NICTD".to_string()),
97                route_id: Some("so_shore".to_string()),
98                ..EntitySelector::default()
99            }
100        ];
101
102        let effect = Effect::UnknownEffect;
103        let cause = Cause::UnknownCause;
104
105        let wrapped_cause_detail = match alert.reason {
106            Some(text) => Some(english_only_translations(text)),
107            None => None,
108        };
109
110        let wrapped_header_text = match alert.alert_heading {
111            Some(text) => Some(english_only_translations(text)),
112            None => None,
113        };
114
115        let wrapped_description_text = match alert.alert_body {
116            Some(desc) => Some(english_only_translations(desc)),
117            None => None,
118        };
119
120        let severity_level = SeverityLevel::UnknownSeverity;
121
122        
123
124        alerts.push(FeedEntity {
125            id: alert.created + &alert.modified, 
126            is_deleted: None,
127            trip_update: None,
128            vehicle: None,
129            alert: Some(Alert {
130                active_period: active_period,
131                informed_entity: informed_entity,
132                cause: Some(cause.into()),
133                effect: Some(effect.into()),
134                url: None,
135                header_text: wrapped_header_text.clone(),
136                description_text: wrapped_description_text.clone(),
137                tts_header_text: wrapped_header_text.clone(),
138                tts_description_text: wrapped_description_text.clone(),
139                severity_level: Some(severity_level.into()),
140                image: None,
141                image_alternative_text: None,
142                cause_detail: wrapped_cause_detail,
143                effect_detail: None,
144            }),
145            shape: None,
146            stop: None,
147            trip_modifications: None
148        });
149    }
150
151    Ok(NICTDResults {
152        alerts: gtfs_realtime::FeedMessage {
153            entity: alerts,
154            header: gtfs_realtime::FeedHeader {
155                timestamp: Some(
156                    SystemTime::now()
157                        .duration_since(UNIX_EPOCH)
158                        .expect("Time went backwards")
159                        .as_secs(),
160                ),
161                gtfs_realtime_version: String::from("2.0"),
162                incrementality: None,
163                feed_version: None,
164            },
165        },
166    })
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use reqwest::Client;
173    use std::{fs, io, path::Path};
174    use zip::ZipArchive;
175
176    #[tokio::test]
177    async fn test_train_feed() {
178        // let trips_file_data = fs::read_to_string("static/trips.txt");
179
180        // println!("Reading gtfs data");
181        // let gtfs_data = gtfs_structures::Gtfs::new("static/").unwrap();
182        // println!("Finished reading gtfs data");
183
184        let train_feeds = train_feed(
185            &reqwest::ClientBuilder::new()
186                .use_rustls_tls()
187                .deflate(true)
188                .gzip(true)
189                .brotli(true)
190                .build()
191                .unwrap(),
192        )
193        .await;
194
195        println!("{:#?}", train_feeds);
196
197        assert!(train_feeds.is_ok());
198    }
199
200    /*
201    #[tokio::test]
202    async fn test_bus_feed() {
203        let api_key = "Det2nqw85D8TqxqF6SpcYYjfu";
204
205        let bus = reqwest::get(
206            "https://www.ctabustracker.com/bustime/api/v2/getvehicles?key=Det2nqw85D8TqxqF6SpcYYjfu&rt=1"
207        ).await.unwrap().text().await.unwrap();
208
209        println!("{}", bus);
210    }*/
211}