sxandie commited on
Commit
ca769ce
·
1 Parent(s): 262624f

feat: smooth marker animations via postMessage and enhanced POI categorization

Browse files
Files changed (2) hide show
  1. app.py +183 -34
  2. src/gpx_parser.py +8 -3
app.py CHANGED
@@ -88,15 +88,40 @@ def generate_folium_map(points, checkpoints, pois, hiker_pos=None):
88
  icon_color = "purple"
89
  icon_name = "info-sign"
90
 
91
- if "water" in poi_type or "spring" in poi_type:
 
 
 
 
 
 
 
 
 
 
92
  icon_color = "blue"
 
 
 
93
  icon_name = "tint"
94
- elif "camp" in poi_type:
95
- icon_color = "orange"
96
  icon_name = "home"
97
- elif "hut" in poi_type or "shelter" in poi_type:
 
 
 
98
  icon_color = "green"
99
- icon_name = "home"
 
 
 
 
 
 
 
 
 
100
 
101
  popup_text = f"""
102
  <div style="font-family: 'Outfit', sans-serif; font-size: 11px;">
@@ -112,24 +137,77 @@ def generate_folium_map(points, checkpoints, pois, hiker_pos=None):
112
  icon=folium.Icon(color=icon_color, icon=icon_name)
113
  ).add_to(m)
114
 
115
- # Draw hiker's simulated position
116
- if hiker_pos:
117
- folium.Marker(
118
- location=[hiker_pos["lat"], hiker_pos["lon"]],
119
- popup=f"Current: {hiker_pos['cum_dist']/1000.0:.2f}km<br>Ele: {hiker_pos['ele']:.1f}m",
120
- tooltip="Hiker Position",
121
- icon=folium.Icon(color="red", icon="user")
122
- ).add_to(m)
123
-
124
  return m._repr_html_()
125
 
126
  def get_map_iframe(map_html):
127
  """
128
  Helper to bundle raw HTML into a secure, sandboxed base64 data URI iframe.
129
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  b64_html = base64.b64encode(map_html.encode('utf-8')).decode('utf-8')
131
  iframe_src = f"data:text/html;base64,{b64_html}"
132
- return f'<iframe src="{iframe_src}" width="100%" height="520px" style="border:1px solid rgba(245,158,11,0.2); border-radius: 12px;"></iframe>'
133
 
134
  def fetch_ors_route(start_coords, end_coords, profile, api_key):
