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