import requests import sys import math import numpy as np from shapely.geometry import Point, MultiPoint from sklearn.cluster import DBSCAN import geopandas as gpd import gradio as gr import geopy.distance import folium def load_countries(): countries_dict = {} countries = gpd.read_file("https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_countries.geojson") countries = countries[['name', 'geometry']] for index, row in countries.iterrows(): centroid = row['geometry'].centroid # store as (lat, lon) countries_dict[row['name']] = (centroid.y, centroid.x) return countries_dict def find_closest_country(coordinate, countries): closest_country = None smallest_distance = None for country in countries: distance = geopy.distance.geodesic(coordinate, countries[country]).km if smallest_distance is None or distance < smallest_distance: smallest_distance = distance closest_country = country return closest_country, smallest_distance def track_balloon(index): index = int(index) global countries points = [] annotations = [] for i in range(24): url = f"https://a.windbornesystems.com/treasure/{i:02}.json" try: response = requests.get(url, timeout=8) if response.status_code != 200: print("failed to fetch from url") continue data = response.json() except Exception as e: print("Failed to retrieve data.", e) continue balloon = data[index - 1] lat, lon = balloon[:2] points.append((lat, lon)) country, distance_km = find_closest_country((lat, lon), countries) description = f"Hour offset: {i} — Closest country: {country} ({distance_km:.1f} km)" annotations.append((lat, lon, description)) m = folium.Map(location=points[-1]) folium.PolyLine(locations=points, weight=3, opacity=1).add_to(m) for idx, (lat, lon, desc) in enumerate(annotations): popup_text = desc if idx == 0: folium.CircleMarker(location=(lat, lon), radius=10, popup=popup_text, tooltip="Last known location", fill=True).add_to(m) else: folium.Marker(location=(lat, lon), popup=popup_text, tooltip=f"Hour {idx}").add_to(m) return m._repr_html_() def cluster_balloons(hour, eps_km, samples, show_hulls): earth_radius = 6371 points = [] url = f"https://a.windbornesystems.com/treasure/{hour:02}.json" r = requests.get(url, timeout=8) if r.status_code != 200: return f"Error: Problem querying {url}" data = r.json() for balloon in data: lat, lon = balloon[:2] points.append((lat, lon)) if not points: return "Error: no points found" coords = np.array(points) coords_rad = np.radians(coords) eps_rad = eps_km /earth_radius labels = DBSCAN(eps=eps_rad, min_samples=samples, metric='haversine').fit_predict(coords_rad) # print(labels) unique_labels = sorted(set(labels)) # print(unique_labels) n_clusters = len(unique_labels) if -1 in unique_labels: n_clusters -=1 base_colors = [ "#000ddd", "#929203", "#2ca02c", "#d62728", "#5b3181", "#5e403a", "#e377c2", "#796060", "#ffff00", "#00e5ff", "#3a657c", "#2b4217" ] def color_for_label(label): if label == -1: return "#000000" return base_colors[label % len(base_colors)] map = folium.Map(location=coords.mean(axis=0).tolist(), zoom_start=3) for (lat, lon), label in zip(coords, labels): folium.CircleMarker( location=(lat, lon), radius=3, color=color_for_label(label), fill=True, fill_opacity=1, popup=f"Cluster: {label}" if label > -1 else "Noise" ).add_to(map) # draw border around clusters if show_hulls: for label in unique_labels: if label == -1: continue mask = (labels == label) curr_cluster = coords[mask] if len(curr_cluster) == 0: continue hull_points = [] for point in curr_cluster: hull_points.append(Point(point[1], point[0])) # change lattitude, longitude order hull calculation mp = MultiPoint(hull_points) hull = mp.convex_hull hull_coords = [] for lon, lat in hull.exterior.coords: hull_coords.append((lat, lon)) # change back for folium map again l_color = color_for_label(label) folium.Polygon( locations=hull_coords, color=l_color, weight=2, fill=True, fill_color=l_color, fill_opacity=0.2, popup=f"Cluster {label}, size: {len(curr_cluster)}" ).add_to(map) title_for_html = f"""
Clusters (data from {hour} hours ago)
DBSCAN eps = {eps_km} km
clusters found = {n_clusters}, total points = {len(coords)}
""" map.get_root().html.add_child(folium.Element(title_for_html)) return map._repr_html_() countries = load_countries() with gr.Blocks() as demo: gr.Markdown("Cluster Visualization + Balloon Tracker") with gr.Tabs(): with gr.TabItem("Cluster balloons (all balloons)"): with gr.Row(): hours_slider = gr.Slider(label="Hours ago", minimum=0, maximum=23, step=1, value=0) num_samples = gr.Number(label="DBSCAN min_samples", value=5, precision=0) eps_km = gr.Number(label="DBSCAN eps (km)", value=750, precision=1) with gr.Row(): hull_checkbox = gr.Checkbox(label="Draw hull around clusters", value=True) cluster_btn = gr.Button("Show Clusters") clusters_map_html = gr.HTML() cluster_btn.click(fn=cluster_balloons, inputs=[hours_slider, eps_km, num_samples, hull_checkbox], outputs=[clusters_map_html]) with gr.TabItem("Track balloon"): with gr.Row(): balloon = gr.Number(label="Balloon index", minimum=1, maximum=1000, value=1) track_btn = gr.Button("Track") map_html = gr.HTML() track_btn.click(fn=track_balloon, inputs=[balloon], outputs=[map_html]) demo.launch(share=True)