Skip to main content

paiagram_core/station/
fetch_name.rs

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