Spaces:
Build error
Build error
feat: Phase 2 - Functional simulation, OSM POI Overpass integration, and first-aid RAG
Browse files- MR_DESCRIPTION.md +24 -28
- app.py +339 -63
- src/data/first_aid_guide.json +22 -0
- src/gpx_parser.py +199 -8
- src/rag.py +81 -0
- test_overpass.py +48 -0
MR_DESCRIPTION.md
CHANGED
|
@@ -1,39 +1,35 @@
|
|
| 1 |
-
# Merge Request / Pull Request Description:
|
| 2 |
|
| 3 |
## 📝 Overview
|
| 4 |
-
This Merge Request
|
| 5 |
|
| 6 |
## 🚀 Key Changes
|
| 7 |
-
1. **
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
3. **
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
|
| 20 |
## 🛠️ Verification Done
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
-
|
| 24 |
|
| 25 |
---
|
| 26 |
|
| 27 |
## 📋 Steps to Push to GitHub
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
git remote add origin <your-github-repo-url>
|
| 36 |
|
| 37 |
-
# Push the main branch to GitHub
|
| 38 |
-
git push -u origin main
|
| 39 |
-
```
|
|
|
|
| 1 |
+
# Merge Request / Pull Request Description: Phase 2 - Functional + POI Integration
|
| 2 |
|
| 3 |
## 📝 Overview
|
| 4 |
+
This Merge Request delivers **Phase 2: Functional + POI Integration** for the **Trailhead** project. It introduces live and offline Points of Interest (POI) extraction via OpenStreetMap (Overpass API), integrates a local Wilderness First-Aid medical manual RAG search, and implements a full route simulation player with metrics HUD and proximity alert indicators.
|
| 5 |
|
| 6 |
## 🚀 Key Changes
|
| 7 |
+
1. **POI Integration (`gpx_parser.py`)**:
|
| 8 |
+
* Fetches key POIs (drinking water, spring, huts, shelters, campsites) dynamically within a custom bounding box of the track via the **OSM Overpass API**.
|
| 9 |
+
* Filters amenities locally using the **Haversine formula** within a 150m buffer of the route.
|
| 10 |
+
* Serializes/saves POIs into GPX XML `<extensions>` and `<wpt>` tags via `save_enhanced_gpx` for offline capabilities.
|
| 11 |
+
2. **Wilderness First-Aid Guide (`first_aid_guide.json` & `rag.py`)**:
|
| 12 |
+
* Created a wilderness medical corpus covering 5 key sections (bleeding, hypothermia, heat, altitude sickness, musculoskeletal).
|
| 13 |
+
* Implemented keyword intersection matching in `rag.py` to ground first-aid queries, returning relevant guide text along with section citations.
|
| 14 |
+
3. **Simulation HUD & Playback UI (`app.py`)**:
|
| 15 |
+
* Added simulation controls (Play, Pause, Speed slider, Reset).
|
| 16 |
+
* Displays dashboard metrics (Route progress %, distance walked, altitude, and dynamic ETA to next checkpoint).
|
| 17 |
+
* Sounds/shows offline proximity alerts when within 150m of any drinking water, camp, or hut.
|
| 18 |
+
* Integrated Wilderness First-Aid tab with static emergency cards and RAG manual search.
|
| 19 |
|
| 20 |
## 🛠️ Verification Done
|
| 21 |
+
* Created [test_overpass.py](file:///c:/Users/skushwaha/Documents/hckthn/TrailHead/test_overpass.py) to parse the preloaded Trento Track.
|
| 22 |
+
* Verified Overpass API (`https://overpass-api.de/api/interpreter`) fetched, filtered, and returned 2 drinking water amenities successfully within a 150m buffer.
|
| 23 |
+
* Validated that the first-aid RAG keyword search retrieves correct sections and references them.
|
| 24 |
|
| 25 |
---
|
| 26 |
|
| 27 |
## 📋 Steps to Push to GitHub
|
| 28 |
+
```bash
|
| 29 |
+
# Add the remote repository (if not already linked)
|
| 30 |
+
git remote add origin <your-github-repo-url>
|
| 31 |
+
|
| 32 |
+
# Push all files to main
|
| 33 |
+
git push -u origin main
|
| 34 |
+
```
|
|
|
|
| 35 |
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -4,8 +4,9 @@ import requests
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
import folium
|
| 7 |
-
from src.gpx_parser import parse_gpx_file
|
| 8 |
import src.llm as llm
|
|
|
|
| 9 |
|
| 10 |
# Initialize cache and temp folders
|
| 11 |
os.makedirs("./temp", exist_ok=True)
|
|
@@ -13,21 +14,37 @@ os.makedirs("./temp", exist_ok=True)
|
|
| 13 |
# Preloaded route path
|
| 14 |
PRELOADED_ROUTE_PATH = r"C:\Users\skushwaha\Documents\hckthn\TrailHead\Routes\track_5-14724236830.gpx"
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"""
|
| 18 |
-
Generate interactive folium map.
|
| 19 |
"""
|
| 20 |
if not points:
|
| 21 |
-
# Default centered map
|
| 22 |
m = folium.Map(location=[46.0734974, 11.1717214], zoom_start=13)
|
| 23 |
return m._repr_html_()
|
| 24 |
|
| 25 |
-
# Center map on
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# Draw track polyline
|
| 33 |
locations = [(p["lat"], p["lon"]) for p in points]
|
|
@@ -41,7 +58,6 @@ def generate_folium_map(points, checkpoints):
|
|
| 41 |
ele = cp["ele"]
|
| 42 |
dist = cp["cum_dist"]
|
| 43 |
|
| 44 |
-
# Color code markers
|
| 45 |
if name == "Start":
|
| 46 |
color = "green"
|
| 47 |
icon = "play"
|
|
@@ -59,7 +75,6 @@ def generate_folium_map(points, checkpoints):
|
|
| 59 |
Elevation: {ele:.1f} m
|
| 60 |
</div>
|
| 61 |
"""
|
| 62 |
-
|
| 63 |
folium.Marker(
|
| 64 |
location=[lat, lon],
|
| 65 |
popup=popup_text,
|
|
@@ -67,6 +82,45 @@ def generate_folium_map(points, checkpoints):
|
|
| 67 |
icon=folium.Icon(color=color, icon=icon)
|
| 68 |
).add_to(m)
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return m._repr_html_()
|
| 71 |
|
| 72 |
def get_map_iframe(map_html):
|
|
@@ -93,7 +147,6 @@ def fetch_ors_route(start_coords, end_coords, profile, api_key):
|
|
| 93 |
file_path = os.path.join(temp_dir, "ors_fetched_route.gpx")
|
| 94 |
|
| 95 |
if not api_key:
|
| 96 |
-
# Create a mock straight-line GPX (3 coordinates: start, mid, end) for demo purposes
|
| 97 |
mid_lat = (start_lat + end_lat) / 2.0
|
| 98 |
mid_lon = (start_lon + end_lon) / 2.0
|
| 99 |
gpx_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -131,7 +184,6 @@ def fetch_ors_route(start_coords, end_coords, profile, api_key):
|
|
| 131 |
else:
|
| 132 |
raise ValueError(f"ORS returned status {response.status_code}")
|
| 133 |
except Exception as e:
|
| 134 |
-
# Fallback straight-line
|
| 135 |
mid_lat = (start_lat + end_lat) / 2.0
|
| 136 |
mid_lon = (start_lon + end_lon) / 2.0
|
| 137 |
gpx_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -149,30 +201,10 @@ def fetch_ors_route(start_coords, end_coords, profile, api_key):
|
|
| 149 |
gr.Warning(f"ORS Fetch failed ({e}). Generated straight-line fallback route.")
|
| 150 |
return file_path
|
| 151 |
|
| 152 |
-
def
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
# Check trigger source
|
| 157 |
-
# We can inspect input priority or simply prioritize upload -> fetch -> preloaded
|
| 158 |
-
if uploaded_file is not None:
|
| 159 |
-
file_path = uploaded_file.name
|
| 160 |
-
elif start_coords and end_coords:
|
| 161 |
-
# If coordinates are changed and user hits the trigger, we can fetch
|
| 162 |
-
# However, to avoid automatic fetching on load, we only fetch when this is called via button click.
|
| 163 |
-
# Since this function handles all triggers, we'll let app buttons set a temporary flag.
|
| 164 |
-
pass
|
| 165 |
-
|
| 166 |
-
try:
|
| 167 |
-
data = parse_gpx_file(file_path)
|
| 168 |
-
except Exception as e:
|
| 169 |
-
return (
|
| 170 |
-
f"<div style='color:#ef4444; padding:15px; border:1px solid #ef4444; border-radius:8px;'>Error loading GPX: {e}</div>",
|
| 171 |
-
f"<iframe srcdoc='<h3 style=\"color:red;\">Error rendering map: {e}</h3>' width='100%' height='520px'></iframe>",
|
| 172 |
-
[]
|
| 173 |
-
)
|
| 174 |
-
|
| 175 |
-
# Generate Stats HUD
|
| 176 |
stats_html = f"""
|
| 177 |
<div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 15px; margin-bottom: 20px;'>
|
| 178 |
<div class='hud-stat-box'>
|
|
@@ -198,11 +230,9 @@ def handle_route_update(preloaded_sel, uploaded_file, start_coords, end_coords,
|
|
| 198 |
</div>
|
| 199 |
"""
|
| 200 |
|
| 201 |
-
|
| 202 |
-
map_html = generate_folium_map(data["points"], data["checkpoints"])
|
| 203 |
map_iframe = get_map_iframe(map_html)
|
| 204 |
|
| 205 |
-
# Format Checkpoint List for Dataframe
|
| 206 |
checkpoint_table_data = []
|
| 207 |
for cp in data["checkpoints"]:
|
| 208 |
checkpoint_table_data.append([
|
|
@@ -214,33 +244,215 @@ def handle_route_update(preloaded_sel, uploaded_file, start_coords, end_coords,
|
|
| 214 |
|
| 215 |
return stats_html, map_iframe, checkpoint_table_data
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def handle_ors_fetch_click(start_coords, end_coords, profile, api_key):
|
| 218 |
-
"""Button click handler for fetching online routes."""
|
| 219 |
try:
|
| 220 |
route_file = fetch_ors_route(start_coords, end_coords, profile, api_key)
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
except Exception as e:
|
| 223 |
return (
|
| 224 |
-
f"<div style='color:#ef4444;
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
)
|
| 228 |
|
| 229 |
-
# ---
|
| 230 |
-
def
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
system_prompt = (
|
| 234 |
-
"You are Trailhead Guide,
|
| 235 |
-
"
|
| 236 |
-
"
|
| 237 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
for token in llm.generate(message, system=system_prompt, history=history, stream=True):
|
| 239 |
response_accumulator += token
|
| 240 |
yield response_accumulator
|
| 241 |
|
| 242 |
# --- Gradio Blocks UI ---
|
| 243 |
with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Computer") as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
gr.HTML("""
|
| 245 |
<div style='text-align: center; padding: 10px 0;'>
|
| 246 |
<h1>🌲 Trailhead 🌲</h1>
|
|
@@ -250,6 +462,9 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 250 |
</div>
|
| 251 |
""")
|
| 252 |
|
|
|
|
|
|
|
|
|
|
| 253 |
with gr.Tabs():
|
| 254 |
with gr.TabItem("🧭 Trek Planner & HUD"):
|
| 255 |
with gr.Row():
|
|
@@ -259,7 +474,7 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 259 |
preloaded_route = gr.Dropdown(
|
| 260 |
choices=["Preloaded Route: Trento Track"],
|
| 261 |
value="Preloaded Route: Trento Track",
|
| 262 |
-
label="Preloaded Routes
|
| 263 |
)
|
| 264 |
|
| 265 |
upload_file = gr.File(
|
|
@@ -268,7 +483,6 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 268 |
)
|
| 269 |
|
| 270 |
with gr.Accordion("🔌 Fetch Online Route (Basecamp Mode)", open=False):
|
| 271 |
-
gr.Markdown("Generate route paths between waypoints using OpenRouteService.")
|
| 272 |
start_pt = gr.Textbox(
|
| 273 |
value="46.0734974, 11.1717214",
|
| 274 |
label="Start Coordinates (Lat, Lon)"
|
|
@@ -278,17 +492,26 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 278 |
label="End Coordinates (Lat, Lon)"
|
| 279 |
)
|
| 280 |
ors_profile = gr.Dropdown(
|
| 281 |
-
choices=["foot-hiking", "foot-walking"
|
| 282 |
value="foot-hiking",
|
| 283 |
label="Profile"
|
| 284 |
)
|
| 285 |
ors_api_key = gr.Textbox(
|
| 286 |
type="password",
|
| 287 |
-
label="OpenRouteService API Key (Optional)"
|
| 288 |
-
placeholder="Paste your API key here..."
|
| 289 |
)
|
| 290 |
fetch_route_btn = gr.Button("Fetch & Load Route", variant="secondary")
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
with gr.Column(scale=2):
|
| 293 |
# Stats display
|
| 294 |
stats_display = gr.HTML()
|
|
@@ -296,6 +519,9 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 296 |
# Interactive Map display
|
| 297 |
map_display = gr.HTML()
|
| 298 |
|
|
|
|
|
|
|
|
|
|
| 299 |
with gr.Accordion("📋 Route Checkpoint Briefing", open=True):
|
| 300 |
checkpoint_table = gr.DataFrame(
|
| 301 |
headers=["Checkpoint", "Coordinates", "Cumulative Distance", "Altitude"],
|
|
@@ -303,6 +529,16 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 303 |
column_count=(4, "fixed")
|
| 304 |
)
|
| 305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
with gr.TabItem("💬 Wilderness Guide AI"):
|
| 307 |
gr.ChatInterface(
|
| 308 |
respond,
|
|
@@ -313,33 +549,73 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Comp
|
|
| 313 |
]
|
| 314 |
)
|
| 315 |
|
| 316 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
# Load default route on startup
|
| 318 |
demo.load(
|
| 319 |
fn=handle_route_update,
|
| 320 |
inputs=[preloaded_route, upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
|
| 321 |
-
outputs=[stats_display, map_display, checkpoint_table]
|
| 322 |
)
|
| 323 |
|
| 324 |
# Preloaded selection change
|
| 325 |
preloaded_route.change(
|
| 326 |
fn=handle_route_update,
|
| 327 |
inputs=[preloaded_route, gr.State(None), gr.State(""), gr.State(""), gr.State(""), gr.State("")],
|
| 328 |
-
outputs=[stats_display, map_display, checkpoint_table]
|
| 329 |
)
|
| 330 |
|
| 331 |
# Uploaded file change
|
| 332 |
upload_file.change(
|
| 333 |
fn=handle_route_update,
|
| 334 |
inputs=[gr.State(None), upload_file, gr.State(""), gr.State(""), gr.State(""), gr.State("")],
|
| 335 |
-
outputs=[stats_display, map_display, checkpoint_table]
|
| 336 |
)
|
| 337 |
|
| 338 |
# Fetch route button click
|
| 339 |
fetch_route_btn.click(
|
| 340 |
fn=handle_ors_fetch_click,
|
| 341 |
inputs=[start_pt, end_pt, ors_profile, ors_api_key],
|
| 342 |
-
outputs=[stats_display, map_display, checkpoint_table]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
)
|
| 344 |
|
| 345 |
if __name__ == "__main__":
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
import folium
|
| 7 |
+
from src.gpx_parser import parse_gpx_file, haversine, save_enhanced_gpx
|
| 8 |
import src.llm as llm
|
| 9 |
+
import src.rag as rag
|
| 10 |
|
| 11 |
# Initialize cache and temp folders
|
| 12 |
os.makedirs("./temp", exist_ok=True)
|
|
|
|
| 14 |
# Preloaded route path
|
| 15 |
PRELOADED_ROUTE_PATH = r"C:\Users\skushwaha\Documents\hckthn\TrailHead\Routes\track_5-14724236830.gpx"
|
| 16 |
|
| 17 |
+
EMERGENCY_CARD = """
|
| 18 |
+
## 🚨 IMMEDIATE BACKCOUNTRY EMERGENCY CARD (OFFLINE)
|
| 19 |
+
If you encounter a medical crisis with no cellular signal, follow these basic steps:
|
| 20 |
+
|
| 21 |
+
1. **Severe Bleeding:** Apply direct pressure with clean dressing. Elevate limb. Use tourniquet if blood is spurting.
|
| 22 |
+
2. **Hypothermia:** Wrap in windproof shell/sleeping bag. Replace wet clothes. Provide warm sweet drinks.
|
| 23 |
+
3. **Heat Stroke:** Move to shade. Actively cool by wetting skin and fanning. Sip cool water.
|
| 24 |
+
4. **Altitude Illness (AMS/HAPE/HACE):** Descend immediately. Do not ascend. Administer oxygen if available.
|
| 25 |
+
5. **Ankle Sprain (R.I.C.E):** Rest the joint. Ice or apply cold pack. Compress with elastic bandage. Elevate limb.
|
| 26 |
+
|
| 27 |
+
*Disclaimer: This guide is for offline reference only. Always carry a PLB/satellite communicator on remote trails.*
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def generate_folium_map(points, checkpoints, pois, hiker_pos=None):
|
| 31 |
"""
|
| 32 |
+
Generate interactive folium map rendering track, checkpoints, POIs, and current hiker pos.
|
| 33 |
"""
|
| 34 |
if not points:
|
|
|
|
| 35 |
m = folium.Map(location=[46.0734974, 11.1717214], zoom_start=13)
|
| 36 |
return m._repr_html_()
|
| 37 |
|
| 38 |
+
# Center map on current hiker position or middle of track
|
| 39 |
+
if hiker_pos:
|
| 40 |
+
center_lat, center_lon = hiker_pos["lat"], hiker_pos["lon"]
|
| 41 |
+
zoom_val = 15
|
| 42 |
+
else:
|
| 43 |
+
mid_idx = len(points) // 2
|
| 44 |
+
center_lat, center_lon = points[mid_idx]["lat"], points[mid_idx]["lon"]
|
| 45 |
+
zoom_val = 14
|
| 46 |
+
|
| 47 |
+
m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom_val)
|
| 48 |
|
| 49 |
# Draw track polyline
|
| 50 |
locations = [(p["lat"], p["lon"]) for p in points]
|
|
|
|
| 58 |
ele = cp["ele"]
|
| 59 |
dist = cp["cum_dist"]
|
| 60 |
|
|
|
|
| 61 |
if name == "Start":
|
| 62 |
color = "green"
|
| 63 |
icon = "play"
|
|
|
|
| 75 |
Elevation: {ele:.1f} m
|
| 76 |
</div>
|
| 77 |
"""
|
|
|
|
| 78 |
folium.Marker(
|
| 79 |
location=[lat, lon],
|
| 80 |
popup=popup_text,
|
|
|
|
| 82 |
icon=folium.Icon(color=color, icon=icon)
|
| 83 |
).add_to(m)
|
| 84 |
|
| 85 |
+
# Draw POIs
|
| 86 |
+
for poi in pois:
|
| 87 |
+
poi_type = poi["type"]
|
| 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;">
|
| 103 |
+
<b>{poi['name']}</b><br>
|
| 104 |
+
Type: {poi_type.replace('_', ' ').title()}<br>
|
| 105 |
+
Distance to Route: {poi['distance']:.1f} m
|
| 106 |
+
</div>
|
| 107 |
+
"""
|
| 108 |
+
folium.Marker(
|
| 109 |
+
location=[poi["lat"], poi["lon"]],
|
| 110 |
+
popup=popup_text,
|
| 111 |
+
tooltip=poi['name'],
|
| 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):
|
|
|
|
| 147 |
file_path = os.path.join(temp_dir, "ors_fetched_route.gpx")
|
| 148 |
|
| 149 |
if not api_key:
|
|
|
|
| 150 |
mid_lat = (start_lat + end_lat) / 2.0
|
| 151 |
mid_lon = (start_lon + end_lon) / 2.0
|
| 152 |
gpx_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
| 184 |
else:
|
| 185 |
raise ValueError(f"ORS returned status {response.status_code}")
|
| 186 |
except Exception as e:
|
|
|
|
| 187 |
mid_lat = (start_lat + end_lat) / 2.0
|
| 188 |
mid_lon = (start_lon + end_lon) / 2.0
|
| 189 |
gpx_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
| 201 |
gr.Warning(f"ORS Fetch failed ({e}). Generated straight-line fallback route.")
|
| 202 |
return file_path
|
| 203 |
|
| 204 |
+
def format_route_view(data):
|
| 205 |
+
"""
|
| 206 |
+
Format route data for presentation inside stats display and checkpoint lists.
|
| 207 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
stats_html = f"""
|
| 209 |
<div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 15px; margin-bottom: 20px;'>
|
| 210 |
<div class='hud-stat-box'>
|
|
|
|
| 230 |
</div>
|
| 231 |
"""
|
| 232 |
|
| 233 |
+
map_html = generate_folium_map(data["points"], data["checkpoints"], data.get("pois", []))
|
|
|
|
| 234 |
map_iframe = get_map_iframe(map_html)
|
| 235 |
|
|
|
|
| 236 |
checkpoint_table_data = []
|
| 237 |
for cp in data["checkpoints"]:
|
| 238 |
checkpoint_table_data.append([
|
|
|
|
| 244 |
|
| 245 |
return stats_html, map_iframe, checkpoint_table_data
|
| 246 |
|
| 247 |
+
def handle_route_update(preloaded_sel, uploaded_file, start_coords, end_coords, profile, api_key):
|
| 248 |
+
file_path = PRELOADED_ROUTE_PATH
|
| 249 |
+
if uploaded_file is not None:
|
| 250 |
+
file_path = uploaded_file.name
|
| 251 |
+
|
| 252 |
+
try:
|
| 253 |
+
data = parse_gpx_file(file_path)
|
| 254 |
+
except Exception as e:
|
| 255 |
+
import traceback
|
| 256 |
+
traceback.print_exc()
|
| 257 |
+
return (
|
| 258 |
+
f"<div style='color:#ef4444;'>Error parsing GPX: {e}</div>",
|
| 259 |
+
"",
|
| 260 |
+
[],
|
| 261 |
+
{},
|
| 262 |
+
0,
|
| 263 |
+
gr.update(active=False),
|
| 264 |
+
"",
|
| 265 |
+
""
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
stats_html, map_iframe, checkpoint_table_data = format_route_view(data)
|
| 269 |
+
# Save a copy with POIs saved to disk
|
| 270 |
+
try:
|
| 271 |
+
enhanced_file = os.path.join("./temp", "trek_with_poi.gpx")
|
| 272 |
+
save_enhanced_gpx(file_path, enhanced_file, data.get("pois", []))
|
| 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:
|
| 280 |
route_file = fetch_ors_route(start_coords, end_coords, profile, api_key)
|
| 281 |
+
data = parse_gpx_file(route_file)
|
| 282 |
+
stats_html, map_iframe, checkpoint_table_data = format_route_view(data)
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
enhanced_file = os.path.join("./temp", "trek_with_poi.gpx")
|
| 286 |
+
save_enhanced_gpx(route_file, enhanced_file, data.get("pois", []))
|
| 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>",
|
| 294 |
+
"",
|
| 295 |
+
[],
|
| 296 |
+
{},
|
| 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
|
| 317 |
+
if next_idx >= len(points):
|
| 318 |
+
next_idx = len(points) - 1
|
| 319 |
+
|
| 320 |
+
current_pt = points[next_idx]
|
| 321 |
+
|
| 322 |
+
lat = current_pt["lat"]
|
| 323 |
+
lon = current_pt["lon"]
|
| 324 |
+
ele = current_pt["ele"]
|
| 325 |
+
cum_dist = current_pt["cum_dist"]
|
| 326 |
+
|
| 327 |
+
total_dist = points[-1]["cum_dist"]
|
| 328 |
+
pct_complete = (cum_dist / total_dist) * 100.0 if total_dist > 0 else 0.0
|
| 329 |
+
|
| 330 |
+
# Proximity alerts check
|
| 331 |
+
active_alerts = []
|
| 332 |
+
for poi in pois:
|
| 333 |
+
d = haversine(lat, lon, poi["lat"], poi["lon"])
|
| 334 |
+
if d <= 150.0:
|
| 335 |
+
icon_map = {
|
| 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>")
|
| 345 |
+
|
| 346 |
+
# Checkpoint ETA progress
|
| 347 |
+
next_cp = None
|
| 348 |
+
for cp in checkpoints:
|
| 349 |
+
if cp["cum_dist"] * 1000.0 > cum_dist:
|
| 350 |
+
next_cp = cp
|
| 351 |
+
break
|
| 352 |
+
|
| 353 |
+
eta_text = "N/A"
|
| 354 |
+
if next_cp:
|
| 355 |
+
dist_to_cp = (next_cp["cum_dist"] * 1000.0) - cum_dist
|
| 356 |
+
eta_sec = dist_to_cp / 1.38
|
| 357 |
+
eta_text = f"{int(eta_sec // 60)}m {int(eta_sec % 60)}s"
|
| 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;'>
|
| 368 |
+
<div class='hud-stat-box'>
|
| 369 |
+
<div class='hud-stat-val mono-display'>{pct_complete:.1f}%</div>
|
| 370 |
+
<div class='hud-stat-lbl'>Route Progress</div>
|
| 371 |
+
</div>
|
| 372 |
+
<div class='hud-stat-box'>
|
| 373 |
+
<div class='hud-stat-val mono-display'>{cum_dist/1000.0:.2f} km</div>
|
| 374 |
+
<div class='hud-stat-lbl'>Distance Hiked</div>
|
| 375 |
+
</div>
|
| 376 |
+
<div class='hud-stat-box'>
|
| 377 |
+
<div class='hud-stat-val mono-display'>{ele:.1f} m</div>
|
| 378 |
+
<div class='hud-stat-lbl'>Current Altitude</div>
|
| 379 |
+
</div>
|
| 380 |
+
<div class='hud-stat-box'>
|
| 381 |
+
<div class='hud-stat-val mono-display'>{eta_text}</div>
|
| 382 |
+
<div class='hud-stat-lbl'>ETA to Next Point</div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
"""
|
| 386 |
+
|
| 387 |
+
# Proximity narration brief
|
| 388 |
+
narration_html = ""
|
| 389 |
+
for cp in checkpoints:
|
| 390 |
+
cp_dist_m = cp["cum_dist"] * 1000.0
|
| 391 |
+
if abs(cum_dist - cp_dist_m) <= 150.0:
|
| 392 |
+
cautions = ""
|
| 393 |
+
if ele > 2400:
|
| 394 |
+
cautions = " WARNING: Altitude is above 2400m. Watch for AMS symptoms (headache, dizziness)."
|
| 395 |
+
narration_html = f"""
|
| 396 |
+
<div style='border-left: 4px solid var(--accent-primary); background: rgba(245,158,11,0.05); padding: 15px; border-radius: 0 8px 8px 0;'>
|
| 397 |
+
<b style='color:var(--accent-primary);'>📻 RADIO BRIEFING FOR {cp['name'].upper()}:</b>
|
| 398 |
+
<p style='margin-top: 5px; font-style: italic;'>
|
| 399 |
+
"Hiker, you have arrived at {cp['name']}. Current altitude is {ele:.1f}m.{cautions}"
|
| 400 |
+
</p>
|
| 401 |
+
</div>
|
| 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):
|
| 409 |
+
if not query.strip():
|
| 410 |
+
return "Please enter symptoms or injury to search the manual."
|
| 411 |
+
|
| 412 |
+
grounding_text, sources = rag.retrieve_first_aid(query)
|
| 413 |
+
if not grounding_text:
|
| 414 |
+
return "No matching first-aid sections found in the manual. (Please carry a PLB/satellite communicator on remote trails)."
|
| 415 |
+
|
| 416 |
system_prompt = (
|
| 417 |
+
"You are Trailhead Guide, an expert wilderness medicine counselor.\n"
|
| 418 |
+
"Provide a concise, direct, and actionable step-by-step first-aid protocol based on the provided guide context.\n"
|
| 419 |
+
"Cite the section at the end."
|
| 420 |
)
|
| 421 |
+
prompt = f"Context:\n{grounding_text}\n\nQuestion: {query}\n\nAnswer:"
|
| 422 |
+
response = llm.generate(prompt, system=system_prompt, stream=False)
|
| 423 |
+
|
| 424 |
+
return f"### Retrieval Results ({', '.join(sources)})\n\n{response}"
|
| 425 |
+
|
| 426 |
+
# --- Chatbot Integration ---
|
| 427 |
+
def respond(message, history):
|
| 428 |
+
response_accumulator = ""
|
| 429 |
+
grounding_text, sources = rag.retrieve_first_aid(message)
|
| 430 |
+
|
| 431 |
+
if grounding_text:
|
| 432 |
+
source_cite = "Sources: " + ", ".join(sources)
|
| 433 |
+
system_prompt = (
|
| 434 |
+
"You are Trailhead Guide, a wilderness first-aid advisor.\n"
|
| 435 |
+
"Answer the query using ONLY the provided guide context.\n"
|
| 436 |
+
f"Context:\n{grounding_text}\n"
|
| 437 |
+
"Keep the instructions clear, numbered, and precise.\n"
|
| 438 |
+
f"Cite: '{source_cite}'."
|
| 439 |
+
)
|
| 440 |
+
else:
|
| 441 |
+
system_prompt = (
|
| 442 |
+
"You are Trailhead Guide, an expert hiking guide.\n"
|
| 443 |
+
"Provide helpful, concise trekking advice."
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
for token in llm.generate(message, system=system_prompt, history=history, stream=True):
|
| 447 |
response_accumulator += token
|
| 448 |
yield response_accumulator
|
| 449 |
|
| 450 |
# --- Gradio Blocks UI ---
|
| 451 |
with gr.Blocks(css="assets/custom.css", title="Trailhead — Tactical Trail Computer") as demo:
|
| 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;'>
|
| 458 |
<h1>🌲 Trailhead 🌲</h1>
|
|
|
|
| 462 |
</div>
|
| 463 |
""")
|
| 464 |
|
| 465 |
+
# Timer loop for simulation
|
| 466 |
+
timer = gr.Timer(value=0.2, active=False)
|
| 467 |
+
|
| 468 |
with gr.Tabs():
|
| 469 |
with gr.TabItem("🧭 Trek Planner & HUD"):
|
| 470 |
with gr.Row():
|
|
|
|
| 474 |
preloaded_route = gr.Dropdown(
|
| 475 |
choices=["Preloaded Route: Trento Track"],
|
| 476 |
value="Preloaded Route: Trento Track",
|
| 477 |
+
label="Preloaded Routes"
|
| 478 |
)
|
| 479 |
|
| 480 |
upload_file = gr.File(
|
|
|
|
| 483 |
)
|
| 484 |
|
| 485 |
with gr.Accordion("🔌 Fetch Online Route (Basecamp Mode)", open=False):
|
|
|
|
| 486 |
start_pt = gr.Textbox(
|
| 487 |
value="46.0734974, 11.1717214",
|
| 488 |
label="Start Coordinates (Lat, Lon)"
|
|
|
|
| 492 |
label="End Coordinates (Lat, Lon)"
|
| 493 |
)
|
| 494 |
ors_profile = gr.Dropdown(
|
| 495 |
+
choices=["foot-hiking", "foot-walking"],
|
| 496 |
value="foot-hiking",
|
| 497 |
label="Profile"
|
| 498 |
)
|
| 499 |
ors_api_key = gr.Textbox(
|
| 500 |
type="password",
|
| 501 |
+
label="OpenRouteService API Key (Optional)"
|
|
|
|
| 502 |
)
|
| 503 |
fetch_route_btn = gr.Button("Fetch & Load Route", variant="secondary")
|
| 504 |
|
| 505 |
+
gr.Markdown("### 🎮 Trek Simulation Controls")
|
| 506 |
+
with gr.Row():
|
| 507 |
+
play_btn = gr.Button("▶ PLAY", variant="primary")
|
| 508 |
+
pause_btn = gr.Button("⏸ PAUSE", variant="secondary")
|
| 509 |
+
reset_btn = gr.Button("🔄 RESET", variant="secondary")
|
| 510 |
+
speed_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Simulation Speed (Points per tick)")
|
| 511 |
+
|
| 512 |
+
gr.Markdown("### ⚠️ Active Proximity Alerts")
|
| 513 |
+
alerts_output = gr.HTML(value="<div style='color:var(--text-muted);'>No active proximity alerts.</div>")
|
| 514 |
+
|
| 515 |
with gr.Column(scale=2):
|
| 516 |
# Stats display
|
| 517 |
stats_display = gr.HTML()
|
|
|
|
| 519 |
# Interactive Map display
|
| 520 |
map_display = gr.HTML()
|
| 521 |
|
| 522 |
+
# Narration briefing output
|
| 523 |
+
narration_output = gr.HTML(value="")
|
| 524 |
+
|
| 525 |
with gr.Accordion("📋 Route Checkpoint Briefing", open=True):
|
| 526 |
checkpoint_table = gr.DataFrame(
|
| 527 |
headers=["Checkpoint", "Coordinates", "Cumulative Distance", "Altitude"],
|
|
|
|
| 529 |
column_count=(4, "fixed")
|
| 530 |
)
|
| 531 |
|
| 532 |
+
with gr.TabItem("🩺 Wilderness First-Aid"):
|
| 533 |
+
with gr.Row():
|
| 534 |
+
with gr.Column(scale=1):
|
| 535 |
+
gr.HTML(EMERGENCY_CARD)
|
| 536 |
+
with gr.Column(scale=1):
|
| 537 |
+
gr.Markdown("## 🔍 Wilderness First-Aid manual RAG Search")
|
| 538 |
+
rag_query = gr.Textbox(placeholder="What symptoms or injury do you want to query?", label="Query Symptoms")
|
| 539 |
+
rag_search_btn = gr.Button("Search manual", variant="primary")
|
| 540 |
+
rag_output = gr.Markdown(value="*Manual results will be displayed here.*")
|
| 541 |
+
|
| 542 |
with gr.TabItem("💬 Wilderness Guide AI"):
|
| 543 |
gr.ChatInterface(
|
| 544 |
respond,
|
|
|
|
| 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(
|
| 560 |
+
fn=lambda: gr.update(active=True),
|
| 561 |
+
inputs=[],
|
| 562 |
+
outputs=[timer]
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
pause_btn.click(
|
| 566 |
+
fn=lambda: gr.update(active=False),
|
| 567 |
+
inputs=[],
|
| 568 |
+
outputs=[timer]
|
| 569 |
+
)
|
| 570 |
+
|
| 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 ---
|
| 586 |
# Load default route on startup
|
| 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(
|
| 616 |
+
fn=handle_first_aid_search,
|
| 617 |
+
inputs=[rag_query],
|
| 618 |
+
outputs=[rag_output]
|
| 619 |
)
|
| 620 |
|
| 621 |
if __name__ == "__main__":
|
src/data/first_aid_guide.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"section": "Section 1: Bleeding and Wound Management",
|
| 4 |
+
"text": "Control severe bleeding by applying direct pressure to the wound with a clean cloth or bandage. Keep the limb elevated. If bleeding does not stop after 10-15 minutes of continuous direct pressure, apply a tourniquet high and tight on the limb. Keep the wound clean using treated water. Cover with sterile dressings. Avoid direct contact with bodily fluids."
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"section": "Section 2: Hypothermia and Frostbite",
|
| 8 |
+
"text": "Hypothermia occurs when the core body temperature drops below 35°C (95°F). Symptoms include shivering, confusion, and slurred speech. Treatment: Move the victim out of the wind/wet, replace wet clothing with dry layers, wrap in a space blanket or sleeping bag, and provide warm sweetened drinks if conscious. Frostbite: Do not rub the affected skin. Rewarm slowly in warm water (38-42°C)."
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"section": "Section 3: Heat Exhaustion and Dehydration",
|
| 12 |
+
"text": "Heat exhaustion presents with heavy sweating, rapid pulse, dizziness, nausea, and headache. Dehydration worsens these symptoms. Treatment: Move the hiker to shade immediately, cool them down with wet cloths, and sip cool water with electrolytes. Do not allow rapid drinking. If they become confused or lose consciousness, it is Heat Stroke—a life-threatening emergency requiring rapid active cooling."
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"section": "Section 4: Altitude Illnesses (AMS, HAPE, HACE)",
|
| 16 |
+
"text": "Acute Mountain Sickness (AMS) occurs above 2400 meters (8000 feet). Symptoms include headache, nausea, fatigue, and dizziness. Treatment: Stop ascending, rest, and hydrate. If symptoms do not improve within 24 hours, descend immediately. High Altitude Pulmonary Edema (HAPE) presents with breathlessness at rest and a cough. High Altitude Cerebral Edema (HACE) presents with confusion and loss of coordination (ataxia). Both HAPE and HACE are emergency conditions requiring immediate descent."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"section": "Section 5: Musculoskeletal Injuries (Sprains, Fractures)",
|
| 20 |
+
"text": "For ankle sprains or joint strains, follow the R.I.C.E. protocol: Rest the joint, Ice or apply cold pack (submerge in cold stream), Compress with elastic bandage, and Elevate the limb. For suspected fractures, immobilize the limb using splints (made of sticks, sleeping pads, or trekking poles) in the position found. Do not try to realign bones."
|
| 21 |
+
}
|
| 22 |
+
]
|
src/gpx_parser.py
CHANGED
|
@@ -82,7 +82,190 @@ def calculate_elevation_gain_loss(elevations, threshold=2.0):
|
|
| 82 |
last_val = val
|
| 83 |
return gain, loss
|
| 84 |
|
| 85 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
"""
|
| 87 |
Parse a GPX file, fetch missing elevations, smooth the profile,
|
| 88 |
and compute trek statistics. Caches results locally to allow offline usage.
|
|
@@ -192,9 +375,7 @@ def parse_gpx_file(file_path, cache_dir="./temp"):
|
|
| 192 |
max_ele = max(smoothed_eles) if smoothed_eles else 0.0
|
| 193 |
|
| 194 |
# Naismith's Rule: 5 km/h base speed + 1 hour per 600m ascent
|
| 195 |
-
# estimated_hours = (dist_km / 5.0) + (gain_m / 600.0)
|
| 196 |
naismith_hours = (total_distance_km / 5.0) + (gain / 600.0)
|
| 197 |
-
# Estimate days assuming 8 hours hiking per day
|
| 198 |
estimated_days = max(1.0, naismith_hours / 8.0)
|
| 199 |
|
| 200 |
# Pre-parse waypoints if they exist in GPX
|
|
@@ -211,9 +392,7 @@ def parse_gpx_file(file_path, cache_dir="./temp"):
|
|
| 211 |
# Generate checkpoints
|
| 212 |
checkpoints = []
|
| 213 |
if waypoints:
|
| 214 |
-
# Match waypoints to track points to find cumulative distance
|
| 215 |
for wpt in waypoints:
|
| 216 |
-
# Find closest track point
|
| 217 |
min_d = float('inf')
|
| 218 |
closest_pt = points_data[0]
|
| 219 |
for pt in points_data:
|
|
@@ -228,12 +407,23 @@ def parse_gpx_file(file_path, cache_dir="./temp"):
|
|
| 228 |
"ele": closest_pt["ele"],
|
| 229 |
"cum_dist": closest_pt["cum_dist"] / 1000.0
|
| 230 |
})
|
| 231 |
-
# Sort by distance
|
| 232 |
checkpoints.sort(key=lambda c: c["cum_dist"])
|
| 233 |
else:
|
| 234 |
-
# Auto-generate checkpoints every 1000 meters
|
| 235 |
checkpoints = generate_checkpoints(points_data, interval_meters=1000.0)
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
result = {
|
| 238 |
"file_name": file_name,
|
| 239 |
"total_distance_km": round(total_distance_km, 2),
|
|
@@ -244,7 +434,8 @@ def parse_gpx_file(file_path, cache_dir="./temp"):
|
|
| 244 |
"estimated_days": round(estimated_days, 1),
|
| 245 |
"naismith_hours": round(naismith_hours, 1),
|
| 246 |
"points": points_data,
|
| 247 |
-
"checkpoints": checkpoints
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
# Save cache
|
|
|
|
| 82 |
last_val = val
|
| 83 |
return gain, loss
|
| 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 |
+
"""
|
| 102 |
+
headers = {
|
| 103 |
+
'User-Agent': 'TrailheadTrekPlanner/1.0 (skushwaha@hckthn.com)'
|
| 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", [])
|
| 111 |
+
pois = []
|
| 112 |
+
for el in elements:
|
| 113 |
+
lat = el.get("lat")
|
| 114 |
+
lon = el.get("lon")
|
| 115 |
+
tags = el.get("tags", {})
|
| 116 |
+
|
| 117 |
+
# Determine type
|
| 118 |
+
poi_type = "unknown"
|
| 119 |
+
if "amenity" in tags:
|
| 120 |
+
poi_type = tags["amenity"]
|
| 121 |
+
elif "natural" in tags:
|
| 122 |
+
poi_type = tags["natural"]
|
| 123 |
+
elif "tourism" in tags:
|
| 124 |
+
poi_type = tags["tourism"]
|
| 125 |
+
|
| 126 |
+
name = tags.get("name", tags.get("water", poi_type.replace("_", " ").title()))
|
| 127 |
+
pois.append({
|
| 128 |
+
"id": el.get("id"),
|
| 129 |
+
"lat": lat,
|
| 130 |
+
"lon": lon,
|
| 131 |
+
"type": poi_type,
|
| 132 |
+
"name": name
|
| 133 |
+
})
|
| 134 |
+
print(f"[gpx_parser] Overpass returned {len(pois)} raw POIs.")
|
| 135 |
+
return pois
|
| 136 |
+
else:
|
| 137 |
+
print(f"[gpx_parser] Overpass API returned status code {response.status_code}: {response.text}")
|
| 138 |
+
return []
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(f"[gpx_parser] Overpass query failed: {e}")
|
| 141 |
+
return []
|
| 142 |
+
|
| 143 |
+
def filter_pois_near_track(points, pois, buffer_meters=150.0):
|
| 144 |
+
"""
|
| 145 |
+
Filter POIs that are within buffer_meters of the track.
|
| 146 |
+
Returns list of POIs with distance and closest track point index.
|
| 147 |
+
"""
|
| 148 |
+
enhanced_pois = []
|
| 149 |
+
if not points or not pois:
|
| 150 |
+
return enhanced_pois
|
| 151 |
+
|
| 152 |
+
for poi in pois:
|
| 153 |
+
min_dist = float('inf')
|
| 154 |
+
closest_idx = -1
|
| 155 |
+
|
| 156 |
+
for idx, pt in enumerate(points):
|
| 157 |
+
d = haversine(poi["lat"], poi["lon"], pt["lat"], pt["lon"])
|
| 158 |
+
if d < min_dist:
|
| 159 |
+
min_dist = d
|
| 160 |
+
closest_idx = idx
|
| 161 |
+
|
| 162 |
+
if min_dist <= buffer_meters:
|
| 163 |
+
enhanced_pois.append({
|
| 164 |
+
"id": poi.get("id", 0),
|
| 165 |
+
"lat": poi["lat"],
|
| 166 |
+
"lon": poi["lon"],
|
| 167 |
+
"type": poi["type"],
|
| 168 |
+
"name": poi["name"],
|
| 169 |
+
"distance": round(min_dist, 1),
|
| 170 |
+
"track_index": closest_idx
|
| 171 |
+
})
|
| 172 |
+
|
| 173 |
+
print(f"[gpx_parser] Filtered {len(enhanced_pois)} POIs within {buffer_meters}m buffer.")
|
| 174 |
+
return enhanced_pois
|
| 175 |
+
|
| 176 |
+
def extract_pois_from_gpx(gpx):
|
| 177 |
+
"""
|
| 178 |
+
Extract POIs from GPX waypoints and track point extensions.
|
| 179 |
+
Returns a list of POI dictionaries.
|
| 180 |
+
"""
|
| 181 |
+
pois = []
|
| 182 |
+
# 1. Parse from waypoints
|
| 183 |
+
for wpt in gpx.waypoints:
|
| 184 |
+
desc = wpt.description or ""
|
| 185 |
+
poi_type = "unknown"
|
| 186 |
+
if "Type: " in desc:
|
| 187 |
+
parts = desc.split(",")
|
| 188 |
+
poi_type = parts[0].replace("Type: ", "").strip()
|
| 189 |
+
elif wpt.name:
|
| 190 |
+
# guess type from name/attributes
|
| 191 |
+
name_l = wpt.name.lower()
|
| 192 |
+
if "water" in name_l or "spring" in name_l or "fountain" in name_l:
|
| 193 |
+
poi_type = "drinking_water"
|
| 194 |
+
elif "camp" in name_l:
|
| 195 |
+
poi_type = "camp_site"
|
| 196 |
+
elif "hut" in name_l or "refuge" in name_l:
|
| 197 |
+
poi_type = "alpine_hut"
|
| 198 |
+
elif "shelter" in name_l:
|
| 199 |
+
poi_type = "shelter"
|
| 200 |
+
|
| 201 |
+
pois.append({
|
| 202 |
+
"lat": wpt.latitude,
|
| 203 |
+
"lon": wpt.longitude,
|
| 204 |
+
"name": wpt.name or "Waypoint",
|
| 205 |
+
"type": poi_type,
|
| 206 |
+
"distance": 0.0
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
# 2. Parse from track point extensions
|
| 210 |
+
idx = 0
|
| 211 |
+
for track in gpx.tracks:
|
| 212 |
+
for segment in track.segments:
|
| 213 |
+
for pt in segment.points:
|
| 214 |
+
if pt.extensions:
|
| 215 |
+
for ext in pt.extensions:
|
| 216 |
+
tag_name = ext.tag if hasattr(ext, 'tag') else ''
|
| 217 |
+
if 'poi' in tag_name:
|
| 218 |
+
poi_type = ext.attrib.get('type', 'unknown')
|
| 219 |
+
poi_name = ext.attrib.get('name', 'Waypoint')
|
| 220 |
+
try:
|
| 221 |
+
dist = float(ext.attrib.get('distance', 0.0))
|
| 222 |
+
except ValueError:
|
| 223 |
+
dist = 0.0
|
| 224 |
+
pois.append({
|
| 225 |
+
"lat": pt.latitude,
|
| 226 |
+
"lon": pt.longitude,
|
| 227 |
+
"name": poi_name,
|
| 228 |
+
"type": poi_type,
|
| 229 |
+
"distance": dist,
|
| 230 |
+
"track_index": idx
|
| 231 |
+
})
|
| 232 |
+
idx += 1
|
| 233 |
+
return pois
|
| 234 |
+
|
| 235 |
+
def save_enhanced_gpx(original_gpx_path, output_gpx_path, pois):
|
| 236 |
+
"""
|
| 237 |
+
Save enhanced GPX file with POIs loaded as waypoints and extensions.
|
| 238 |
+
"""
|
| 239 |
+
with open(original_gpx_path, "r", encoding="utf-8") as f:
|
| 240 |
+
gpx = gpxpy.parse(f)
|
| 241 |
+
|
| 242 |
+
# Overwrite waypoints
|
| 243 |
+
gpx.waypoints = []
|
| 244 |
+
for poi in pois:
|
| 245 |
+
wpt = gpxpy.gpx.GPXWaypoint(latitude=poi['lat'], longitude=poi['lon'], name=poi['name'])
|
| 246 |
+
wpt.description = f"Type: {poi['type']}, Distance from track: {poi['distance']:.1f}m"
|
| 247 |
+
gpx.waypoints.append(wpt)
|
| 248 |
+
|
| 249 |
+
# Add extensions to trackpoints
|
| 250 |
+
points = []
|
| 251 |
+
for track in gpx.tracks:
|
| 252 |
+
for segment in track.segments:
|
| 253 |
+
points.extend(segment.points)
|
| 254 |
+
|
| 255 |
+
import xml.etree.ElementTree as ET
|
| 256 |
+
for poi in pois:
|
| 257 |
+
idx = poi.get('track_index')
|
| 258 |
+
if idx is not None and 0 <= idx < len(points):
|
| 259 |
+
pt = points[idx]
|
| 260 |
+
# Create sub-element under extensions
|
| 261 |
+
poi_el = ET.Element('poi', type=poi['type'], name=poi['name'], distance=f"{poi['distance']:.1f}")
|
| 262 |
+
pt.extensions.append(poi_el)
|
| 263 |
+
|
| 264 |
+
with open(output_gpx_path, "w", encoding="utf-8") as f:
|
| 265 |
+
f.write(gpx.to_xml())
|
| 266 |
+
print(f"[gpx_parser] Saved enhanced GPX with {len(pois)} POIs to {output_gpx_path}")
|
| 267 |
+
|
| 268 |
+
def parse_gpx_file(file_path, cache_dir="./temp", buffer_meters=150.0):
|
| 269 |
"""
|
| 270 |
Parse a GPX file, fetch missing elevations, smooth the profile,
|
| 271 |
and compute trek statistics. Caches results locally to allow offline usage.
|
|
|
|
| 375 |
max_ele = max(smoothed_eles) if smoothed_eles else 0.0
|
| 376 |
|
| 377 |
# Naismith's Rule: 5 km/h base speed + 1 hour per 600m ascent
|
|
|
|
| 378 |
naismith_hours = (total_distance_km / 5.0) + (gain / 600.0)
|
|
|
|
| 379 |
estimated_days = max(1.0, naismith_hours / 8.0)
|
| 380 |
|
| 381 |
# Pre-parse waypoints if they exist in GPX
|
|
|
|
| 392 |
# Generate checkpoints
|
| 393 |
checkpoints = []
|
| 394 |
if waypoints:
|
|
|
|
| 395 |
for wpt in waypoints:
|
|
|
|
| 396 |
min_d = float('inf')
|
| 397 |
closest_pt = points_data[0]
|
| 398 |
for pt in points_data:
|
|
|
|
| 407 |
"ele": closest_pt["ele"],
|
| 408 |
"cum_dist": closest_pt["cum_dist"] / 1000.0
|
| 409 |
})
|
|
|
|
| 410 |
checkpoints.sort(key=lambda c: c["cum_dist"])
|
| 411 |
else:
|
|
|
|
| 412 |
checkpoints = generate_checkpoints(points_data, interval_meters=1000.0)
|
| 413 |
|
| 414 |
+
# Parse existing POIs from GPX
|
| 415 |
+
pois = extract_pois_from_gpx(gpx)
|
| 416 |
+
|
| 417 |
+
# If no POIs exist (like raw user upload), fetch from Overpass API (planning mode online)
|
| 418 |
+
if not pois:
|
| 419 |
+
lats = [pt["lat"] for pt in points_data]
|
| 420 |
+
lons = [pt["lon"] for pt in points_data]
|
| 421 |
+
min_lat, max_lat = min(lats) - 0.002, max(lats) + 0.002
|
| 422 |
+
min_lon, max_lon = min(lons) - 0.002, max(lons) + 0.002
|
| 423 |
+
|
| 424 |
+
raw_pois = fetch_overpass_pois(min_lat, min_lon, max_lat, max_lon)
|
| 425 |
+
pois = filter_pois_near_track(points_data, raw_pois, buffer_meters)
|
| 426 |
+
|
| 427 |
result = {
|
| 428 |
"file_name": file_name,
|
| 429 |
"total_distance_km": round(total_distance_km, 2),
|
|
|
|
| 434 |
"estimated_days": round(estimated_days, 1),
|
| 435 |
"naismith_hours": round(naismith_hours, 1),
|
| 436 |
"points": points_data,
|
| 437 |
+
"checkpoints": checkpoints,
|
| 438 |
+
"pois": pois
|
| 439 |
}
|
| 440 |
|
| 441 |
# Save cache
|
src/rag.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
| 6 |
+
GUIDES_FILE = os.path.join(DATA_DIR, "first_aid_guide.json")
|
| 7 |
+
|
| 8 |
+
def retrieve_first_aid(query_text):
|
| 9 |
+
"""
|
| 10 |
+
Search first_aid_guide.json for sections relevant to the query.
|
| 11 |
+
Returns (markdown_grounding_text, source_list) or (None, [])
|
| 12 |
+
"""
|
| 13 |
+
if not os.path.exists(GUIDES_FILE):
|
| 14 |
+
print(f"[rag.py] Guide file not found at {GUIDES_FILE}")
|
| 15 |
+
return None, []
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
with open(GUIDES_FILE, "r", encoding="utf-8") as f:
|
| 19 |
+
guides = json.load(f)
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"[rag.py] Error reading guides: {e}")
|
| 22 |
+
return None, []
|
| 23 |
+
|
| 24 |
+
query_text_lower = query_text.lower()
|
| 25 |
+
query_words = set(re.findall(r"\w+", query_text_lower))
|
| 26 |
+
|
| 27 |
+
matches = []
|
| 28 |
+
|
| 29 |
+
# Pre-defined keyword map for high relevance scores
|
| 30 |
+
keywords_map = {
|
| 31 |
+
"section 1": ["bleed", "wound", "cut", "blood", "bandage", "tourniquet", "injury"],
|
| 32 |
+
"section 2": ["cold", "hypothermia", "freeze", "frostbite", "shiver", "rewarm"],
|
| 33 |
+
"section 3": ["heat", "exhaustion", "dehydration", "stroke", "hot", "sunstroke", "sweat"],
|
| 34 |
+
"section 4": ["altitude", "ams", "hape", "hace", "headache", "dizzy", "mountain sickness", "nausea", "pulmonary", "cerebral"],
|
| 35 |
+
"section 5": ["sprain", "fracture", "break", "splint", "ankle", "joint", "bone", "rice", "strain"]
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
for guide in guides:
|
| 39 |
+
score = 0
|
| 40 |
+
section = guide.get("section", "")
|
| 41 |
+
text = guide.get("text", "")
|
| 42 |
+
section_lower = section.lower()
|
| 43 |
+
|
| 44 |
+
# 1. Map based matching
|
| 45 |
+
for key, words in keywords_map.items():
|
| 46 |
+
if key in section_lower:
|
| 47 |
+
for w in words:
|
| 48 |
+
if w in query_text_lower:
|
| 49 |
+
score += 3
|
| 50 |
+
|
| 51 |
+
# 2. General overlap matching
|
| 52 |
+
combined_text = (section + " " + text).lower()
|
| 53 |
+
for word in query_words:
|
| 54 |
+
if len(word) > 2 and word in combined_text:
|
| 55 |
+
score += 1
|
| 56 |
+
|
| 57 |
+
if score > 0:
|
| 58 |
+
matches.append((score, guide))
|
| 59 |
+
|
| 60 |
+
# Sort matches by score descending
|
| 61 |
+
matches.sort(key=lambda x: x[0], reverse=True)
|
| 62 |
+
|
| 63 |
+
if not matches:
|
| 64 |
+
return None, []
|
| 65 |
+
|
| 66 |
+
grounding_parts = []
|
| 67 |
+
sources = []
|
| 68 |
+
|
| 69 |
+
# Take top matching guide to ground the model response
|
| 70 |
+
for idx, (score, guide) in enumerate(matches[:1]):
|
| 71 |
+
sec = guide["section"]
|
| 72 |
+
txt = guide["text"]
|
| 73 |
+
|
| 74 |
+
grounding_parts.append(
|
| 75 |
+
f"### {sec}\n"
|
| 76 |
+
f"{txt}\n"
|
| 77 |
+
)
|
| 78 |
+
sources.append(sec)
|
| 79 |
+
|
| 80 |
+
grounding_text = "\n---\n".join(grounding_parts)
|
| 81 |
+
return grounding_text, sources
|
test_overpass.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from src.gpx_parser import parse_gpx_file, fetch_overpass_pois, filter_pois_near_track
|
| 4 |
+
|
| 5 |
+
def run_test():
|
| 6 |
+
gpx_path = r"C:\Users\skushwaha\Documents\hckthn\TrailHead\Routes\track_5-14724236830.gpx"
|
| 7 |
+
print(f"--- Testing GPX parsing and Overpass integration on: {gpx_path} ---")
|
| 8 |
+
|
| 9 |
+
if not os.path.exists(gpx_path):
|
| 10 |
+
print(f"Error: GPX file not found at {gpx_path}")
|
| 11 |
+
sys.exit(1)
|
| 12 |
+
|
| 13 |
+
# 1. Parse GPX file and see what's loaded
|
| 14 |
+
print("\n1. Running parse_gpx_file...")
|
| 15 |
+
result = parse_gpx_file(gpx_path)
|
| 16 |
+
|
| 17 |
+
print(f"File Name: {result['file_name']}")
|
| 18 |
+
print(f"Total Distance: {result['total_distance_km']} km")
|
| 19 |
+
print(f"Elevation Gain: {result['elevation_gain_m']} m")
|
| 20 |
+
print(f"Elevation Loss: {result['elevation_loss_m']} m")
|
| 21 |
+
print(f"Min/Max Elevation: {result['min_elevation_m']}m / {result['max_elevation_m']}m")
|
| 22 |
+
print(f"Number of points: {len(result['points'])}")
|
| 23 |
+
print(f"Number of checkpoints: {len(result['checkpoints'])}")
|
| 24 |
+
|
| 25 |
+
pois = result.get("pois", [])
|
| 26 |
+
print(f"Number of POIs parsed/fetched: {len(pois)}")
|
| 27 |
+
for i, poi in enumerate(pois[:10]):
|
| 28 |
+
print(f" [{i+1}] Name: {poi['name']}, Type: {poi['type']}, Distance from route: {poi.get('distance', 0.0)}m, Lat/Lon: {poi['lat']}, {poi['lon']}")
|
| 29 |
+
|
| 30 |
+
# 2. Test live Overpass API query directly using the bounding box of the track
|
| 31 |
+
print("\n2. Direct Test of Overpass API Query...")
|
| 32 |
+
points_data = result["points"]
|
| 33 |
+
lats = [pt["lat"] for pt in points_data]
|
| 34 |
+
lons = [pt["lon"] for pt in points_data]
|
| 35 |
+
min_lat, max_lat = min(lats) - 0.002, max(lats) + 0.002
|
| 36 |
+
min_lon, max_lon = min(lons) - 0.002, max(lons) + 0.002
|
| 37 |
+
|
| 38 |
+
print(f"Bounding box: [{min_lat:.5f}, {min_lon:.5f}, {max_lat:.5f}, {max_lon:.5f}]")
|
| 39 |
+
raw_pois = fetch_overpass_pois(min_lat, min_lon, max_lat, max_lon)
|
| 40 |
+
print(f"Overpass returned {len(raw_pois)} raw POIs.")
|
| 41 |
+
|
| 42 |
+
filtered_pois = filter_pois_near_track(points_data, raw_pois, buffer_meters=150.0)
|
| 43 |
+
print(f"Filtered {len(filtered_pois)} POIs within 150m buffer.")
|
| 44 |
+
for i, poi in enumerate(filtered_pois[:10]):
|
| 45 |
+
print(f" [{i+1}] Name: {poi['name']}, Type: {poi['type']}, Buffer Dist: {poi['distance']}m")
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
run_test()
|