Lode Nachtergaele commited on
Commit
b7b98b6
·
1 Parent(s): 3f2b17c

added climb profiles

Browse files
Files changed (1) hide show
  1. app.py +155 -11
app.py CHANGED
@@ -119,6 +119,9 @@ def grade_to_color(grade):
119
 
120
 
121
  def find_climbs(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
122
  peaks, _ = find_peaks(df["smoothed_elevation"])
123
  df_peaks = df.iloc[peaks, :].assign(base=0).assign(kind="peak")
124
  valleys, _ = find_peaks(df["smoothed_elevation"].max() - df["smoothed_elevation"])
@@ -184,7 +187,14 @@ def generate_height_profile_json(df: pd.DataFrame) -> str:
184
 
185
  elevation = (
186
  alt.Chart(
187
- df[["distance_from_start", "smoothed_elevation", "smoothed_grade_color"]]
 
 
 
 
 
 
 
188
  )
189
  .mark_bar()
190
  .encode(
@@ -204,6 +214,16 @@ def generate_height_profile_json(df: pd.DataFrame) -> str:
204
  title=None,
205
  ),
206
  color=alt.Color("smoothed_grade_color").scale(None),
 
 
 
 
 
 
 
 
 
 
207
  )
208
  )
209
  max_elevation = df["elev"].max().round(-1)
@@ -272,7 +292,17 @@ def generate_height_profile_json(df: pd.DataFrame) -> str:
272
  df_peaks_filtered.reset_index(drop=True)
273
  .assign(number=lambda df_: df_.index + 1)
274
  .assign(circle_pos=lambda df_: df_["max_elevation"] + 20)[
275
- ["distance_from_start", "max_elevation", "circle_pos", "number"]
 
 
 
 
 
 
 
 
 
 
276
  ]
277
  )
278
  # annotation = (
@@ -294,6 +324,19 @@ def generate_height_profile_json(df: pd.DataFrame) -> str:
294
  ),
295
  y="max_elevation",
296
  text="number",
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  )
298
  )
299
  chart = (
@@ -306,15 +349,47 @@ def generate_height_profile_json(df: pd.DataFrame) -> str:
306
  return chart, df_peaks_filtered
307
 
308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  gpx_file = st.file_uploader("Upload gpx file", type=["gpx"])
310
 
311
  if gpx_file is not None:
312
  ave_lat, ave_lon, lon_list, lat_list, h_list = get_gpx(gpx_file)
313
  df = pd.DataFrame({"lon": lon_list, "lat": lat_list, "elev": h_list})
314
- route_map = folium.Map(
315
- location=[ave_lat, ave_lon],
316
- zoom_start=12,
317
- )
318
  folium.PolyLine(
319
  list(zip(lat_list, lon_list)), color="red", weight=2.5, opacity=1
320
  ).add_to(route_map)
@@ -338,15 +413,62 @@ if gpx_file is not None:
338
  if row["length"] >= 1
339
  else f"{row['length']*1000:.0f} m"
340
  )
341
- popup_text = f"""Lenght: {length}<br>
342
- Avg. grade: {row['grade']/1000:.1f}%"""
 
 
 
343
  popup = folium.Popup(popup_text, min_width=100, max_width=200)
344
  folium.Marker(
345
  [row["lat"], row["lon"]],
346
- popup=popup,
347
  icon=icon_div,
348
  ).add_to(route_map)
349
- route_map.add_child(folium.CircleMarker([row["lat"], row["lon"]], radius=15))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
  st.table(
352
  df_peaks[
@@ -354,6 +476,28 @@ if gpx_file is not None:
354
  ].reset_index(drop=True)
355
  )
356
 
357
- st_data = st_folium(route_map, height=450, width=850)
358
 