135
  """
@@ -262,6 +340,7 @@ def handle_route_update(preloaded_sel, uploaded_file, start_coords, end_coords,
262
  0,
263
  gr.update(active=False),
264
  "",
 
265
  ""
266
  )
267
 
@@ -273,7 +352,16 @@ def handle_route_update(preloaded_sel, uploaded_file, start_coords, end_coords,
273
  except Exception as ex:
274
  print(f"[app] Error saving enhanced GPX: {ex}")
275
 
276
- return stats_html, map_iframe, checkpoint_table_data, data, 0, gr.update(active=False), "", ""
 
 
 
 
 
 
 
 
 
277
 
278
  def handle_ors_fetch_click(start_coords, end_coords, profile, api_key):
279
  try:
@@ -287,7 +375,15 @@ def handle_ors_fetch_click(start_coords, end_coords, profile, api_key):
287
  except Exception as ex:
288
  print(f"[app] Error saving enhanced GPX: {ex}")
289
 
290
- return stats_html, map_iframe, checkpoint_table_data, data, 0, gr.update(active=False), "", ""
 
 
 
 
 
 
 
 
291
  except Exception as e:
292
  return (
293
  f"<div style='color:#ef4444;'>Error: {e}</div>",
@@ -297,20 +393,24 @@ def handle_ors_fetch_click(start_coords, end_coords, profile, api_key):
297
  0,
298
  gr.update(active=False),
299
  "",
 
300
  ""
301
  )
302
 
 
 
 
303
  # --- Playback Simulation Loop ---
304
  def step_simulation(current_idx, route_data, speed):
305
  if not route_data or "points" not in route_data:
306
- return current_idx, gr.update(), gr.update(), gr.update(), gr.update()
307
 
308
  points = route_data["points"]
309
  checkpoints = route_data["checkpoints"]
310
  pois = route_data.get("pois", [])
311
 
312
  if current_idx >= len(points):
313
- return current_idx, gr.update(), gr.update(), gr.update(), gr.update()
314
 
315
  step_size = int(speed)
316
  next_idx = current_idx + step_size
@@ -336,9 +436,14 @@ def step_simulation(current_idx, route_data, speed):
336
  "drinking_water": "💧",
337
  "spring": "💧",
338
  "water_point": "💧",
 
339
  "alpine_hut": "🏡",
 
340
  "camp_site": "⛺",
341
- "shelter": "🛡️"
 
 
 
342
  }
343
  icon = icon_map.get(poi["type"], "📍")
344
  active_alerts.append(f"<div style='background:rgba(245,158,11,0.15); border:1px solid #f59e0b; padding:10px; border-radius:8px; margin-bottom:5px; color:#f59e0b;'>{icon} <b>PROXIMITY:</b> {poi['name']} is {d:.0f}m away! ({poi['type'].replace('_', ' ').title()})</div>")
@@ -358,10 +463,6 @@ def step_simulation(current_idx, route_data, speed):
358
 
359
  alerts_html = "".join(active_alerts) if active_alerts else "<div style='color:var(--text-muted);'>No active proximity alerts.</div>"
360
 
361
- # Render updated folium map
362
- map_html = generate_folium_map(points, checkpoints, pois, hiker_pos=current_pt)
363
- map_iframe = get_map_iframe(map_html)
364
-
365
  # Live HUD Panel
366
  hud_html = f"""
367
  <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 15px; margin-bottom: 20px;'>
@@ -402,7 +503,15 @@ def step_simulation(current_idx, route_data, speed):
402
  """
403
  break
404
 
405
- return next_idx, hud_html, map_iframe, alerts_html, narration_html
 
 
 
 
 
 
 
 
406
 
407
  # --- First-Aid Manual Search ---
408
  def handle_first_aid_search(query):
@@ -452,6 +561,36 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
452
  # State management
453
  route_state = gr.State({})
454
  current_point_idx = gr.State(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
  gr.HTML("""
457
  <div style='text-align: center; padding: 10px 0;'>
@@ -549,11 +688,13 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
549
  ]
550
  )
551
 
 
 
552
  # --- Simulation player bindings ---
553
  timer.tick(
554
  fn=step_simulation,
555
  inputs=[current_point_idx, route_state, speed_slider],
556
- outputs=[current_point_idx, stats_display, map_display, alerts_output, narration_output]
557
  )
558
 
559
  play_btn.click(
@@ -571,15 +712,22 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
571
  def handle_reset(route):
572
  pts = route.get("points", [])
573
  if pts:
574
- # Reformat map to reset pose
575
  stats_html, map_iframe, checkpoint_table_data = format_route_view(route)
576
- return 0, gr.update(active=False), map_iframe, stats_html, "", ""
577
- return 0, gr.update(active=False), gr.update(), gr.update(), "", ""
 
 
 
 
 
 
 
 
578
 
579
  reset_btn.click(
580
  fn=handle_reset,
581
  inputs=[route_state],
582
- outputs=[current_point_idx, timer, map_display, stats_display, alerts_output, narration_output]
583
  )
584
 
585
  # --- Route Ingestion Triggers ---
@@ -587,29 +735,30 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
587
  demo.load(
588
  fn=handle_route_update,
589
  inputs=[preloaded_route, upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
590
- outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output]
591
  )
592
 
593
  # Preloaded selection change
594
  preloaded_route.change(
595
  fn=handle_route_update,
596
  inputs=[preloaded_route, gr.State(None), gr.State(""), gr.State(""), gr.State(""), gr.State("")],
597
- outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output]
598
  )
