Chris773 commited on
Commit
b47609c
·
verified ·
1 Parent(s): 418df02

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -226
app.py CHANGED
@@ -4,8 +4,7 @@
4
  # (6) multi-leg editor as a table (Dataframe)
5
  # (7) Add Leg + Reset buttons
6
  # (8) Stats panel
7
- # Leaflet-style interactive map like your screenshot (pan/zoom/tiles/popups)
8
- # FIX: Folium map is embedded via <iframe srcdoc=...> so it shows correctly in Gradio
9
 
10
  import re
11
  import html
@@ -24,15 +23,20 @@ DEFAULT_LEGS = [
24
  [270, 500, 10],
25
  [240, 500, 10],
26
  ]
27
- NEW_LEG_DEFAULT = [0, 500, 10] # heading_deg, speed_knots, minutes
28
-
 
 
 
 
 
 
 
 
 
29
 
30
  # ---------------- KML parsing ----------------
31
  def load_ping_rings_from_kml(path=KML_FILE):
32
- """
33
- Returns dict: ring_name -> list[(lat, lon), ...]
34
- Supports LineString rings.
35
- """
36
  ns = {"kml": "http://www.opengis.net/kml/2.2"}
37
  tree = ET.parse(path)
38
  root = tree.getroot()
@@ -40,10 +44,9 @@ def load_ping_rings_from_kml(path=KML_FILE):
40
  rings = {}
41
  for pm in root.findall(".//kml:Placemark", ns):
42
  name_el = pm.find("kml:name", ns)
43
- if name_el is None or not name_el.text:
44
  continue
45
  name = name_el.text.strip()
46
-
47
  if not name.lower().startswith("ping ring"):
48
  continue
49
 
@@ -52,310 +55,158 @@ def load_ping_rings_from_kml(path=KML_FILE):
52
  continue
53
 
54
  pts = []
55
- text = coords_el.text.strip()
56
- for token in re.split(r"\s+", text):
57
- if not token:
58
- continue
59
- parts = token.split(",")
60
- if len(parts) < 2:
61
- continue
62
- lon = float(parts[0])
63
- lat = float(parts[1])
64
- pts.append((lat, lon))
65
 
66
  if pts:
67
  rings[name] = pts
68
 
69
- if not rings:
70
- raise RuntimeError(
71
- f"No ping rings found in KML. Check file name/path: {path} and Placemark names."
72
- )
73
  return rings
74
 
75
 
76
  PING_RINGS = load_ping_rings_from_kml()
77
 
78
-
79
  # ---------------- legs helpers ----------------
80
  def df_to_rows(df):
81
  if df is None:
82
  return []
83
- if hasattr(df, "to_numpy"): # pandas.DataFrame
84
  return df.to_numpy().tolist()
85
- return df # list-of-lists
86
 
87
 
88
  def parse_legs_df(df):
89
- rows = df_to_rows(df)
90
  legs = []
91
- for row in rows:
92
- if row is None or len(row) < 3:
93
- continue
94
- h, s, m = row[0], row[1], row[2]
95
- if h is None or s is None or m is None:
96
- continue
97
- if isinstance(h, float) and np.isnan(h):
98
  continue
99
- if isinstance(s, float) and np.isnan(s):
100
- continue
101
- if isinstance(m, float) and np.isnan(m):
102
  continue
103
  legs.append((float(h), float(s), float(m)))
104
-
105
- if not legs:
106
- legs = [(300, 500, 10), (270, 500, 10), (240, 500, 10)]
107
- return legs
108
 
109
 
110
  def add_leg(df):
111
  rows = df_to_rows(df)
112
-
113
- cleaned = []
114
- for r in rows:
115
- if r is None or len(r) < 3:
116
- continue
117
- a, b, c = r[0], r[1], r[2]
118
- if (a is None or (isinstance(a, float) and np.isnan(a))) and \
119
- (b is None or (isinstance(b, float) and np.isnan(b))) and \
120
- (c is None or (isinstance(c, float) and np.isnan(c))):
121
- continue
122
- cleaned.append([a, b, c])
123
-
124
- cleaned.append(list(NEW_LEG_DEFAULT))
125
- return gr.update(value=cleaned)
126
 
127
 
128
  def reset_legs():
129
  return gr.update(value=[row[:] for row in DEFAULT_LEGS])
130
 
