Spaces:
Sleeping
Sleeping
| 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""" | |
| <div style="position: fixed; | |
| top: 10px; left: 10px; width: auto; height: auto; | |
| z-index: 9999; font-size: 15px; | |
| background-color: white; padding: 8px; border: 2px solid #000;"> | |
| <b>Clusters (data from {hour} hours ago)</b><br/> | |
| DBSCAN eps = {eps_km} km <br/> | |
| clusters found = {n_clusters}, total points = {len(coords)} | |
| </div> | |
| """ | |
| 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) | |