599
 
600
  # Uploaded file change
601
  upload_file.change(
602
  fn=handle_route_update,
603
  inputs=[gr.State(None), upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
604
- outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output]
605
  )
606
 
607
  # Fetch route button click
608
  fetch_route_btn.click(
609
  fn=handle_ors_fetch_click,
610
  inputs=[start_pt, end_pt, ors_profile, ors_api_key],
611
- outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output]
612
  )
 
613
 
614
  # --- RAG Trigger ---
615
  rag_search_btn.click(
 
88
  icon_color = "purple"
89
  icon_name = "info-sign"
90
 
91
+ # Color coding:
92
+ # - drinking_water / water_point / fountain -> blue / glass
93
+ # - spring -> lightblue / tint
94
+ # - alpine_hut / wilderness_hut -> darkgreen / home
95
+ # - camp_site -> orange / fire
96
+ # - shelter -> green / leaf
97
+ # - viewpoint -> purple / camera
98
+ # - peak -> darkpurple / flag
99
+ # - phone -> red / phone
100
+
101
+ if poi_type in ["drinking_water", "water_point", "fountain"]:
102
  icon_color = "blue"
103
+ icon_name = "glass"
104
+ elif poi_type == "spring":
105
+ icon_color = "lightblue"
106
  icon_name = "tint"
107
+ elif poi_type in ["alpine_hut", "wilderness_hut"]:
108
+ icon_color = "darkgreen"
109
  icon_name = "home"
110
+ elif poi_type == "camp_site":
111
+ icon_color = "orange"
112
+ icon_name = "fire"
113
+ elif poi_type == "shelter":
114
  icon_color = "green"
115
+ icon_name = "leaf"
116
+ elif poi_type == "viewpoint":
117
+ icon_color = "purple"
118
+ icon_name = "camera"
119
+ elif poi_type == "peak":
120
+ icon_color = "darkpurple"
121
+ icon_name = "flag"
122
+ elif poi_type == "phone":
123
+ icon_color = "red"
124
+ icon_name = "phone"
125
 
126
  popup_text = f"""
127
  <div style="font-family: 'Outfit', sans-serif; font-size: 11px;">
 
137
  icon=folium.Icon(color=icon_color, icon=icon_name)
138
  ).add_to(m)
139
 
 
 
 
 
 
 
 
 
 
140
  return m._repr_html_()
141
 
142
  def get_map_iframe(map_html):
143
  """
144
  Helper to bundle raw HTML into a secure, sandboxed base64 data URI iframe.
145
  """
146
+ injected_js = """
147
+ <script>
148
+ // Poll for Leaflet to load and override L.map to capture the map object
149
+ (function() {
150
+ var checkExist = setInterval(function() {
151
+ if (typeof L !== 'undefined' && L.map) {
152
+ clearInterval(checkExist);
153
+ var originalMap = L.map;
154
+ L.map = function(id, options) {
155
+ var m = originalMap(id, options);
156
+ window.myLeafletMap = m;
157
+ return m;
158
+ };
159
+ }
160
+ }, 50);
161
+ })();
162
+
163
+ // Listen for coordinates update from parent Gradio frame
164
+ window.addEventListener("message", function(event) {
165
+ if (event.data && event.data.type === "update_hiker_pos") {
166
+ var lat = event.data.lat;
167
+ var lon = event.data.lon;
168
+ var ele = event.data.ele;
169
+ var dist = event.data.dist;
170
+
171
+ var map = window.myLeafletMap;
172
+ if (!map) return;
173
+
174
+ // Check if hikerMarker exists, otherwise create it
175
+ if (!window.hikerMarker) {
176
+ var redIcon = L.icon({
177
+ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
178
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
179
+ iconSize: [25, 41],
180
+ iconAnchor: [12, 41],
181
+ popupAnchor: [1, -34],
182
+ shadowSize: [41, 41]
183
+ });
184
+ window.hikerMarker = L.marker([lat, lon], {icon: redIcon}).addTo(map);
185
+ } else {
186
+ window.hikerMarker.setLatLng([lat, lon]);
187
+ }
188
+
189
+ window.hikerMarker.bindPopup(
190
+ "<div style='font-family: \\\"Outfit\\\", sans-serif; font-size: 11px;'>" +
191
+ "<b>Current Position</b><br>" +
192
+ "Distance: " + dist.toFixed(2) + " km<br>" +
193
+ "Altitude: " + ele.toFixed(1) + " m" +
194
+ "</div>"
195
+ );
196
+
197
+ // Center the map smoothly on the updated coordinate
198
+ map.panTo([lat, lon]);
199
+ }
200
+ });
201
+ </script>
202
+ """
203
+ if "</body>" in map_html:
204
+ map_html = map_html.replace("</body>", injected_js + "</body>")
205
+ else:
206
+ map_html = map_html + injected_js
207
+
208
  b64_html = base64.b64encode(map_html.encode('utf-8')).decode('utf-8')
209
  iframe_src = f"data:text/html;base64,{b64_html}"
210
+ return f'<div id="trailhead-map-iframe"><iframe src="{iframe_src}" width="100%" height="520px" style="border:1px solid rgba(245,158,11,0.2); border-radius: 12px;"></iframe></div>'
211
 
212
  def fetch_ors_route(start_coords, end_coords, profile, api_key):
213
  """
 
340
  0,
341
  gr.update(active=False),
342
  "",
343
+ "",
344
  ""
345
  )