131
-
132
  # ---------------- sim core + stats ----------------
133
  def simulate_path(start_lat, start_lon, legs, method):
134
- """
135
- Returns:
136
- pts: [(lat,lon), ...] including start and end of each leg
137
- seg_dists_m: [distance_m for each leg]
138
- total_time_min: float
139
- """
140
  lat, lon = float(start_lat), float(start_lon)
141
  pts = [(lat, lon)]
142
  seg_dists_m = []
143
  total_time_min = 0.0
144
 
145
- for heading, speed_knots, minutes in legs:
146
- dist_m = speed_knots * 1852.0 * (minutes / 60.0)
147
-
148
  if method == "VINCENTY":
149
  (lat, lon), _ = fp.vincenty_direct((lat, lon), heading, dist_m)
150
  else:
151
  lat, lon = fp.rhumb_direct((lat, lon), heading, dist_m)
152
-
153
  pts.append((lat, lon))
154
  seg_dists_m.append(dist_m)
155
- total_time_min += float(minutes)
156
 
157
  return pts, seg_dists_m, total_time_min
158
 
159
 
160
- def compute_stats(path_pts, seg_dists_m, total_time_min, legs):
161
- total_dist_m = float(np.sum(seg_dists_m)) if seg_dists_m else 0.0
162
- total_dist_km = total_dist_m / 1000.0
163
- total_dist_nm = total_dist_m / 1852.0
164
-
165
- total_time_hr = total_time_min / 60.0
166
- avg_gs_knots = (total_dist_nm / total_time_hr) if total_time_hr > 0 else 0.0
167
-
168
- end_lat, end_lon = path_pts[-1]
169
-
170
- lines = [
171
- f"**Legs:** {len(legs)}",
172
- f"**Total time:** {total_time_min:.1f} min ({total_time_hr:.2f} hr)",
173
- f"**Total distance:** {total_dist_nm:.1f} nm ({total_dist_km:.1f} km)",
174
- f"**Average ground speed:** {avg_gs_knots:.1f} kt",
175
- f"**Final position:** lat {end_lat:.4f}, lon {end_lon:.4f}",
176
- ]
177
- return "\n\n".join(lines)
178
-
179
-
180
- # ---------------- Leaflet map (Folium) ----------------
181
- def _all_bounds(points_list):
182
- lats, lons = [], []
183
- for pts in points_list:
184
- for (la, lo) in pts:
185
- if la is None or lo is None:
186
- continue
187
- lats.append(float(la))
188
- lons.append(float(lo))
189
- if not lats:
190
- return None
191
- return [[min(lats), min(lons)], [max(lats), max(lons)]]
192
-
193
-
194
- def build_leaflet_map(path_pts, legs, seg_dists_m, selected_rings):
195
- start_lat, start_lon = path_pts[0]
196
- m = folium.Map(
197
- location=[start_lat, start_lon],
198
- zoom_start=4,
199
- tiles="OpenStreetMap",
200
- control_scale=True,
201
- )
202
 
203
- # ---- ping rings (blue like your screenshot) ----
204
- for ring_name in selected_rings or []:
205
- ring_pts = PING_RINGS.get(ring_name)
206
- if not ring_pts:
207
- continue
208
- folium.PolyLine(
209
- ring_pts,
210
- weight=3,
211
- opacity=0.95,
212
- color="blue",
213
- tooltip=ring_name,
214
- ).add_to(m)
215
-
216
- # ---- flight path (red like your screenshot) ----
217
- folium.PolyLine(
218
- path_pts,
219
- weight=4,
220
- opacity=1.0,
221
- color="red",
222
- tooltip="Flight path",
223
- ).add_to(m)
224
 
225
- # ---- start/end markers ----
 
226
  folium.CircleMarker(
227
- location=path_pts[0],
228
- radius=6,
 
229
  fill=True,
230
- color="green",
231
- fill_opacity=1.0,
232
- popup=folium.Popup(
233
- f"<b>Start</b><br>Lat: {path_pts[0][0]:.5f}<br>Lon: {path_pts[0][1]:.5f}",
234
- max_width=300,
235
- ),
236
  ).add_to(m)
237
 