359
  st.altair_chart(chart, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
 
121
  def find_climbs(df: pd.DataFrame) -> pd.DataFrame:
122
+ """Detect all valleys and peaks. Filter out climbs and
123
+ add meta data (lenght, meters climbed, average grade, climb_score, ...)
124
+ """
125
  peaks, _ = find_peaks(df["smoothed_elevation"])
126
  df_peaks = df.iloc[peaks, :].assign(base=0).assign(kind="peak")
127
  valleys, _ = find_peaks(df["smoothed_elevation"].max() - df["smoothed_elevation"])
 
187
 
188
  elevation = (
189
  alt.Chart(
190
+ df[
191
+ [
192
+ "distance_from_start",
193
+ "smoothed_elevation",
194
+ "smoothed_grade_color",
195
+ "grade",
196
+ ]
197
+ ]
198
  )
199
  .mark_bar()
200
  .encode(
 
214
  title=None,
215
  ),
216
  color=alt.Color("smoothed_grade_color").scale(None),
217
+ tooltip=[
218
+ alt.Tooltip(
219
+ "distance_from_start:Q", title="Distance (km)", format=".2f"
220
+ ),
221
+ alt.Tooltip("smoothed_elevation:Q", title="Elevation (m)", format="d"),
222
+ alt.Tooltip("grade_percent:Q", title="Grade (%)", format=".0%"),
223
+ ],
224
+ )
225
+ .transform_calculate(
226
+ grade_percent="datum.grade/100",
227
  )
228
  )
229
  max_elevation = df["elev"].max().round(-1)
 
292
  df_peaks_filtered.reset_index(drop=True)
293
  .assign(number=lambda df_: df_.index + 1)
294
  .assign(circle_pos=lambda df_: df_["max_elevation"] + 20)[
295
+ [
296
+ "distance_from_start",
297
+ "max_elevation",
298
+ "circle_pos",
299
+ "number",
300
+ "length",
301
+ "total_ascent",
302
+ "grade",
303
+ "climb_score",
304
+ "prev_distance_from_start",
305
+ ]
306
  ]
307
  )
308
  # annotation = (
 
324
  ),
325
  y="max_elevation",
326
  text="number",
327
+ tooltip=[
328
+ alt.Tooltip(
329
+ "prev_distance_from_start:Q", title="Starts at (km)", format=".2f"
330
+ ),
331
+ alt.Tooltip("total_ascent:Q", title="Total ascent (m)", format="d"),
332
+ alt.Tooltip("length:Q", title="Length (km)", format=".2f"),
333
+ alt.Tooltip("grade_percent:Q", title="Average Grade", format=".0%"),
334
+ alt.Tooltip("climb_score:Q", title="Climb score", format="d"),
335
+ ],
336
+ )
337
+ .transform_calculate(
338
+ grade_percent="datum.grade/(100*1000)",
339
+ # total_ascent_int="Math.round(datum.total_ascent)",
340
  )
341
  )
342
  chart = (
 
349
  return chart, df_peaks_filtered
350
 
351
 
352
+ def generate_climb_profile(df_hill: pd.DataFrame, title: str):
353
+ climb_profile = (
354
+ alt.Chart(
355
+ df_hill,
356
+ title=alt.Title(
357
+ title,
358
+ anchor="start",
359
+ ),
360
+ )
361
+ .mark_area()
362
+ .encode(
363
+ x=alt.X("distance_from_start")
364
+ .axis(grid=False, tickCount=10, labelExpr="datum.label + ' m'", title=None)
365
+ .scale(domain=(0, df_hill["distance_from_start"].max())),
366
+ y=alt.Y("elev").axis(
367
+ domain=False,
368
+ ticks=False,
369
+ tickCount=5,
370
+ labelExpr="datum.label + ' m'",
371
+ title=None,
372
+ ),
373
+ color=alt.Color("color_grade").scale(None),
374
+ tooltip=[
375
+ alt.Tooltip("distance_from_start:Q", title="Distance (m)", format="d"),
376
+ alt.Tooltip("elev:Q", title="Elevation (m)", format="d"),
377
+ alt.Tooltip("grade_percent:Q", title="Grade (%)", format=".0%"),
378
+ ],
379
+ )
380
+ .transform_calculate(
381
+ grade_percent="datum.grade/100",
382
+ )
383
+ )
384
+ return climb_profile
385
+
386
+
387
  gpx_file = st.file_uploader("Upload gpx file", type=["gpx"])
388
 
389
  if gpx_file is not None:
390
  ave_lat, ave_lon, lon_list, lat_list, h_list = get_gpx(gpx_file)
391
  df = pd.DataFrame({"lon": lon_list, "lat": lat_list, "elev": h_list})
392
+ route_map = folium.Map(location=[ave_lat, ave_lon], zoom_start=12, height=400)
 
 
 
393
  folium.PolyLine(
394
  list(zip(lat_list, lon_list)), color="red", weight=2.5, opacity=1
395
  ).add_to(route_map)
 
413
  if row["length"] >= 1
414
  else f"{row['length']*1000:.0f} m"
415
  )