346
 
 
352
  except Exception as ex:
353
  print(f"[app] Error saving enhanced GPX: {ex}")
354
 
355
+ import json
356
+ start_pt = data["points"][0]
357
+ hiker_coords_json = json.dumps({
358
+ "lat": start_pt["lat"],
359
+ "lon": start_pt["lon"],
360
+ "ele": start_pt["ele"],
361
+ "cum_dist": start_pt["cum_dist"] / 1000.0
362
+ })
363
+
364
+ return stats_html, map_iframe, checkpoint_table_data, data, 0, gr.update(active=False), "", "", hiker_coords_json
365
 
366
  def handle_ors_fetch_click(start_coords, end_coords, profile, api_key):
367
  try:
 
375
  except Exception as ex:
376
  print(f"[app] Error saving enhanced GPX: {ex}")
377
 
378
+ import json
379
+ start_pt = data["points"][0]
380
+ hiker_coords_json = json.dumps({
381
+ "lat": start_pt["lat"],
382
+ "lon": start_pt["lon"],
383
+ "ele": start_pt["ele"],
384
+ "cum_dist": start_pt["cum_dist"] / 1000.0
385
+ })
386
+ return stats_html, map_iframe, checkpoint_table_data, data, 0, gr.update(active=False), "", "", hiker_coords_json
387
  except Exception as e:
388
  return (
389
  f"<div style='color:#ef4444;'>Error: {e}</div>",
 
393
  0,
394
  gr.update(active=False),
395
  "",
396
+ "",
397
  ""
398
  )
399
 
400
+
401
+
402
+
403
  # --- Playback Simulation Loop ---
404
  def step_simulation(current_idx, route_data, speed):
405
  if not route_data or "points" not in route_data:
406
+ return current_idx, gr.update(), gr.update(), gr.update(), gr.update(), ""
407
 
408
  points = route_data["points"]
409
  checkpoints = route_data["checkpoints"]
410
  pois = route_data.get("pois", [])
411
 
412
  if current_idx >= len(points):
413
+ return current_idx, gr.update(), gr.update(), gr.update(), gr.update(), ""
414
 
415
  step_size = int(speed)
416
  next_idx = current_idx + step_size
 
436
  "drinking_water": "💧",
437
  "spring": "💧",
438
  "water_point": "💧",
439
+ "fountain": "⛲",
440
  "alpine_hut": "🏡",
441
+ "wilderness_hut": "🏡",
442
  "camp_site": "⛺",
