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 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(×tamp).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 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 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 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 }