paiagram_core/station/
fetch_name.rs1use 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#[derive(Component)]
20pub struct StationNamePending(Task<Option<(String, NodeCoor)>>);
21
22#[derive(Deserialize)]
23struct OSMResponse {
24 elements: Vec<OSMResponseInner>,
25}
26
27#[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}