443
+ "shelter": "🛡️",
444
+ "viewpoint": "👁️",
445
+ "peak": "🏔️",
446
+ "phone": "📞"
447
  }
448
  icon = icon_map.get(poi["type"], "📍")
449
  active_alerts.append(f"<div style='background:rgba(245,158,11,0.15); border:1px solid #f59e0b; padding:10px; border-radius:8px; margin-bottom:5px; color:#f59e0b;'>{icon} <b>PROXIMITY:</b> {poi['name']} is {d:.0f}m away! ({poi['type'].replace('_', ' ').title()})</div>")
 
463
 
464
  alerts_html = "".join(active_alerts) if active_alerts else "<div style='color:var(--text-muted);'>No active proximity alerts.</div>"
465
 
 
 
 
 
466
  # Live HUD Panel
467
  hud_html = f"""
468
  <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 15px; margin-bottom: 20px;'>
 
503
  """
504
  break
505
 
506
+ import json
507
+ hiker_coords_json = json.dumps({
508
+ "lat": lat,
509
+ "lon": lon,
510
+ "ele": ele,
511
+ "cum_dist": cum_dist / 1000.0
512
+ })
513
+ return next_idx, hud_html, gr.update(), alerts_html, narration_html, hiker_coords_json
514
+
515
 
516
  # --- First-Aid Manual Search ---
517
  def handle_first_aid_search(query):
 
561
  # State management
562
  route_state = gr.State({})
563
  current_point_idx = gr.State(0)
564
+ hiker_pos_coords = gr.Textbox(visible=False, elem_id="hiker-pos-coords")
565
+
566
+ hiker_pos_coords.change(
567
+ fn=None,
568
+ inputs=[hiker_pos_coords],
569
+ outputs=None,
570
+ js="""
571
+ (coords) => {
572
+ if (!coords) return;
573
+ try {
574
+ var data = JSON.parse(coords);
575
+ var container = document.getElementById("trailhead-map-iframe");
576
+ if (container) {
577
+ var iframe = container.querySelector("iframe");
578
+ if (iframe && iframe.contentWindow) {
579
+ iframe.contentWindow.postMessage({
580
+ type: "update_hiker_pos",
581
+ lat: data.lat,
582
+ lon: data.lon,
583
+ ele: data.ele,
584
+ dist: data.cum_dist
585
+ }, "*");
586
+ }
587
+ }
588
+ } catch(e) {
589
+ console.error("Error parsing hiker coords:", e);
590
+ }
591
+ }
592
+ """
593
+ )
594
 