238
- folium.CircleMarker(
239
- location=path_pts[-1],
240
- radius=6,
241
- fill=True,
242
- color="purple",
243
- fill_opacity=1.0,
244
- popup=folium.Popup(
245
- f"<b>End</b><br>Lat: {path_pts[-1][0]:.5f}<br>Lon: {path_pts[-1][1]:.5f}",
246
- max_width=300,
 
 
247
  ),
248
  ).add_to(m)
249
 
250
- # ---- waypoint markers with popups ----
251
- total_dist_km = 0.0
252
- elapsed_min = 0.0
253
 
254
- for i, (lat, lon) in enumerate(path_pts):
255
- if i == 0:
256
- heading = None
257
- speed = None
258
- minutes = 0.0
259
- else:
260
- heading, speed, minutes = legs[i - 1]
261
- total_dist_km += (seg_dists_m[i - 1] / 1000.0)
262
- elapsed_min += minutes
263
-
264
- popup_lines = [
265
- f"<b>Waypoint {i}</b>",
266
- f"Lat: {lat:.5f}",
267
- f"Lon: {lon:.5f}",
268
- f"Time elapsed: {elapsed_min:.1f} min",
269
- ]
270
- if i > 0:
271
- popup_lines += [
272
- f"Heading: {heading:.1f}°",
273
- f"Speed: {speed:.0f} kt",
274
- f"Leg: {minutes:.1f} min",
275
- f"Distance flown: {total_dist_km:.2f} km",
276
- ]
277
-
278
- folium.CircleMarker(
279
- location=(lat, lon),
280
- radius=5,
281
- fill=True,
282
- color="white",
283
- fill_color="white",
284
- fill_opacity=1.0,
285
- popup=folium.Popup("<br>".join(popup_lines), max_width=340),
286
- ).add_to(m)
287
-
288
- # ---- fit map to all plotted data ----
289
- bounds = _all_bounds(
290
- [path_pts]
291
- + [PING_RINGS[r] for r in (selected_rings or []) if r in PING_RINGS]
292
- )
293
- if bounds:
294
- m.fit_bounds(bounds, padding=(20, 20))
295
 
296
- # ---- IMPORTANT FIX: embed as iframe with fixed height ----
297
- map_html = m.get_root().render()
298
- escaped = html.escape(map_html, quote=True)
299
 
 
300
  return f"""
301
- <iframe
302
- srcdoc="{escaped}"
303
- style="width: 100%; height: 650px; border: 0; border-radius: 12px;"
304
- loading="lazy"
305
- referrerpolicy="no-referrer"
306
- ></iframe>
307
  """
308
 
309
 
310
  def run_sim(start_lat, start_lon, legs_df, method, rings):
311
  legs = parse_legs_df(legs_df)
312
- path_pts, seg_dists_m, total_time_min = simulate_path(start_lat, start_lon, legs, method)
313
-
314
- html_map = build_leaflet_map(path_pts, legs, seg_dists_m, rings)
315
- stats_md = compute_stats(path_pts, seg_dists_m, total_time_min, legs)
316
- return html_map, stats_md
317
-
318
 
319
  # ---------------- UI ----------------
320
  with gr.Blocks() as demo:
321
- gr.Markdown("## ✈️ MH370 Flight Simulator — Leaflet Map + Ping Rings (KML)")
322
 
323
  with gr.Row():
324
  start_lat = gr.Slider(-10, 10, value=6.3, label="Start Latitude")
325
  start_lon = gr.Slider(90, 110, value=96.3, label="Start Longitude")
326
- method = gr.Dropdown(["VINCENTY", "RHUMB"], value="VINCENTY", label="Method")
327
 
328
  legs_df = gr.Dataframe(
329
  headers=["heading_deg", "speed_knots", "minutes"],
330
  datatype=["number", "number", "number"],
331
- row_count=(10, "dynamic"),
332
  column_count=(3, "fixed"),
333
- value=[row[:] for row in DEFAULT_LEGS],
334
- label="Multi-leg plan",
335
  )
336
 
337
  with gr.Row():
338
  add_btn = gr.Button("➕ Add leg")
339
  reset_btn = gr.Button("♻️ Reset legs")
340
 
341
- rings = gr.CheckboxGroup(
342
- choices=sorted(PING_RINGS.keys()),
343
- value=[sorted(PING_RINGS.keys())[0]],
344
- label="Ping rings (from your KML)",
345
- )
346
 
347
  btn = gr.Button("Simulate on Map")
348
-
349
  out_map = gr.HTML()
350
  stats = gr.Markdown()
