1use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::Duration;
14
15use chrono::Utc;
16use reqwest::{Client, StatusCode};
17use serde::Deserialize;
18use tokio::time::sleep;
19
20use crate::api_clients::SimpleEmbedder;
21use crate::ruvector_native::{Domain, SemanticVector};
22use crate::{FrameworkError, Result};
23
24const GTFS_RATE_LIMIT_MS: u64 = 1000; const MOBILITY_DB_RATE_LIMIT_MS: u64 = 600; const OPENROUTE_RATE_LIMIT_MS: u64 = 1000; const OPENCHARGEMAP_RATE_LIMIT_MS: u64 = 100; const MAX_RETRIES: u32 = 3;
30const RETRY_DELAY_MS: u64 = 1000;
31const DEFAULT_EMBEDDING_DIM: usize = 256;
32
33#[derive(Debug, Deserialize)]
39struct TransitlandStopsResponse {
40 #[serde(default)]
41 stops: Vec<TransitlandStop>,
42 #[serde(default)]
43 meta: Option<TransitlandMeta>,
44}
45
46#[derive(Debug, Default, Deserialize)]
47struct TransitlandStop {
48 #[serde(default)]
49 onestop_id: String,
50 #[serde(default)]
51 stop_name: String,
52 #[serde(default)]
53 stop_desc: String,
54 #[serde(default)]
55 geometry: Option<TransitlandGeometry>,
56 #[serde(default)]
57 stop_timezone: String,
58 #[serde(default)]
59 wheelchair_boarding: i32,
60}
61
62#[derive(Debug, Deserialize)]
63struct TransitlandGeometry {
64 #[serde(default)]
65 coordinates: Vec<f64>,
66}
67
68#[derive(Debug, Deserialize)]
69struct TransitlandMeta {
70 #[serde(default)]
71 next: Option<String>,
72 #[serde(default)]
73 total: Option<u64>,
74}
75
76#[derive(Debug, Deserialize)]
78struct TransitlandRoutesResponse {
79 #[serde(default)]
80 routes: Vec<TransitlandRoute>,
81}
82
83#[derive(Debug, Deserialize)]
84struct TransitlandRoute {
85 #[serde(default)]
86 onestop_id: String,
87 #[serde(default)]
88 route_long_name: String,
89 #[serde(default)]
90 route_short_name: String,
91 #[serde(default)]
92 route_type: i32,
93 #[serde(default)]
94 route_color: String,
95 #[serde(default)]
96 route_desc: String,
97}
98
99#[derive(Debug, Deserialize)]
101struct TransitlandDeparturesResponse {
102 #[serde(default)]
103 stops: Vec<TransitlandStopDepartures>,
104}
105
106#[derive(Debug, Deserialize)]
107struct TransitlandStopDepartures {
108 #[serde(default)]
109 stop: TransitlandStop,
110 #[serde(default)]
111 schedule_stop_pairs: Vec<TransitlandScheduleStopPair>,
112}
113
114#[derive(Debug, Deserialize)]
115struct TransitlandScheduleStopPair {
116 #[serde(default)]
117 origin_departure_time: String,
118 #[serde(default)]
119 destination_arrival_time: String,
120 #[serde(default)]
121 trip_headsign: String,
122}
123
124#[derive(Debug, Deserialize)]
126struct TransitlandAgenciesResponse {
127 #[serde(default)]
128 operators: Vec<TransitlandOperator>,
129}
130
131#[derive(Debug, Deserialize)]
132struct TransitlandOperator {
133 #[serde(default)]
134 onestop_id: String,
135 #[serde(default)]
136 name: String,
137 #[serde(default)]
138 website: String,
139 #[serde(default)]
140 short_name: String,
141 #[serde(default)]
142 tags: HashMap<String, String>,
143}
144
145pub struct GtfsClient {
163 client: Client,
164 base_url: String,
165 api_key: Option<String>,
166 rate_limit_delay: Duration,
167 embedder: Arc<SimpleEmbedder>,
168}
169
170impl GtfsClient {
171 pub fn new() -> Self {
177 Self::with_api_key(None)
178 }
179
180 pub fn with_api_key(api_key: Option<String>) -> Self {
182 Self {
183 client: Client::builder()
184 .user_agent("RuVector-Discovery/1.0")
185 .timeout(Duration::from_secs(30))
186 .build()
187 .expect("Failed to create HTTP client"),
188 base_url: "https://transit.land/api/v2".to_string(),
189 api_key,
190 rate_limit_delay: Duration::from_millis(GTFS_RATE_LIMIT_MS),
191 embedder: Arc::new(SimpleEmbedder::new(DEFAULT_EMBEDDING_DIM)),
192 }
193 }
194
195 pub async fn search_stops(&self, query: &str) -> Result<Vec<SemanticVector>> {
205 let url = format!(
206 "{}/rest/stops?search={}&limit=50",
207 self.base_url,
208 urlencoding::encode(query)
209 );
210
211 sleep(self.rate_limit_delay).await;
212 let response = self.fetch_with_retry(&url).await?;
213
214 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
216 return Ok(self.mock_stops(query));
217 }
218
219 let stops_response: TransitlandStopsResponse = response.json().await?;
220
221 let mut vectors = Vec::new();
222 for stop in stops_response.stops {
223 let (lat, lng) = stop
224 .geometry
225 .as_ref()
226 .and_then(|g| {
227 if g.coordinates.len() >= 2 {
228 Some((g.coordinates[1], g.coordinates[0]))
229 } else {
230 None
231 }
232 })
233 .unwrap_or((0.0, 0.0));
234
235 let text = format!(
237 "{} {} {} at ({}, {})",
238 stop.stop_name, stop.stop_desc, stop.stop_timezone, lat, lng
239 );
240 let embedding = self.embedder.embed_text(&text);
241
242 let mut metadata = HashMap::new();
243 metadata.insert("onestop_id".to_string(), stop.onestop_id.clone());
244 metadata.insert("stop_name".to_string(), stop.stop_name.clone());
245 metadata.insert("stop_desc".to_string(), stop.stop_desc);
246 metadata.insert("latitude".to_string(), lat.to_string());
247 metadata.insert("longitude".to_string(), lng.to_string());
248 metadata.insert("timezone".to_string(), stop.stop_timezone);
249 metadata.insert(
250 "wheelchair_accessible".to_string(),
251 (stop.wheelchair_boarding == 1).to_string(),
252 );
253 metadata.insert("source".to_string(), "gtfs_transitland".to_string());
254
255 vectors.push(SemanticVector {
256 id: format!("GTFS:STOP:{}", stop.onestop_id),
257 embedding,
258 domain: Domain::Transportation,
259 timestamp: Utc::now(),
260 metadata,
261 });
262 }
263
264 Ok(vectors)
265 }
266
267 pub async fn get_routes(&self, operator_id: &str) -> Result<Vec<SemanticVector>> {
277 let url = format!(
278 "{}/rest/routes?operator_onestop_id={}&limit=100",
279 self.base_url, operator_id
280 );
281
282 sleep(self.rate_limit_delay).await;
283 let response = self.fetch_with_retry(&url).await?;
284
285 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
286 return Ok(self.mock_routes(operator_id));
287 }
288
289 let routes_response: TransitlandRoutesResponse = response.json().await?;
290
291 let mut vectors = Vec::new();
292 for route in routes_response.routes {
293 let route_type_name = Self::route_type_to_name(route.route_type);
294
295 let text = format!(
297 "{} {} {} ({})",
298 route.route_short_name, route.route_long_name, route.route_desc, route_type_name
299 );
300 let embedding = self.embedder.embed_text(&text);
301
302 let mut metadata = HashMap::new();
303 metadata.insert("onestop_id".to_string(), route.onestop_id.clone());
304 metadata.insert("route_short_name".to_string(), route.route_short_name);
305 metadata.insert("route_long_name".to_string(), route.route_long_name);
306 metadata.insert("route_type".to_string(), route_type_name);
307 metadata.insert("route_color".to_string(), route.route_color);
308 metadata.insert("route_desc".to_string(), route.route_desc);
309 metadata.insert("operator_id".to_string(), operator_id.to_string());
310 metadata.insert("source".to_string(), "gtfs_transitland".to_string());
311
312 vectors.push(SemanticVector {
313 id: format!("GTFS:ROUTE:{}", route.onestop_id),
314 embedding,
315 domain: Domain::Transportation,
316 timestamp: Utc::now(),
317 metadata,
318 });
319 }
320
321 Ok(vectors)
322 }
323
324 pub async fn get_departures(&self, stop_id: &str) -> Result<Vec<SemanticVector>> {
334 let url = format!(
335 "{}/rest/stops/{}?include_departures=true",
336 self.base_url, stop_id
337 );
338
339 sleep(self.rate_limit_delay).await;
340 let response = self.fetch_with_retry(&url).await?;
341
342 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
343 return Ok(self.mock_departures(stop_id));
344 }
345
346 let departures_response: TransitlandDeparturesResponse = response.json().await?;
347
348 let mut vectors = Vec::new();
349 for stop_data in departures_response.stops {
350 for (idx, pair) in stop_data.schedule_stop_pairs.iter().enumerate() {
351 let text = format!(
353 "{} departing at {} to {}",
354 pair.trip_headsign, pair.origin_departure_time, pair.destination_arrival_time
355 );
356 let embedding = self.embedder.embed_text(&text);
357
358 let mut metadata = HashMap::new();
359 metadata.insert("stop_id".to_string(), stop_id.to_string());
360 metadata.insert("stop_name".to_string(), stop_data.stop.stop_name.clone());
361 metadata.insert("departure_time".to_string(), pair.origin_departure_time.clone());
362 metadata.insert("arrival_time".to_string(), pair.destination_arrival_time.clone());
363 metadata.insert("headsign".to_string(), pair.trip_headsign.clone());
364 metadata.insert("source".to_string(), "gtfs_transitland".to_string());
365
366 vectors.push(SemanticVector {
367 id: format!("GTFS:DEPARTURE:{}:{}", stop_id, idx),
368 embedding,
369 domain: Domain::Transportation,
370 timestamp: Utc::now(),
371 metadata,
372 });
373 }
374 }
375
376 Ok(vectors)
377 }
378
379 pub async fn search_agencies(&self, query: &str) -> Result<Vec<SemanticVector>> {
389 let url = format!(
390 "{}/rest/operators?search={}&limit=50",
391 self.base_url,
392 urlencoding::encode(query)
393 );
394
395 sleep(self.rate_limit_delay).await;
396 let response = self.fetch_with_retry(&url).await?;
397
398 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
399 return Ok(self.mock_agencies(query));
400 }
401
402 let agencies_response: TransitlandAgenciesResponse = response.json().await?;
403
404 let mut vectors = Vec::new();
405 for operator in agencies_response.operators {
406 let text = format!("{} {} {}", operator.name, operator.short_name, operator.website);
408 let embedding = self.embedder.embed_text(&text);
409
410 let mut metadata = HashMap::new();
411 metadata.insert("onestop_id".to_string(), operator.onestop_id.clone());
412 metadata.insert("name".to_string(), operator.name);
413 metadata.insert("short_name".to_string(), operator.short_name);
414 metadata.insert("website".to_string(), operator.website);
415 metadata.insert("source".to_string(), "gtfs_transitland".to_string());
416
417 vectors.push(SemanticVector {
418 id: format!("GTFS:AGENCY:{}", operator.onestop_id),
419 embedding,
420 domain: Domain::Transportation,
421 timestamp: Utc::now(),
422 metadata,
423 });
424 }
425
426 Ok(vectors)
427 }
428
429 fn route_type_to_name(route_type: i32) -> String {
431 match route_type {
432 0 => "Tram/Light Rail".to_string(),
433 1 => "Subway/Metro".to_string(),
434 2 => "Rail".to_string(),
435 3 => "Bus".to_string(),
436 4 => "Ferry".to_string(),
437 5 => "Cable Tram".to_string(),
438 6 => "Aerial Lift".to_string(),
439 7 => "Funicular".to_string(),
440 _ => format!("Type {}", route_type),
441 }
442 }
443
444 fn mock_stops(&self, query: &str) -> Vec<SemanticVector> {
446 let text = format!("Mock transit stop for {}", query);
447 let embedding = self.embedder.embed_text(&text);
448
449 let mut metadata = HashMap::new();
450 metadata.insert("onestop_id".to_string(), format!("s-mock-{}", query));
451 metadata.insert("stop_name".to_string(), format!("{} Station", query));
452 metadata.insert("stop_desc".to_string(), "Mock stop data".to_string());
453 metadata.insert("latitude".to_string(), "37.7749".to_string());
454 metadata.insert("longitude".to_string(), "-122.4194".to_string());
455 metadata.insert("source".to_string(), "gtfs_mock".to_string());
456
457 vec![SemanticVector {
458 id: format!("GTFS:STOP:MOCK:{}", query),
459 embedding,
460 domain: Domain::Transportation,
461 timestamp: Utc::now(),
462 metadata,
463 }]
464 }
465
466 fn mock_routes(&self, operator_id: &str) -> Vec<SemanticVector> {
467 let text = format!("Mock route for operator {}", operator_id);
468 let embedding = self.embedder.embed_text(&text);
469
470 let mut metadata = HashMap::new();
471 metadata.insert("onestop_id".to_string(), format!("r-mock-{}", operator_id));
472 metadata.insert("route_short_name".to_string(), "1".to_string());
473 metadata.insert("route_long_name".to_string(), "Mock Route 1".to_string());
474 metadata.insert("route_type".to_string(), "Bus".to_string());
475 metadata.insert("source".to_string(), "gtfs_mock".to_string());
476
477 vec![SemanticVector {
478 id: format!("GTFS:ROUTE:MOCK:{}", operator_id),
479 embedding,
480 domain: Domain::Transportation,
481 timestamp: Utc::now(),
482 metadata,
483 }]
484 }
485
486 fn mock_departures(&self, stop_id: &str) -> Vec<SemanticVector> {
487 let text = format!("Mock departure from {}", stop_id);
488 let embedding = self.embedder.embed_text(&text);
489
490 let mut metadata = HashMap::new();
491 metadata.insert("stop_id".to_string(), stop_id.to_string());
492 metadata.insert("departure_time".to_string(), "12:00:00".to_string());
493 metadata.insert("headsign".to_string(), "Mock Destination".to_string());
494 metadata.insert("source".to_string(), "gtfs_mock".to_string());
495
496 vec![SemanticVector {
497 id: format!("GTFS:DEPARTURE:MOCK:{}", stop_id),
498 embedding,
499 domain: Domain::Transportation,
500 timestamp: Utc::now(),
501 metadata,
502 }]
503 }
504
505 fn mock_agencies(&self, query: &str) -> Vec<SemanticVector> {
506 let text = format!("Mock transit agency for {}", query);
507 let embedding = self.embedder.embed_text(&text);
508
509 let mut metadata = HashMap::new();
510 metadata.insert("onestop_id".to_string(), format!("o-mock-{}", query));
511 metadata.insert("name".to_string(), format!("{} Transit Authority", query));
512 metadata.insert("source".to_string(), "gtfs_mock".to_string());
513
514 vec![SemanticVector {
515 id: format!("GTFS:AGENCY:MOCK:{}", query),
516 embedding,
517 domain: Domain::Transportation,
518 timestamp: Utc::now(),
519 metadata,
520 }]
521 }
522
523 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
525 let mut retries = 0;
526 loop {
527 let mut request = self.client.get(url);
528 if let Some(key) = &self.api_key {
529 request = request.header("apikey", key);
530 }
531
532 match request.send().await {
533 Ok(response) => {
534 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
535 retries += 1;
536 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
537 continue;
538 }
539 return Ok(response);
540 }
541 Err(_) if retries < MAX_RETRIES => {
542 retries += 1;
543 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
544 }
545 Err(e) => return Err(FrameworkError::Network(e)),
546 }
547 }
548 }
549}
550
551impl Default for GtfsClient {
552 fn default() -> Self {
553 Self::new()
554 }
555}
556
557#[derive(Debug, Deserialize)]
563struct MobilityDbFeedsResponse {
564 #[serde(default)]
565 results: Vec<MobilityDbFeed>,
566 #[serde(default)]
567 total: Option<u64>,
568}
569
570#[derive(Debug, Deserialize)]
571struct MobilityDbFeed {
572 #[serde(default)]
573 id: String,
574 #[serde(default)]
575 provider: String,
576 #[serde(default)]
577 name: String,
578 #[serde(default)]
579 data_type: String,
580 #[serde(default)]
581 location: MobilityDbLocation,
582 #[serde(default)]
583 urls: MobilityDbUrls,
584 #[serde(default)]
585 status: String,
586}
587
588#[derive(Debug, Default, Deserialize)]
589struct MobilityDbLocation {
590 #[serde(default)]
591 country_code: String,
592 #[serde(default)]
593 subdivision_name: String,
594 #[serde(default)]
595 municipality: String,
596}
597
598#[derive(Debug, Default, Deserialize)]
599struct MobilityDbUrls {
600 #[serde(default)]
601 direct_download: String,
602 #[serde(default)]
603 latest: String,
604}
605
606pub struct MobilityDatabaseClient {
623 client: Client,
624 base_url: String,
625 rate_limit_delay: Duration,
626 embedder: Arc<SimpleEmbedder>,
627}
628
629impl MobilityDatabaseClient {
630 pub fn new() -> Self {
632 Self {
633 client: Client::builder()
634 .user_agent("RuVector-Discovery/1.0")
635 .timeout(Duration::from_secs(30))
636 .build()
637 .expect("Failed to create HTTP client"),
638 base_url: "https://api.mobilitydatabase.org/v1".to_string(),
639 rate_limit_delay: Duration::from_millis(MOBILITY_DB_RATE_LIMIT_MS),
640 embedder: Arc::new(SimpleEmbedder::new(DEFAULT_EMBEDDING_DIM)),
641 }
642 }
643
644 pub async fn list_feeds(&self) -> Result<Vec<SemanticVector>> {
651 let url = format!("{}/feeds?limit=100", self.base_url);
652
653 sleep(self.rate_limit_delay).await;
654 let response = self.fetch_with_retry(&url).await?;
655
656 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
657 return Ok(self.mock_feeds());
658 }
659
660 let feeds_response: MobilityDbFeedsResponse = response.json().await?;
661 self.feeds_to_vectors(feeds_response.results)
662 }
663
664 pub async fn get_feed(&self, feed_id: &str) -> Result<Vec<SemanticVector>> {
674 let url = format!("{}/feeds/{}", self.base_url, feed_id);
675
676 sleep(self.rate_limit_delay).await;
677 let response = self.fetch_with_retry(&url).await?;
678
679 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
680 return Ok(self.mock_feed(feed_id));
681 }
682
683 let feed: MobilityDbFeed = response.json().await?;
684 self.feeds_to_vectors(vec![feed])
685 }
686
687 pub async fn search_feeds(&self, query: &str) -> Result<Vec<SemanticVector>> {
697 let url = format!(
698 "{}/feeds?search={}&limit=50",
699 self.base_url,
700 urlencoding::encode(query)
701 );
702
703 sleep(self.rate_limit_delay).await;
704 let response = self.fetch_with_retry(&url).await?;
705
706 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
707 return Ok(self.mock_feeds());
708 }
709
710 let feeds_response: MobilityDbFeedsResponse = response.json().await?;
711 self.feeds_to_vectors(feeds_response.results)
712 }
713
714 pub async fn get_feed_versions(&self, feed_id: &str) -> Result<Vec<SemanticVector>> {
724 Ok(self.mock_feed_versions(feed_id))
726 }
727
728 fn feeds_to_vectors(&self, feeds: Vec<MobilityDbFeed>) -> Result<Vec<SemanticVector>> {
730 let mut vectors = Vec::new();
731
732 for feed in feeds {
733 let text = format!(
735 "{} {} {} {} {}",
736 feed.provider,
737 feed.name,
738 feed.data_type,
739 feed.location.municipality,
740 feed.location.country_code
741 );
742 let embedding = self.embedder.embed_text(&text);
743
744 let mut metadata = HashMap::new();
745 metadata.insert("feed_id".to_string(), feed.id.clone());
746 metadata.insert("provider".to_string(), feed.provider);
747 metadata.insert("name".to_string(), feed.name);
748 metadata.insert("data_type".to_string(), feed.data_type);
749 metadata.insert("country".to_string(), feed.location.country_code);
750 metadata.insert("subdivision".to_string(), feed.location.subdivision_name);
751 metadata.insert("municipality".to_string(), feed.location.municipality);
752 metadata.insert("status".to_string(), feed.status);
753 metadata.insert("download_url".to_string(), feed.urls.direct_download);
754 metadata.insert("source".to_string(), "mobility_database".to_string());
755
756 vectors.push(SemanticVector {
757 id: format!("MDB:FEED:{}", feed.id),
758 embedding,
759 domain: Domain::Transportation,
760 timestamp: Utc::now(),
761 metadata,
762 });
763 }
764
765 Ok(vectors)
766 }
767
768 fn mock_feeds(&self) -> Vec<SemanticVector> {
770 let text = "Mock mobility database feed";
771 let embedding = self.embedder.embed_text(text);
772
773 let mut metadata = HashMap::new();
774 metadata.insert("feed_id".to_string(), "mdb-mock-1".to_string());
775 metadata.insert("provider".to_string(), "Mock Transit".to_string());
776 metadata.insert("data_type".to_string(), "gtfs".to_string());
777 metadata.insert("source".to_string(), "mobility_database_mock".to_string());
778
779 vec![SemanticVector {
780 id: "MDB:FEED:MOCK:1".to_string(),
781 embedding,
782 domain: Domain::Transportation,
783 timestamp: Utc::now(),
784 metadata,
785 }]
786 }
787
788 fn mock_feed(&self, feed_id: &str) -> Vec<SemanticVector> {
789 let text = format!("Mock feed {}", feed_id);
790 let embedding = self.embedder.embed_text(&text);
791
792 let mut metadata = HashMap::new();
793 metadata.insert("feed_id".to_string(), feed_id.to_string());
794 metadata.insert("provider".to_string(), "Mock Provider".to_string());
795 metadata.insert("source".to_string(), "mobility_database_mock".to_string());
796
797 vec![SemanticVector {
798 id: format!("MDB:FEED:MOCK:{}", feed_id),
799 embedding,
800 domain: Domain::Transportation,
801 timestamp: Utc::now(),
802 metadata,
803 }]
804 }
805
806 fn mock_feed_versions(&self, feed_id: &str) -> Vec<SemanticVector> {
807 let text = format!("Mock feed version for {}", feed_id);
808 let embedding = self.embedder.embed_text(&text);
809
810 let mut metadata = HashMap::new();
811 metadata.insert("feed_id".to_string(), feed_id.to_string());
812 metadata.insert("version".to_string(), "1.0.0".to_string());
813 metadata.insert("source".to_string(), "mobility_database_mock".to_string());
814
815 vec![SemanticVector {
816 id: format!("MDB:VERSION:MOCK:{}", feed_id),
817 embedding,
818 domain: Domain::Transportation,
819 timestamp: Utc::now(),
820 metadata,
821 }]
822 }
823
824 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
826 let mut retries = 0;
827 loop {
828 match self.client.get(url).send().await {
829 Ok(response) => {
830 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
831 retries += 1;
832 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
833 continue;
834 }
835 return Ok(response);
836 }
837 Err(_) if retries < MAX_RETRIES => {
838 retries += 1;
839 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
840 }
841 Err(e) => return Err(FrameworkError::Network(e)),
842 }
843 }
844 }
845}
846
847impl Default for MobilityDatabaseClient {
848 fn default() -> Self {
849 Self::new()
850 }
851}
852
853#[derive(Debug, Deserialize)]
859struct OrsDirectionsResponse {
860 #[serde(default)]
861 routes: Vec<OrsRoute>,
862}
863
864#[derive(Debug, Deserialize)]
865struct OrsRoute {
866 #[serde(default)]
867 summary: OrsRouteSummary,
868 #[serde(default)]
869 geometry: String,
870}
871
872#[derive(Debug, Default, Deserialize)]
873struct OrsRouteSummary {
874 #[serde(default)]
875 distance: f64,
876 #[serde(default)]
877 duration: f64,
878}
879
880#[derive(Debug, Deserialize)]
882struct OrsIsochronesResponse {
883 #[serde(default)]
884 features: Vec<serde_json::Value>,
885}
886
887#[derive(Debug, Deserialize)]
889struct OrsGeocodeResponse {
890 #[serde(default)]
891 features: Vec<OrsGeocodeFeature>,
892}
893
894#[derive(Debug, Deserialize)]
895struct OrsGeocodeFeature {
896 #[serde(default)]
897 properties: OrsGeocodeProperties,
898 #[serde(default)]
899 geometry: OrsGeocodeGeometry,
900}
901
902#[derive(Debug, Default, Deserialize)]
903struct OrsGeocodeProperties {
904 #[serde(default)]
905 label: String,
906 #[serde(default)]
907 name: String,
908 #[serde(default)]
909 country: String,
910 #[serde(default)]
911 region: String,
912}
913
914#[derive(Debug, Default, Deserialize)]
915struct OrsGeocodeGeometry {
916 #[serde(default)]
917 coordinates: Vec<f64>,
918}
919
920pub struct OpenRouteServiceClient {
937 client: Client,
938 base_url: String,
939 api_key: Option<String>,
940 rate_limit_delay: Duration,
941 embedder: Arc<SimpleEmbedder>,
942}
943
944impl OpenRouteServiceClient {
945 pub fn new(api_key: Option<String>) -> Self {
951 Self {
952 client: Client::builder()
953 .user_agent("RuVector-Discovery/1.0")
954 .timeout(Duration::from_secs(30))
955 .build()
956 .expect("Failed to create HTTP client"),
957 base_url: "https://api.openrouteservice.org/v2".to_string(),
958 api_key,
959 rate_limit_delay: Duration::from_millis(OPENROUTE_RATE_LIMIT_MS),
960 embedder: Arc::new(SimpleEmbedder::new(DEFAULT_EMBEDDING_DIM)),
961 }
962 }
963
964 pub async fn get_directions(
980 &self,
981 start: (f64, f64),
982 end: (f64, f64),
983 profile: &str,
984 ) -> Result<Vec<SemanticVector>> {
985 let url = format!("{}/directions/{}", self.base_url, profile);
986 let body = serde_json::json!({
987 "coordinates": [[start.0, start.1], [end.0, end.1]]
988 });
989
990 sleep(self.rate_limit_delay).await;
991
992 let mut request = self.client.post(&url).json(&body);
993 if let Some(key) = &self.api_key {
994 request = request.header("Authorization", key);
995 }
996
997 let response = match request.send().await {
998 Ok(r) => r,
999 Err(_) => return Ok(self.mock_directions(start, end, profile)),
1000 };
1001
1002 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
1003 return Ok(self.mock_directions(start, end, profile));
1004 }
1005
1006 let directions: OrsDirectionsResponse = response.json().await?;
1007
1008 let mut vectors = Vec::new();
1009 for (idx, route) in directions.routes.iter().enumerate() {
1010 let distance_km = route.summary.distance / 1000.0;
1011 let duration_min = route.summary.duration / 60.0;
1012
1013 let text = format!(
1015 "Route from ({}, {}) to ({}, {}) via {}: {:.2} km, {:.0} min",
1016 start.0, start.1, end.0, end.1, profile, distance_km, duration_min
1017 );
1018 let embedding = self.embedder.embed_text(&text);
1019
1020 let mut metadata = HashMap::new();
1021 metadata.insert("start_lon".to_string(), start.0.to_string());
1022 metadata.insert("start_lat".to_string(), start.1.to_string());
1023 metadata.insert("end_lon".to_string(), end.0.to_string());
1024 metadata.insert("end_lat".to_string(), end.1.to_string());
1025 metadata.insert("profile".to_string(), profile.to_string());
1026 metadata.insert("distance_meters".to_string(), route.summary.distance.to_string());
1027 metadata.insert("duration_seconds".to_string(), route.summary.duration.to_string());
1028 metadata.insert("geometry".to_string(), route.geometry.clone());
1029 metadata.insert("source".to_string(), "openrouteservice".to_string());
1030
1031 vectors.push(SemanticVector {
1032 id: format!("ORS:ROUTE:{}:{}:{}", profile, idx, Utc::now().timestamp()),
1033 embedding,
1034 domain: Domain::Transportation,
1035 timestamp: Utc::now(),
1036 metadata,
1037 });
1038 }
1039
1040 Ok(vectors)
1041 }
1042
1043 pub async fn get_isochrones(
1059 &self,
1060 location: (f64, f64),
1061 range: Vec<i32>,
1062 profile: &str,
1063 ) -> Result<Vec<SemanticVector>> {
1064 let url = format!("{}/isochrones/{}", self.base_url, profile);
1065 let body = serde_json::json!({
1066 "locations": [[location.0, location.1]],
1067 "range": range
1068 });
1069
1070 sleep(self.rate_limit_delay).await;
1071
1072 let mut request = self.client.post(&url).json(&body);
1073 if let Some(key) = &self.api_key {
1074 request = request.header("Authorization", key);
1075 }
1076
1077 let response = match request.send().await {
1078 Ok(r) => r,
1079 Err(_) => return Ok(self.mock_isochrones(location, &range, profile)),
1080 };
1081
1082 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
1083 return Ok(self.mock_isochrones(location, &range, profile));
1084 }
1085
1086 let _isochrones: OrsIsochronesResponse = response.json().await?;
1087
1088 let text = format!(
1090 "Isochrone analysis from ({}, {}) via {} for ranges {:?}",
1091 location.0, location.1, profile, range
1092 );
1093 let embedding = self.embedder.embed_text(&text);
1094
1095 let mut metadata = HashMap::new();
1096 metadata.insert("center_lon".to_string(), location.0.to_string());
1097 metadata.insert("center_lat".to_string(), location.1.to_string());
1098 metadata.insert("profile".to_string(), profile.to_string());
1099 metadata.insert("ranges".to_string(), format!("{:?}", range));
1100 metadata.insert("source".to_string(), "openrouteservice".to_string());
1101
1102 Ok(vec![SemanticVector {
1103 id: format!("ORS:ISOCHRONE:{}:{}", profile, Utc::now().timestamp()),
1104 embedding,
1105 domain: Domain::Transportation,
1106 timestamp: Utc::now(),
1107 metadata,
1108 }])
1109 }
1110
1111 pub async fn geocode(&self, query: &str) -> Result<Vec<SemanticVector>> {
1121 let url = format!(
1122 "{}/geocode/search?text={}",
1123 self.base_url,
1124 urlencoding::encode(query)
1125 );
1126
1127 sleep(self.rate_limit_delay).await;
1128
1129 let mut request = self.client.get(&url);
1130 if let Some(key) = &self.api_key {
1131 request = request.header("Authorization", key);
1132 }
1133
1134 let response = match request.send().await {
1135 Ok(r) => r,
1136 Err(_) => return Ok(self.mock_geocode(query)),
1137 };
1138
1139 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
1140 return Ok(self.mock_geocode(query));
1141 }
1142
1143 let geocode_response: OrsGeocodeResponse = response.json().await?;
1144
1145 let mut vectors = Vec::new();
1146 for feature in geocode_response.features {
1147 let coords = &feature.geometry.coordinates;
1148 let (lon, lat) = if coords.len() >= 2 {
1149 (coords[0], coords[1])
1150 } else {
1151 (0.0, 0.0)
1152 };
1153
1154 let text = format!(
1156 "{} {} {} at ({}, {})",
1157 feature.properties.name,
1158 feature.properties.region,
1159 feature.properties.country,
1160 lon,
1161 lat
1162 );
1163 let embedding = self.embedder.embed_text(&text);
1164
1165 let mut metadata = HashMap::new();
1166 metadata.insert("label".to_string(), feature.properties.label);
1167 metadata.insert("name".to_string(), feature.properties.name);
1168 metadata.insert("country".to_string(), feature.properties.country);
1169 metadata.insert("region".to_string(), feature.properties.region);
1170 metadata.insert("longitude".to_string(), lon.to_string());
1171 metadata.insert("latitude".to_string(), lat.to_string());
1172 metadata.insert("source".to_string(), "openrouteservice".to_string());
1173
1174 vectors.push(SemanticVector {
1175 id: format!("ORS:GEOCODE:{}:{}", query, lon),
1176 embedding,
1177 domain: Domain::Geospatial,
1178 timestamp: Utc::now(),
1179 metadata,
1180 });
1181 }
1182
1183 Ok(vectors)
1184 }
1185
1186 fn mock_directions(&self, start: (f64, f64), end: (f64, f64), profile: &str) -> Vec<SemanticVector> {
1188 let text = format!(
1189 "Mock route from ({}, {}) to ({}, {}) via {}",
1190 start.0, start.1, end.0, end.1, profile
1191 );
1192 let embedding = self.embedder.embed_text(&text);
1193
1194 let mut metadata = HashMap::new();
1195 metadata.insert("profile".to_string(), profile.to_string());
1196 metadata.insert("distance_meters".to_string(), "5000.0".to_string());
1197 metadata.insert("duration_seconds".to_string(), "600.0".to_string());
1198 metadata.insert("source".to_string(), "openrouteservice_mock".to_string());
1199
1200 vec![SemanticVector {
1201 id: format!("ORS:ROUTE:MOCK:{}", Utc::now().timestamp()),
1202 embedding,
1203 domain: Domain::Transportation,
1204 timestamp: Utc::now(),
1205 metadata,
1206 }]
1207 }
1208
1209 fn mock_isochrones(&self, location: (f64, f64), range: &[i32], profile: &str) -> Vec<SemanticVector> {
1210 let text = format!(
1211 "Mock isochrone from ({}, {}) via {} for {:?}",
1212 location.0, location.1, profile, range
1213 );
1214 let embedding = self.embedder.embed_text(&text);
1215
1216 let mut metadata = HashMap::new();
1217 metadata.insert("profile".to_string(), profile.to_string());
1218 metadata.insert("ranges".to_string(), format!("{:?}", range));
1219 metadata.insert("source".to_string(), "openrouteservice_mock".to_string());
1220
1221 vec![SemanticVector {
1222 id: format!("ORS:ISOCHRONE:MOCK:{}", Utc::now().timestamp()),
1223 embedding,
1224 domain: Domain::Transportation,
1225 timestamp: Utc::now(),
1226 metadata,
1227 }]
1228 }
1229
1230 fn mock_geocode(&self, query: &str) -> Vec<SemanticVector> {
1231 let text = format!("Mock geocode result for {}", query);
1232 let embedding = self.embedder.embed_text(&text);
1233
1234 let mut metadata = HashMap::new();
1235 metadata.insert("name".to_string(), query.to_string());
1236 metadata.insert("longitude".to_string(), "0.0".to_string());
1237 metadata.insert("latitude".to_string(), "0.0".to_string());
1238 metadata.insert("source".to_string(), "openrouteservice_mock".to_string());
1239
1240 vec![SemanticVector {
1241 id: format!("ORS:GEOCODE:MOCK:{}", query),
1242 embedding,
1243 domain: Domain::Geospatial,
1244 timestamp: Utc::now(),
1245 metadata,
1246 }]
1247 }
1248}
1249
1250#[derive(Debug, Deserialize)]
1256struct OcmPoiResponse {
1257 #[serde(default)]
1258 #[serde(rename = "AddressInfo")]
1259 address_info: OcmAddressInfo,
1260 #[serde(default)]
1261 #[serde(rename = "NumberOfPoints")]
1262 number_of_points: Option<i32>,
1263 #[serde(default)]
1264 #[serde(rename = "StatusType")]
1265 status_type: Option<OcmStatusType>,
1266 #[serde(default)]
1267 #[serde(rename = "Connections")]
1268 connections: Vec<OcmConnection>,
1269 #[serde(rename = "ID")]
1270 id: i32,
1271}
1272
1273#[derive(Debug, Default, Deserialize)]
1274struct OcmAddressInfo {
1275 #[serde(default)]
1276 #[serde(rename = "Title")]
1277 title: String,
1278 #[serde(default)]
1279 #[serde(rename = "AddressLine1")]
1280 address_line1: String,
1281 #[serde(default)]
1282 #[serde(rename = "Town")]
1283 town: String,
1284 #[serde(default)]
1285 #[serde(rename = "StateOrProvince")]
1286 state: String,
1287 #[serde(default)]
1288 #[serde(rename = "Country")]
1289 country: Option<OcmCountry>,
1290 #[serde(default)]
1291 #[serde(rename = "Latitude")]
1292 latitude: f64,
1293 #[serde(default)]
1294 #[serde(rename = "Longitude")]
1295 longitude: f64,
1296}
1297
1298#[derive(Debug, Deserialize)]
1299struct OcmCountry {
1300 #[serde(rename = "Title")]
1301 title: String,
1302}
1303
1304#[derive(Debug, Deserialize)]
1305struct OcmStatusType {
1306 #[serde(default)]
1307 #[serde(rename = "Title")]
1308 title: String,
1309}
1310
1311#[derive(Debug, Deserialize)]
1312struct OcmConnection {
1313 #[serde(default)]
1314 #[serde(rename = "PowerKW")]
1315 power_kw: Option<f64>,
1316 #[serde(default)]
1317 #[serde(rename = "CurrentType")]
1318 current_type: Option<OcmCurrentType>,
1319 #[serde(default)]
1320 #[serde(rename = "Level")]
1321 level: Option<OcmLevel>,
1322}
1323
1324#[derive(Debug, Deserialize)]
1325struct OcmCurrentType {
1326 #[serde(rename = "Title")]
1327 title: String,
1328}
1329
1330#[derive(Debug, Deserialize)]
1331struct OcmLevel {
1332 #[serde(rename = "Title")]
1333 title: String,
1334}
1335
1336pub struct OpenChargeMapClient {
1352 client: Client,
1353 base_url: String,
1354 api_key: Option<String>,
1355 rate_limit_delay: Duration,
1356 embedder: Arc<SimpleEmbedder>,
1357}
1358
1359impl OpenChargeMapClient {
1360 pub fn new(api_key: Option<String>) -> Self {
1366 Self {
1367 client: Client::builder()
1368 .user_agent("RuVector-Discovery/1.0")
1369 .timeout(Duration::from_secs(30))
1370 .build()
1371 .expect("Failed to create HTTP client"),
1372 base_url: "https://api.openchargemap.io/v3".to_string(),
1373 api_key,
1374 rate_limit_delay: Duration::from_millis(OPENCHARGEMAP_RATE_LIMIT_MS),
1375 embedder: Arc::new(SimpleEmbedder::new(DEFAULT_EMBEDDING_DIM)),
1376 }
1377 }
1378
1379 pub async fn get_poi(&self, lat: f64, lng: f64, distance: f64) -> Result<Vec<SemanticVector>> {
1391 let mut url = format!(
1392 "{}/poi?latitude={}&longitude={}&distance={}&distanceunit=KM&maxresults=100",
1393 self.base_url, lat, lng, distance
1394 );
1395
1396 if let Some(key) = &self.api_key {
1397 url.push_str(&format!("&key={}", key));
1398 }
1399
1400 sleep(self.rate_limit_delay).await;
1401 let response = self.fetch_with_retry(&url).await?;
1402
1403 if response.status() == StatusCode::SERVICE_UNAVAILABLE {
1404 return Ok(self.mock_poi(lat, lng));
1405 }
1406
1407 let pois: Vec<OcmPoiResponse> = response.json().await?;
1408
1409 let mut vectors = Vec::new();
1410 for poi in pois {
1411 let country_name = poi
1412 .address_info
1413 .country
1414 .as_ref()
1415 .map(|c| c.title.clone())
1416 .unwrap_or_default();
1417
1418 let status = poi
1419 .status_type
1420 .as_ref()
1421 .map(|s| s.title.clone())
1422 .unwrap_or_else(|| "Unknown".to_string());
1423
1424 let max_power = poi
1426 .connections
1427 .iter()
1428 .filter_map(|c| c.power_kw)
1429 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1430 .unwrap_or(0.0);
1431
1432 let text = format!(
1434 "{} {} {} {} at ({}, {}) - {} kW - {}",
1435 poi.address_info.title,
1436 poi.address_info.address_line1,
1437 poi.address_info.town,
1438 country_name,
1439 poi.address_info.latitude,
1440 poi.address_info.longitude,
1441 max_power,
1442 status
1443 );
1444 let embedding = self.embedder.embed_text(&text);
1445
1446 let mut metadata = HashMap::new();
1447 metadata.insert("station_id".to_string(), poi.id.to_string());
1448 metadata.insert("title".to_string(), poi.address_info.title);
1449 metadata.insert("address".to_string(), poi.address_info.address_line1);
1450 metadata.insert("town".to_string(), poi.address_info.town);
1451 metadata.insert("state".to_string(), poi.address_info.state);
1452 metadata.insert("country".to_string(), country_name);
1453 metadata.insert("latitude".to_string(), poi.address_info.latitude.to_string());
1454 metadata.insert("longitude".to_string(), poi.address_info.longitude.to_string());
1455 metadata.insert("status".to_string(), status);
1456 metadata.insert("max_power_kw".to_string(), max_power.to_string());
1457 metadata.insert(
1458 "num_points".to_string(),
1459 poi.number_of_points.unwrap_or(0).to_string(),
1460 );
1461 metadata.insert("source".to_string(), "openchargemap".to_string());
1462
1463 vectors.push(SemanticVector {
1464 id: format!("OCM:POI:{}", poi.id),
1465 embedding,
1466 domain: Domain::Transportation,
1467 timestamp: Utc::now(),
1468 metadata,
1469 });
1470 }
1471
1472 Ok(vectors)
1473 }
1474
1475 pub async fn search_poi(&self, query: &str) -> Result<Vec<SemanticVector>> {
1485 Ok(self.mock_search(query))
1488 }
1489
1490 pub async fn get_reference_data(&self) -> Result<Vec<SemanticVector>> {
1497 let url = format!("{}/referencedata", self.base_url);
1498
1499 sleep(self.rate_limit_delay).await;
1500 let _response = self.fetch_with_retry(&url).await?;
1501
1502 Ok(self.mock_reference_data())
1504 }
1505
1506 fn mock_poi(&self, lat: f64, lng: f64) -> Vec<SemanticVector> {
1508 let text = format!("Mock EV charging station near ({}, {})", lat, lng);
1509 let embedding = self.embedder.embed_text(&text);
1510
1511 let mut metadata = HashMap::new();
1512 metadata.insert("station_id".to_string(), "mock-1".to_string());
1513 metadata.insert("title".to_string(), "Mock Charging Station".to_string());
1514 metadata.insert("latitude".to_string(), lat.to_string());
1515 metadata.insert("longitude".to_string(), lng.to_string());
1516 metadata.insert("max_power_kw".to_string(), "150.0".to_string());
1517 metadata.insert("source".to_string(), "openchargemap_mock".to_string());
1518
1519 vec![SemanticVector {
1520 id: "OCM:POI:MOCK:1".to_string(),
1521 embedding,
1522 domain: Domain::Transportation,
1523 timestamp: Utc::now(),
1524 metadata,
1525 }]
1526 }
1527
1528 fn mock_search(&self, query: &str) -> Vec<SemanticVector> {
1529 let text = format!("Mock charging station search for {}", query);
1530 let embedding = self.embedder.embed_text(&text);
1531
1532 let mut metadata = HashMap::new();
1533 metadata.insert("title".to_string(), format!("{} Charging Hub", query));
1534 metadata.insert("query".to_string(), query.to_string());
1535 metadata.insert("source".to_string(), "openchargemap_mock".to_string());
1536
1537 vec![SemanticVector {
1538 id: format!("OCM:SEARCH:MOCK:{}", query),
1539 embedding,
1540 domain: Domain::Transportation,
1541 timestamp: Utc::now(),
1542 metadata,
1543 }]
1544 }
1545
1546 fn mock_reference_data(&self) -> Vec<SemanticVector> {
1547 let text = "OpenChargeMap reference data";
1548 let embedding = self.embedder.embed_text(text);
1549
1550 let mut metadata = HashMap::new();
1551 metadata.insert("type".to_string(), "reference_data".to_string());
1552 metadata.insert("source".to_string(), "openchargemap_mock".to_string());
1553
1554 vec![SemanticVector {
1555 id: "OCM:REFERENCE:MOCK".to_string(),
1556 embedding,
1557 domain: Domain::Transportation,
1558 timestamp: Utc::now(),
1559 metadata,
1560 }]
1561 }
1562
1563 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
1565 let mut retries = 0;
1566 loop {
1567 match self.client.get(url).send().await {
1568 Ok(response) => {
1569 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
1570 retries += 1;
1571 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
1572 continue;
1573 }
1574 return Ok(response);
1575 }
1576 Err(_) if retries < MAX_RETRIES => {
1577 retries += 1;
1578 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
1579 }
1580 Err(e) => return Err(FrameworkError::Network(e)),
1581 }
1582 }
1583 }
1584}
1585
1586impl Default for OpenChargeMapClient {
1587 fn default() -> Self {
1588 Self::new(None)
1589 }
1590}
1591
1592#[cfg(test)]
1597mod tests {
1598 use super::*;
1599
1600 #[test]
1602 fn test_gtfs_client_creation() {
1603 let client = GtfsClient::new();
1604 assert_eq!(client.base_url, "https://transit.land/api/v2");
1605 }
1606
1607 #[test]
1608 fn test_gtfs_route_type_conversion() {
1609 assert_eq!(GtfsClient::route_type_to_name(0), "Tram/Light Rail");
1610 assert_eq!(GtfsClient::route_type_to_name(1), "Subway/Metro");
1611 assert_eq!(GtfsClient::route_type_to_name(3), "Bus");
1612 assert_eq!(GtfsClient::route_type_to_name(4), "Ferry");
1613 }
1614
1615 #[tokio::test]
1616 async fn test_gtfs_mock_stops() {
1617 let client = GtfsClient::new();
1618 let stops = client.mock_stops("test");
1619 assert_eq!(stops.len(), 1);
1620 assert!(stops[0].id.contains("MOCK"));
1621 assert_eq!(stops[0].domain, Domain::Transportation);
1622 }
1623
1624 #[tokio::test]
1625 async fn test_gtfs_mock_routes() {
1626 let client = GtfsClient::new();
1627 let routes = client.mock_routes("o-test");
1628 assert_eq!(routes.len(), 1);
1629 assert!(routes[0].metadata.contains_key("route_short_name"));
1630 }
1631
1632 #[test]
1634 fn test_mobility_db_client_creation() {
1635 let client = MobilityDatabaseClient::new();
1636 assert_eq!(client.base_url, "https://api.mobilitydatabase.org/v1");
1637 }
1638
1639 #[tokio::test]
1640 async fn test_mobility_db_mock_feeds() {
1641 let client = MobilityDatabaseClient::new();
1642 let feeds = client.mock_feeds();
1643 assert_eq!(feeds.len(), 1);
1644 assert!(feeds[0].id.contains("MDB"));
1645 }
1646
1647 #[test]
1648 fn test_mobility_db_rate_limiting() {
1649 let client = MobilityDatabaseClient::new();
1650 assert_eq!(
1651 client.rate_limit_delay,
1652 Duration::from_millis(MOBILITY_DB_RATE_LIMIT_MS)
1653 );
1654 }
1655
1656 #[test]
1658 fn test_openroute_client_creation() {
1659 let client = OpenRouteServiceClient::new(None);
1660 assert_eq!(client.base_url, "https://api.openrouteservice.org/v2");
1661 }
1662
1663 #[tokio::test]
1664 async fn test_openroute_mock_directions() {
1665 let client = OpenRouteServiceClient::new(None);
1666 let route = client.mock_directions((8.68, 49.41), (8.69, 49.42), "driving-car");
1667 assert_eq!(route.len(), 1);
1668 assert!(route[0].metadata.contains_key("distance_meters"));
1669 assert!(route[0].metadata.contains_key("duration_seconds"));
1670 }
1671
1672 #[tokio::test]
1673 async fn test_openroute_mock_geocode() {
1674 let client = OpenRouteServiceClient::new(None);
1675 let results = client.mock_geocode("Heidelberg");
1676 assert_eq!(results.len(), 1);
1677 assert_eq!(results[0].domain, Domain::Geospatial);
1678 }
1679
1680 #[test]
1682 fn test_openchargemap_client_creation() {
1683 let client = OpenChargeMapClient::new(None);
1684 assert_eq!(client.base_url, "https://api.openchargemap.io/v3");
1685 }
1686
1687 #[tokio::test]
1688 async fn test_openchargemap_mock_poi() {
1689 let client = OpenChargeMapClient::new(None);
1690 let stations = client.mock_poi(37.7749, -122.4194);
1691 assert_eq!(stations.len(), 1);
1692 assert!(stations[0].metadata.contains_key("max_power_kw"));
1693 assert_eq!(stations[0].domain, Domain::Transportation);
1694 }
1695
1696 #[test]
1697 fn test_rate_limit_configuration() {
1698 assert_eq!(GTFS_RATE_LIMIT_MS, 1000);
1699 assert_eq!(MOBILITY_DB_RATE_LIMIT_MS, 600);
1700 assert_eq!(OPENROUTE_RATE_LIMIT_MS, 1000);
1701 assert_eq!(OPENCHARGEMAP_RATE_LIMIT_MS, 100);
1702 }
1703
1704 #[tokio::test]
1705 #[ignore] async fn test_gtfs_search_stops_integration() {
1707 let client = GtfsClient::new();
1708 let result = client.search_stops("San Francisco").await;
1709 assert!(result.is_ok());
1711 }
1712
1713 #[tokio::test]
1714 #[ignore] async fn test_mobility_db_list_feeds_integration() {
1716 let client = MobilityDatabaseClient::new();
1717 let result = client.list_feeds().await;
1718 assert!(result.is_ok());
1719 }
1720}