595
  gr.HTML("""
596
  <div style='text-align: center; padding: 10px 0;'>
 
688
  ]
689
  )
690
 
691
+
692
+
693
  # --- Simulation player bindings ---
694
  timer.tick(
695
  fn=step_simulation,
696
  inputs=[current_point_idx, route_state, speed_slider],
697
+ outputs=[current_point_idx, stats_display, map_display, alerts_output, narration_output, hiker_pos_coords]
698
  )
699
 
700
  play_btn.click(
 
712
  def handle_reset(route):
713
  pts = route.get("points", [])
714
  if pts:
 
715
  stats_html, map_iframe, checkpoint_table_data = format_route_view(route)
716
+ import json
717
+ start_pt = pts[0]
718
+ hiker_coords_json = json.dumps({
719
+ "lat": start_pt["lat"],
720
+ "lon": start_pt["lon"],
721
+ "ele": start_pt["ele"],
722
+ "cum_dist": start_pt["cum_dist"] / 1000.0
723
+ })
724
+ return 0, gr.update(active=False), map_iframe, stats_html, "", "", hiker_coords_json
725
+ return 0, gr.update(active=False), gr.update(), gr.update(), "", "", ""
726
 
727
  reset_btn.click(
728
  fn=handle_reset,
729
  inputs=[route_state],
730
+ outputs=[current_point_idx, timer, map_display, stats_display, alerts_output, narration_output, hiker_pos_coords]
731
  )
732
 
733
  # --- Route Ingestion Triggers ---
 
735
  demo.load(
736
  fn=handle_route_update,
737
  inputs=[preloaded_route, upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
738
+ outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output, hiker_pos_coords]
739
  )
740
 
741
  # Preloaded selection change
742
  preloaded_route.change(
743
  fn=handle_route_update,
744
  inputs=[preloaded_route, gr.State(None), gr.State(""), gr.State(""), gr.State(""), gr.State("")],
745
+ outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output, hiker_pos_coords]
746
  )
747
 
748
  # Uploaded file change
749
  upload_file.change(
750
  fn=handle_route_update,
751
  inputs=[gr.State(None), upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
752
+ outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output, hiker_pos_coords]
753
  )
754
 
755
  # Fetch route button click
756
  fetch_route_btn.click(
757
  fn=handle_ors_fetch_click,
758
  inputs=[start_pt, end_pt, ors_profile, ors_api_key],
759
+ outputs=[stats_display, map_display, checkpoint_table, route_state, current_point_idx, timer, alerts_output, narration_output, hiker_pos_coords]
760
  )
761
+
762
 
763
  # --- RAG Trigger ---
764
  rag_search_btn.click(
src/gpx_parser.py CHANGED
@@ -84,18 +84,23 @@ def calculate_elevation_gain_loss(elevations, threshold=2.0):
84
 
85
  def fetch_overpass_pois(min_lat, min_lon, max_lat, max_lon):
86
  """
87
- Fetch POIs (water, spring, huts, camps, shelter) from Overpass API in the bounding box.
88
  """
89
  url = "https://overpass-api.de/api/interpreter"
90
  query = f"""
91
- [out:json][timeout:20];
92
  (
93
  node["amenity"="drinking_water"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
94
  node["natural"="spring"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
95
  node["amenity"="water_point"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
 
96
  node["tourism"="alpine_hut"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
 
97
  node["tourism"="camp_site"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
98
  node["amenity"="shelter"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
 
 
 
99
  );
100
  out body;
101
  """
@@ -104,7 +109,7 @@ def fetch_overpass_pois(min_lat, min_lon, max_lat, max_lon):
104
  }
105
  try:
106
  print(f"[gpx_parser] Querying Overpass API for POIs in bbox: [{min_lat:.5f}, {min_lon:.5f}, {max_lat:.5f}, {max_lon:.5f}]...")
107
- response = requests.post(url, data={'data': query}, headers=headers, timeout=15)
108
  if response.status_code == 200:
109
  data = response.json()
110
  elements = data.get("elements", [])
 
84
 
85
  def fetch_overpass_pois(min_lat, min_lon, max_lat, max_lon):
86
  """
87
+ Fetch POIs (water, spring, huts, camps, shelter, viewpoint, peak, phone) from Overpass API in the bounding box.
88
  """
89
  url = "https://overpass-api.de/api/interpreter"
90
  query = f"""
91
+ [out:json][timeout:25];
92
  (
93
  node["amenity"="drinking_water"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
94
  node["natural"="spring"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
95
  node["amenity"="water_point"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
96
+ node["amenity"="fountain"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
97
  node["tourism"="alpine_hut"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
98
+ node["tourism"="wilderness_hut"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
99
  node["tourism"="camp_site"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
100
  node["amenity"="shelter"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
101
+ node["tourism"="viewpoint"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
102
+ node["natural"="peak"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
103
+ node["amenity"="phone"]({min_lat:.5f},{min_lon:.5f},{max_lat:.5f},{max_lon:.5f});
104
  );
105
  out body;
106
  """
 
109
  }
110
  try:
111
  print(f"[gpx_parser] Querying Overpass API for POIs in bbox: [{min_lat:.5f}, {min_lon:.5f}, {max_lat:.5f}, {max_lon:.5f}]...")
112
+ response = requests.get(url, params={'data': query}, headers=headers, timeout=25)
113
  if response.status_code == 200:
114
  data = response.json()
115
  elements = data.get("elements", [])