351
 
352
- btn.click(
353
- run_sim,
354
- inputs=[start_lat, start_lon, legs_df, method, rings],
355
- outputs=[out_map, stats],
356
- )
357
-
358
- add_btn.click(add_leg, inputs=[legs_df], outputs=[legs_df])
359
  reset_btn.click(reset_legs, outputs=[legs_df])
360
 
361
  demo.launch()
 
4
  # (6) multi-leg editor as a table (Dataframe)
5
  # (7) Add Leg + Reset buttons
6
  # (8) Stats panel
7
+ # (13) Permanent labeled waypoints (no toggle)
 
8
 
9
  import re
10
  import html
 
23
  [270, 500, 10],
24
  [240, 500, 10],
25
  ]
26
+ NEW_LEG_DEFAULT = [0, 500, 10]
27
+
28
+ # ---------------- Waypoints (permanent labels) ----------------
29
+ WAYPOINTS = [
30
+ ("IGARI", 6.936111111111112, 103.58500000000001),
31
+ ("VAMPI", 6.182222222222222, 97.58555555555554),
32
+ ("NILAM", 6.756388888888889, 95.97638888888889),
33
+ ("SANOB", 6.586111111111111, 95.66916666666667),
34
+ ("MEKAR", 6.500000000000000, 96.48333333333333),
35
+ ("VPG", 5.279638888888889, 100.26038888888888),
36
+ ]
37
 
38
  # ---------------- KML parsing ----------------
39
  def load_ping_rings_from_kml(path=KML_FILE):
 
 
 
 
40
  ns = {"kml": "http://www.opengis.net/kml/2.2"}
41
  tree = ET.parse(path)
42
  root = tree.getroot()
 
44
  rings = {}
45
  for pm in root.findall(".//kml:Placemark", ns):
46
  name_el = pm.find("kml:name", ns)
47
+ if not name_el or not name_el.text:
48
  continue
49
  name = name_el.text.strip()
 
50
  if not name.lower().startswith("ping ring"):
51
  continue
52
 
 
55
  continue
56
 
57
  pts = []
58
+ for token in re.split(r"\s+", coords_el.text.strip()):
59
+ lon, lat, *_ = token.split(",")
60
+ pts.append((float(lat), float(lon)))
 
 
 
 
 
 
 
61
 
62
  if pts:
63
  rings[name] = pts
64
 
 
 
 
 
65
  return rings
66
 
67
 
68
  PING_RINGS = load_ping_rings_from_kml()
69
 
 
70
  # ---------------- legs helpers ----------------
71
  def df_to_rows(df):
72
  if df is None:
73
  return []
74
+ if hasattr(df, "to_numpy"):
75
  return df.to_numpy().tolist()
76
+ return df
77
 
78
 
79
  def parse_legs_df(df):
 
80
  legs = []
81
+ for row in df_to_rows(df):
82
+ if not row or len(row) < 3:
 
 
 
 
 
83
  continue
84
+ h, s, m = row
85
+ if any(v is None or (isinstance(v, float) and np.isnan(v)) for v in row):
 
86
  continue
87
  legs.append((float(h), float(s), float(m)))
88
+ return legs or [(300, 500, 10), (270, 500, 10), (240, 500, 10)]
 
 
 
89
 
90
 
91
  def add_leg(df):
92
  rows = df_to_rows(df)
93
+ rows.append(list(NEW_LEG_DEFAULT))
94
+ return gr.update(value=rows)
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
 
97
  def reset_legs():
98
  return gr.update(value=[row[:] for row in DEFAULT_LEGS])
99
 
 
100
  # ---------------- sim core + stats ----------------
101
  def simulate_path(start_lat, start_lon, legs, method):
 
 
 
 
 
 
102
  lat, lon = float(start_lat), float(start_lon)
103
  pts = [(lat, lon)]
104
  seg_dists_m = []
105
  total_time_min = 0.0
106
 
107
+ for heading, speed, minutes in legs:
108
+ dist_m = speed * 1852.0 * (minutes / 60.0)
 
109
  if method == "VINCENTY":
110
  (lat, lon), _ = fp.vincenty_direct((lat, lon), heading, dist_m)
111
  else:
112
  lat, lon = fp.rhumb_direct((lat, lon), heading, dist_m)
 
113
  pts.append((lat, lon))
