File size: 6,998 Bytes
cd20d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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)