ruvector_data_framework/
transportation_clients.rs

1//! Transportation and Mobility API Integrations
2//!
3//! This module provides async clients for fetching transportation data including:
4//! - **GTFS** - General Transit Feed Specification (public transit)
5//! - **Mobility Database** - Global mobility data catalog
6//! - **OpenRouteService** - Routing and directions
7//! - **OpenChargeMap** - Electric vehicle charging stations
8//!
9//! All clients convert responses to SemanticVector format for RuVector discovery.
10
11use 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
24/// Rate limiting configuration
25const GTFS_RATE_LIMIT_MS: u64 = 1000; // 60 requests/minute
26const MOBILITY_DB_RATE_LIMIT_MS: u64 = 600; // 100 requests/minute
27const OPENROUTE_RATE_LIMIT_MS: u64 = 1000; // Conservative for free tier
28const OPENCHARGEMAP_RATE_LIMIT_MS: u64 = 100; // 10 requests/second
29const MAX_RETRIES: u32 = 3;
30const RETRY_DELAY_MS: u64 = 1000;
31const DEFAULT_EMBEDDING_DIM: usize = 256;
32
33// ============================================================================
34// GTFS (General Transit Feed Specification) Client - Transitland API
35// ============================================================================
36
37/// Transitland API stop response
38#[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/// Transitland API route response
77#[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/// Transitland API departures response
100#[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/// Transitland API agencies response
125#[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
145/// Client for GTFS (General Transit Feed Specification) via Transitland API
146///
147/// Provides access to public transit data including:
148/// - Transit stops and stations
149/// - Routes and schedules
150/// - Real-time departures
151/// - Transit agencies/operators
152///
153/// # Example
154/// ```rust,ignore
155/// use ruvector_data_framework::GtfsClient;
156///
157/// let client = GtfsClient::new();
158/// let stops = client.search_stops("San Francisco").await?;
159/// let routes = client.get_routes("o-9q9-bart").await?;
160/// let departures = client.get_departures("s-9q8y-16thandmission").await?;
161/// ```
162pub 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    /// Create a new GTFS client using Transitland API v2
172    ///
173    /// # Arguments
174    /// * `api_key` - Optional Transitland API key for higher rate limits
175    ///               Free tier: 60 requests/minute
176    pub fn new() -> Self {
177        Self::with_api_key(None)
178    }
179
180    /// Create a new GTFS client with API key
181    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    /// Search for transit stops by name or location
196    ///
197    /// # Arguments
198    /// * `query` - Search query (stop name, city, or location)
199    ///
200    /// # Example
201    /// ```rust,ignore
202    /// let stops = client.search_stops("Union Station").await?;
203    /// ```
204    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 API is unavailable, return mock data
215        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            // Create text for embedding
236            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    /// Get routes for a specific transit operator
268    ///
269    /// # Arguments
270    /// * `operator_id` - Transitland operator onestop_id (e.g., "o-9q9-bart")
271    ///
272    /// # Example
273    /// ```rust,ignore
274    /// let routes = client.get_routes("o-9q9-bart").await?;
275    /// ```
276    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            // Create text for embedding
296            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    /// Get upcoming departures for a transit stop
325    ///
326    /// # Arguments
327    /// * `stop_id` - Transitland stop onestop_id
328    ///
329    /// # Example
330    /// ```rust,ignore
331    /// let departures = client.get_departures("s-9q8y-16thandmission").await?;
332    /// ```
333    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                // Create text for embedding
352                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    /// Search for transit agencies/operators
380    ///
381    /// # Arguments
382    /// * `query` - Search query (agency name or location)
383    ///
384    /// # Example
385    /// ```rust,ignore
386    /// let agencies = client.search_agencies("New York").await?;
387    /// ```
388    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            // Create text for embedding
407            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    /// Convert GTFS route type integer to human-readable name
430    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    // Mock data methods for when API is unavailable
445    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    /// Fetch with retry logic
524    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// ============================================================================
558// Mobility Database Client
559// ============================================================================
560
561/// Mobility Database feed response
562#[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
606/// Client for Mobility Database - Global mobility data catalog
607///
608/// Provides access to:
609/// - Global GTFS feeds catalog
610/// - Transit provider information
611/// - Feed versions and updates
612///
613/// # Example
614/// ```rust,ignore
615/// use ruvector_data_framework::MobilityDatabaseClient;
616///
617/// let client = MobilityDatabaseClient::new();
618/// let feeds = client.list_feeds().await?;
619/// let feed = client.get_feed("mdb-123").await?;
620/// let search = client.search_feeds("San Francisco").await?;
621/// ```
622pub struct MobilityDatabaseClient {
623    client: Client,
624    base_url: String,
625    rate_limit_delay: Duration,
626    embedder: Arc<SimpleEmbedder>,
627}
628
629impl MobilityDatabaseClient {
630    /// Create a new Mobility Database client
631    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    /// List all available feeds
645    ///
646    /// # Example
647    /// ```rust,ignore
648    /// let feeds = client.list_feeds().await?;
649    /// ```
650    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    /// Get a specific feed by ID
665    ///
666    /// # Arguments
667    /// * `feed_id` - Mobility Database feed ID
668    ///
669    /// # Example
670    /// ```rust,ignore
671    /// let feed = client.get_feed("mdb-123").await?;
672    /// ```
673    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    /// Search feeds by query
688    ///
689    /// # Arguments
690    /// * `query` - Search query (provider name, location, etc.)
691    ///
692    /// # Example
693    /// ```rust,ignore
694    /// let results = client.search_feeds("London").await?;
695    /// ```
696    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    /// Get feed versions for a specific feed
715    ///
716    /// # Arguments
717    /// * `feed_id` - Mobility Database feed ID
718    ///
719    /// # Example
720    /// ```rust,ignore
721    /// let versions = client.get_feed_versions("mdb-123").await?;
722    /// ```
723    pub async fn get_feed_versions(&self, feed_id: &str) -> Result<Vec<SemanticVector>> {
724        // Mock implementation as versioning endpoint may vary
725        Ok(self.mock_feed_versions(feed_id))
726    }
727
728    /// Convert feeds to SemanticVectors
729    fn feeds_to_vectors(&self, feeds: Vec<MobilityDbFeed>) -> Result<Vec<SemanticVector>> {
730        let mut vectors = Vec::new();
731
732        for feed in feeds {
733            // Create text for embedding
734            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    // Mock data methods
769    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    /// Fetch with retry logic
825    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// ============================================================================
854// OpenRouteService Client
855// ============================================================================
856
857/// OpenRouteService directions response
858#[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/// OpenRouteService isochrones response
881#[derive(Debug, Deserialize)]
882struct OrsIsochronesResponse {
883    #[serde(default)]
884    features: Vec<serde_json::Value>,
885}
886
887/// OpenRouteService geocoding response
888#[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
920/// Client for OpenRouteService - Routing and directions
921///
922/// Provides access to:
923/// - Route directions and navigation
924/// - Isochrones (reachability analysis)
925/// - Geocoding and reverse geocoding
926///
927/// # Example
928/// ```rust,ignore
929/// use ruvector_data_framework::OpenRouteServiceClient;
930///
931/// let client = OpenRouteServiceClient::new(None);
932/// let route = client.get_directions((8.681495, 49.41461), (8.687872, 49.420318), "driving-car").await?;
933/// let isochrones = client.get_isochrones((8.681495, 49.41461), vec![300, 600], "foot-walking").await?;
934/// let geocode = client.geocode("Heidelberg").await?;
935/// ```
936pub 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    /// Create a new OpenRouteService client
946    ///
947    /// # Arguments
948    /// * `api_key` - Optional API key (free tier: 2000 requests/day)
949    ///               Get key from https://openrouteservice.org/dev/#/signup
950    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    /// Get directions between two points
965    ///
966    /// # Arguments
967    /// * `start` - Start coordinates (longitude, latitude)
968    /// * `end` - End coordinates (longitude, latitude)
969    /// * `profile` - Routing profile: "driving-car", "cycling-regular", "foot-walking", etc.
970    ///
971    /// # Example
972    /// ```rust,ignore
973    /// let route = client.get_directions(
974    ///     (8.681495, 49.41461),
975    ///     (8.687872, 49.420318),
976    ///     "driving-car"
977    /// ).await?;
978    /// ```
979    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            // Create text for embedding
1014            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    /// Get isochrones (reachability polygons)
1044    ///
1045    /// # Arguments
1046    /// * `location` - Center point (longitude, latitude)
1047    /// * `range` - Time ranges in seconds (e.g., vec![300, 600, 900] for 5, 10, 15 minutes)
1048    /// * `profile` - Travel profile: "driving-car", "cycling-regular", "foot-walking"
1049    ///
1050    /// # Example
1051    /// ```rust,ignore
1052    /// let isochrones = client.get_isochrones(
1053    ///     (8.681495, 49.41461),
1054    ///     vec![300, 600, 900],
1055    ///     "foot-walking"
1056    /// ).await?;
1057    /// ```
1058    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        // Create vector for isochrone analysis
1089        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    /// Geocode an address to coordinates
1112    ///
1113    /// # Arguments
1114    /// * `query` - Address or place name to geocode
1115    ///
1116    /// # Example
1117    /// ```rust,ignore
1118    /// let results = client.geocode("Heidelberg, Germany").await?;
1119    /// ```
1120    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            // Create text for embedding
1155            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    // Mock data methods
1187    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// ============================================================================
1251// OpenChargeMap Client
1252// ============================================================================
1253
1254/// OpenChargeMap POI response
1255#[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
1336/// Client for OpenChargeMap - EV charging stations
1337///
1338/// Provides access to:
1339/// - Electric vehicle charging station locations
1340/// - Connector types and power levels
1341/// - Station availability status
1342///
1343/// # Example
1344/// ```rust,ignore
1345/// use ruvector_data_framework::OpenChargeMapClient;
1346///
1347/// let client = OpenChargeMapClient::new(None);
1348/// let stations = client.get_poi(37.7749, -122.4194, 10.0).await?;
1349/// let search = client.search_poi("San Francisco").await?;
1350/// ```
1351pub 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    /// Create a new OpenChargeMap client
1361    ///
1362    /// # Arguments
1363    /// * `api_key` - Optional API key (not required for basic access)
1364    ///               Rate limit: 10 requests/second
1365    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    /// Get charging stations near a location
1380    ///
1381    /// # Arguments
1382    /// * `lat` - Latitude
1383    /// * `lng` - Longitude
1384    /// * `distance` - Search radius in kilometers
1385    ///
1386    /// # Example
1387    /// ```rust,ignore
1388    /// let stations = client.get_poi(37.7749, -122.4194, 10.0).await?;
1389    /// ```
1390    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            // Extract connection info
1425            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            // Create text for embedding
1433            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    /// Search for charging stations by query
1476    ///
1477    /// # Arguments
1478    /// * `query` - Search query (city, address, etc.)
1479    ///
1480    /// # Example
1481    /// ```rust,ignore
1482    /// let results = client.search_poi("Los Angeles").await?;
1483    /// ```
1484    pub async fn search_poi(&self, query: &str) -> Result<Vec<SemanticVector>> {
1485        // OpenChargeMap doesn't have direct text search, so we use mock data
1486        // In production, you'd geocode first then search by coordinates
1487        Ok(self.mock_search(query))
1488    }
1489
1490    /// Get reference data (connector types, networks, etc.)
1491    ///
1492    /// # Example
1493    /// ```rust,ignore
1494    /// let reference = client.get_reference_data().await?;
1495    /// ```
1496    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        // Mock reference data
1503        Ok(self.mock_reference_data())
1504    }
1505
1506    // Mock data methods
1507    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    /// Fetch with retry logic
1564    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// ============================================================================
1593// Tests
1594// ============================================================================
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599
1600    // GTFS Client Tests
1601    #[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    // Mobility Database Tests
1633    #[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    // OpenRouteService Tests
1657    #[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    // OpenChargeMap Tests
1681    #[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] // Ignore by default to avoid hitting APIs
1706    async fn test_gtfs_search_stops_integration() {
1707        let client = GtfsClient::new();
1708        let result = client.search_stops("San Francisco").await;
1709        // Should either succeed or gracefully fail with mock data
1710        assert!(result.is_ok());
1711    }
1712
1713    #[tokio::test]
1714    #[ignore] // Ignore by default to avoid hitting APIs
1715    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}