114
  seg_dists_m.append(dist_m)
115
+ total_time_min += minutes
116
 
117
  return pts, seg_dists_m, total_time_min
118
 
119
 
120
+ def compute_stats(path_pts, seg_dists_m, total_time_min):
121
+ total_nm = sum(seg_dists_m) / 1852.0
122
+ total_hr = total_time_min / 60.0
123
+ avg_gs = total_nm / total_hr if total_hr > 0 else 0.0
124
+ lat, lon = path_pts[-1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ return "\n\n".join([
127
+ f"**Total time:** {total_time_min:.1f} min",
128
+ f"**Total distance:** {total_nm:.1f} nm",
129
+ f"**Average GS:** {avg_gs:.1f} kt",
130
+ f"**Final position:** {lat:.4f}, {lon:.4f}",
131
+ ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ # ---------------- Leaflet map ----------------
134
+ def add_label(m, name, lat, lon):
135
  folium.CircleMarker(
136
+ location=(lat, lon),
137
+ radius=4,
138
+ color="black",
139
  fill=True,
140
+ fill_color="white",
141
+ fill_opacity=1,
 
 
 
 
142
  ).add_to(m)
143
 
144
+ folium.Marker(
145
+ location=(lat, lon),
146
+ icon=folium.DivIcon(
147
+ html=f"""
148
+ <div style="
149
+ font-size:13px;
150
+ font-weight:700;
151
+ text-shadow:1px 1px 2px white;
152
+ transform: translate(8px,-12px);
153
+ ">{html.escape(name)}</div>
154
+ """
155
  ),
156
  ).add_to(m)
157
 
 
 
 
158
 
159
+ def build_map(path_pts, legs, seg_dists_m, rings):
160
+ m = folium.Map(location=path_pts[0], zoom_start=5, tiles="OpenStreetMap")
161
+
162
+ for r in rings:
163
+ folium.PolyLine(PING_RINGS[r], color="blue", weight=3).add_to(m)
164
+
165
+ folium.PolyLine(path_pts, color="red", weight=4).add_to(m)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ for name, lat, lon in WAYPOINTS:
168
+ add_label(m, name, lat, lon)
 
169
 
170
+ html_map = m.get_root().render()
171
  return f"""
172
+ <iframe srcdoc="{html.escape(html_map, quote=True)}"
173
+ style="width:100%;height:650px;border:0;"></iframe>
 
 
 
 
174
  """
175
 
176
 
177
  def run_sim(start_lat, start_lon, legs_df, method, rings):
178
  legs = parse_legs_df(legs_df)
179
+ pts, dists, tmin = simulate_path(start_lat, start_lon, legs, method)
180
+ return build_map(pts, legs, dists, rings), compute_stats(pts, dists, tmin)
 
 
 
 
181
 
182
  # ---------------- UI ----------------
183
  with gr.Blocks() as demo:
184
+ gr.Markdown("## ✈️ MH370 Flight Simulator — Leaflet Map")
185
 
186
  with gr.Row():
187
  start_lat = gr.Slider(-10, 10, value=6.3, label="Start Latitude")
188
  start_lon = gr.Slider(90, 110, value=96.3, label="Start Longitude")
189
+ method = gr.Dropdown(["VINCENTY", "RHUMB"], value="VINCENTY")
190
 
191
  legs_df = gr.Dataframe(
192
  headers=["heading_deg", "speed_knots", "minutes"],
193
  datatype=["number", "number", "number"],
 
194
  column_count=(3, "fixed"),
195
+ value=DEFAULT_LEGS,
 
196
  )
197
 
198
  with gr.Row():
199
  add_btn = gr.Button("➕ Add leg")
200
  reset_btn = gr.Button("♻️ Reset legs")
201
 
202
+ rings = gr.CheckboxGroup(choices=sorted(PING_RINGS.keys()), value=list(PING_RINGS)[:1])
 
 
 
 
203
 
204
  btn = gr.Button("Simulate on Map")
 
205
  out_map = gr.HTML()
206
  stats = gr.Markdown()
207
 
208
+ btn.click(run_sim, [start_lat, start_lon, legs_df, method, rings], [out_map, stats])
209
+ add_btn.click(add_leg, [legs_df], [legs_df])
 
 
 
 
 
210
  reset_btn.click(reset_legs, outputs=[legs_df])
211
 
212
  demo.launch()