416
+ popup_text = f"""Climb {index+1}<br>
417
+ Lenght: {length}<br>
418
+ Avg. grade: {row['grade']/1000:.1f}%<br>
419
+ Total ascend: {int(row['total_ascent'])}m
420
+ """
421
  popup = folium.Popup(popup_text, min_width=100, max_width=200)
422
  folium.Marker(
423
  [row["lat"], row["lon"]],
424
+ # popup=popup,
425
  icon=icon_div,
426
  ).add_to(route_map)
427
+
428
+ df_hill = (
429
+ df[
430
+ df["distance_from_start"].between(
431
+ row["prev_distance_from_start"],
432
+ row["distance_from_start"],
433
+ )
434
+ ]
435
+ .assign(
436
+ distance_from_start=lambda df_: (
437
+ df_["distance_from_start"] - row["prev_distance_from_start"]
438
+ )
439
+ * 1_000
440
+ )
441
+ .assign(color_grade=lambda df_: df_["grade"].map(grade_to_color))
442
+ )
443
+ # df_hill_resample = df_hill.groupby((df_hill["distance_from_start"]*1000).round(-2)).agg({"elev":"mean", "grade":"mean"}).reset_index()
444
+ # df_hill_resample["color_grade"] = df_resampled["grade"].map(grade_to_color)
445
+ title = f"Climb {index+1}: {row['length']:.2f}km {(row['grade']/100_000):.2%} {int(row['total_ascent']):d}hm"
446
+ climb_profile = generate_climb_profile(df_hill, title)
447
+ climb_profile_json = json.loads(climb_profile.to_json())
448
+
449
+ vega = folium.features.VegaLite(
450
+ climb_profile_json,
451
+ width=200,
452
+ height=200,
453
+ )
454
+ circle = folium.CircleMarker(
455
+ radius=15,
456
+ location=[row["lat"], row["lon"]],
457
+ # tooltip = label,
458
+ color="crimson",
459
+ fill=True,
460
+ )
461
+ # popup = folium.Popup()
462
+ # vega.add_to(popup)
463
+ popup.add_to(circle)
464
+ circle.add_to(route_map)
465
+ # circle_marker = folium.CircleMarker(
466
+ # [row["lat"], row["lon"]],
467
+ # radius=15,
468
+ # popup=folium.Popup(max_width=400).add_child(
469
+ # folium.VegaLite(climb_profile_json, width=400, height=400)
470
+ # ),
471
+ # )
472
 
473
  st.table(
474
  df_peaks[
 
476
  ].reset_index(drop=True)
477
  )
478
 
479
+ st_data = st_folium(route_map, height=600, width=850)
480
 
481
  st.altair_chart(chart, use_container_width=True)
482
+
483
+ for index, row in df_peaks.reset_index(drop=True).iterrows():
484
+ df_hill = (
485
+ df[
486
+ df["distance_from_start"].between(
487
+ row["prev_distance_from_start"],
488
+ row["distance_from_start"],
489
+ )
490
+ ]
491
+ .assign(
492
+ distance_from_start=lambda df_: (
493
+ df_["distance_from_start"] - row["prev_distance_from_start"]
494
+ )
495
+ * 1_000
496
+ )
497
+ .assign(color_grade=lambda df_: df_["grade"].map(grade_to_color))
498
+ )
499
+ # df_hill_resample = df_hill.groupby((df_hill["distance_from_start"]*1000).round(-2)).agg({"elev":"mean", "grade":"mean"}).reset_index()
500
+ # df_hill_resample["color_grade"] = df_resampled["grade"].map(grade_to_color)
501
+ title = f"Climb {index+1}: {row['length']:.2f}km {(row['grade']/100_000):.2%} {int(row['total_ascent']):d}hm"
502
+ climb_profile = generate_climb_profile(df_hill, title)
503
+ st.altair_chart(climb_profile, use_container_width=True)