Skip to main content

paiagram_core/station/
fetch_name.rs

1use std::collections::HashMap;
2
3use bevy::prelude::*;
4use bevy::tasks::{AsyncComputeTaskPool, Task, block_on, poll_once};
5use serde::Deserialize;
6
7use crate::graph::{Node, NodeCoor};
8
9pub(super) struct FetchNamePlugin;
10
11impl Plugin for FetchNamePlugin {
12    fn build(&self, app: &mut App) {
13        app.add_systems(Update, fetch_station_name);
14    }
15}
16
17/// Stations with this marker component is created with a default name, and the
18/// name would be fetched via OSM services
19#[derive(Component)]
20pub struct StationNamePending(Task<Option<(String, NodeCoor)>>);
21
22#[derive(Deserialize)]
23struct OSMResponse {
24    elements: Vec<OSMResponseInner>,
25}
26
27// TODO: unify the networking parts
28#[derive(Deserialize)]
29struct OSMResponseInner {
30    lon: Option<f64>,
31    lat: Option<f64>,
32    center: Option<OSMCenter>,
33    tags: HashMap<String, String>,
34}
35
36#[derive(Deserialize)]
37struct OSMCenter {
38    lon: f64,
39    lat: f64,
40}
41
42impl StationNamePending {
43    pub fn new(coor: NodeCoor) -> Self {
44        let task = AsyncComputeTaskPool::get().spawn(Self::fetch(coor));
45        Self(task)
46    }
47    async fn fetch(coor: NodeCoor) -> Option<(String, NodeCoor)> {
48        let NodeCoor { lon, lat } = coor;
49        const RADIUS_METERS: u32 = 1000;
50        const MAX_RETRY_COUNT: usize = 3;
51        const OVERPASS_ENDPOINTS: [&str; 2] = [
52            "https://maps.mail.ru/osm/tools/overpass/api/interpreter",
53            "https://overpass-api.de/api/interpreter",
54        ];
55        let query = format!(
56            r#"
57[out:json][timeout:25];
58nwr[~"^(railway|public_transport|station|subway|light_rail)$"~"^(station|halt|stop|tram_stop|subway_entrance|monorail_station|light_rail_station|narrow_gauge_station|funicular_station|preserved|disused_station|stop_position|platform|stop_area|subway|railway|tram)$"](around:{RADIUS_METERS}, {lat}, {lon});
59out center;
60"#
61        );
62
63        let mut osm_data: Option<OSMResponse> = None;
64
65        'breakpoint: for i in 1..=MAX_RETRY_COUNT {
66            for &endpoint in &OVERPASS_ENDPOINTS {
67                info!("Fetching name of ({coor}) via OSM... ({i}/{MAX_RETRY_COUNT})");
68                let request = ehttp::Request::post(
69                    endpoint,
70                    format!("data={}", urlencoding::encode(&query)).into_bytes(),
71                );
72                let response = match ehttp::fetch_async(request).await {
73                    Ok(resp) => resp,
74                    Err(e) => {
75                        warn!("OSM request failed: {e}");
76                        continue;
77                    }
78                };
79                if !response.ok {
80                    let body_preview = response
81                        .text()
82                        .map(|t| t.chars().take(200).collect::<String>())
83                        .unwrap_or_else(|| "<non-utf8>".to_string());
84                    warn!(
85                        "OSM bad response: endpoint={}, status={} {}, content_type={:?}, body_preview={:?}",
86                        endpoint,
87                        response.status,
88                        response.status_text,
89                        response.content_type(),
90                        body_preview
91                    );
92                    continue;
93                }
94                match response.json() {
95                    Ok(data) => {
96                        osm_data = Some(data);
97                        break 'breakpoint;
98                    }
99                    Err(e) => {
100                        warn!(?e)
101                    }
102                };
103            }
104        }
105        let Some(osm_data) = osm_data else {
106            return None;
107        };
108        osm_data
109            .elements
110            .into_iter()
111            .filter_map(|mut data| {
112                let name = data.tags.remove("name")?;
113                let coor = match (data.lon, data.lat, data.center) {
114                    (Some(lon), Some(lat), _) => NodeCoor { lon, lat },
115                    (_, _, Some(center)) => NodeCoor {
116                        lon: center.lon,
117                        lat: center.lat,
118                    },
119                    _ => return None,
120                };
121                Some((name, coor))
122            })
123            .min_by(|(_, coor_a), (_, coor_b)| {
124                let dist_a = (coor_a.lon - lon).powi(2) + (coor_a.lat - lat).powi(2);
125                let dist_b = (coor_b.lon - lon).powi(2) + (coor_b.lat - lat).powi(2);
126                dist_a.total_cmp(&dist_b)
127            })
128    }
129}
130
131fn fetch_station_name(
132    mut pending_entries: Query<(Entity, &mut Node, &mut Name, &mut StationNamePending)>,
133    mut commands: Commands,
134) {
135    for (entity, mut node, mut name, mut pending_name) in pending_entries.iter_mut() {
136        let Some(found) = block_on(poll_once(&mut pending_name.0)) else {
137            continue;
138        };
139        if let Some((found_name, found_coor)) = found {
140            name.set(found_name);
141            node.coor = found_coor;
142        } else {
143            name.set("Name Not Found")
144        };
145        commands.entity(entity).remove::<StationNamePending>();
146    }
147}