geo-activity-playground 0.37.0__tar.gz → 0.38.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/PKG-INFO +6 -6
  2. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/activities.py +12 -0
  3. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/config.py +6 -2
  4. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/enrichment.py +9 -0
  5. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/meta_search.py +49 -9
  6. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/summary_stats.py +1 -1
  7. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/test_meta_search.py +9 -0
  8. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/controller.py +20 -0
  9. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -10
  10. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -0
  11. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +17 -0
  12. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/app.py +18 -5
  13. geo_activity_playground-0.38.1/geo_activity_playground/webui/eddington_blueprint.py +190 -0
  14. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/equipment_blueprint.py +12 -3
  15. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/explorer/blueprint.py +4 -0
  16. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/heatmap/blueprint.py +3 -0
  17. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
  18. geo_activity_playground-0.38.1/geo_activity_playground/webui/search_blueprint.py +70 -0
  19. geo_activity_playground-0.38.1/geo_activity_playground/webui/search_util.py +64 -0
  20. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/summary_blueprint.py +31 -34
  21. geo_activity_playground-0.38.1/geo_activity_playground/webui/templates/eddington/index.html.j2 +128 -0
  22. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/search/index.html.j2 +8 -4
  23. geo_activity_playground-0.38.1/geo_activity_playground/webui/templates/search_form.html.j2 +116 -0
  24. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/summary/index.html.j2 +1 -1
  25. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/pyproject.toml +9 -9
  26. geo_activity_playground-0.37.0/geo_activity_playground/webui/eddington_blueprint.py +0 -86
  27. geo_activity_playground-0.37.0/geo_activity_playground/webui/search_blueprint.py +0 -38
  28. geo_activity_playground-0.37.0/geo_activity_playground/webui/search_util.py +0 -31
  29. geo_activity_playground-0.37.0/geo_activity_playground/webui/templates/eddington/index.html.j2 +0 -61
  30. geo_activity_playground-0.37.0/geo_activity_playground/webui/templates/search_form.html.j2 +0 -82
  31. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/LICENSE +0 -0
  32. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/__init__.py +0 -0
  33. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/__main__.py +0 -0
  34. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/__init__.py +0 -0
  35. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/coordinates.py +0 -0
  36. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/heart_rate.py +0 -0
  37. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/paths.py +0 -0
  38. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/privacy_zones.py +0 -0
  39. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/raster_map.py +0 -0
  40. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/similarity.py +0 -0
  41. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/tasks.py +0 -0
  42. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/test_summary_stats.py +0 -0
  43. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/test_tiles.py +0 -0
  44. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/test_time_conversion.py +0 -0
  45. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/tiles.py +0 -0
  46. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/core/time_conversion.py +0 -0
  47. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/explorer/__init__.py +0 -0
  48. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/explorer/grid_file.py +0 -0
  49. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/explorer/tile_visits.py +0 -0
  50. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/explorer/video.py +0 -0
  51. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/heatmap_video.py +0 -0
  52. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/__init__.py +0 -0
  53. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/activity_parsers.py +0 -0
  54. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/csv_parser.py +0 -0
  55. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/directory.py +0 -0
  56. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/strava_api.py +0 -0
  57. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/strava_checkout.py +0 -0
  58. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  59. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/test_directory.py +0 -0
  60. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
  61. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/__init__.py +0 -0
  62. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/__init__.py +0 -0
  63. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  64. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -0
  65. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  66. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/auth_blueprint.py +0 -0
  67. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/authenticator.py +0 -0
  68. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  69. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  70. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/calendar/controller.py +0 -0
  71. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  72. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  73. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/entry_controller.py +0 -0
  74. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  75. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/explorer/controller.py +0 -0
  76. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  77. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  78. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -0
  79. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/plot_util.py +0 -0
  80. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/blueprint.py +0 -0
  81. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/controller.py +0 -0
  82. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  83. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
  84. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  85. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  86. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +0 -0
  87. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -0
  88. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  89. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  90. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  91. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +0 -0
  92. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  93. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  94. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/square_planner_blueprint.py +0 -0
  95. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +0 -0
  96. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/MarkerCluster.Default.css +0 -0
  97. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/MarkerCluster.css +0 -0
  98. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  99. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  100. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  101. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  102. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/bootstrap.bundle.min.js +0 -0
  103. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/bootstrap.min.css +0 -0
  104. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  105. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  106. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  107. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  108. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
  109. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/favicon.svg +0 -0
  110. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/fullscreen.png +0 -0
  111. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  112. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/leaflet.css +0 -0
  113. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/leaflet.fullscreen.css +0 -0
  114. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/leaflet.js +0 -0
  115. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/leaflet.markercluster.js +0 -0
  116. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  117. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  118. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/table-sort.min.js +0 -0
  119. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/vega-embed@6 +0 -0
  120. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/vega-lite@4 +0 -0
  121. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/vega@5 +0 -0
  122. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  123. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  124. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/auth/index.html.j2 +0 -0
  125. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/equipment/index.html.j2 +0 -0
  126. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/home.html.j2 +0 -0
  127. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  128. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/square_planner/index.html.j2 +0 -0
  129. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/upload/index.html.j2 +0 -0
  130. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/templates/upload/reload.html.j2 +0 -0
  131. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/tile_blueprint.py +0 -0
  132. {geo_activity_playground-0.37.0 → geo_activity_playground-0.38.1}/geo_activity_playground/webui/upload_blueprint.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.37.0
3
+ Version: 0.38.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: Pillow (>=11.0.0,<12.0.0)
15
- Requires-Dist: altair (>=5.1.2,<6.0.0)
15
+ Requires-Dist: altair (>=5.5.0,<6.0.0)
16
16
  Requires-Dist: appdirs (>=1.4.4,<2.0.0)
17
17
  Requires-Dist: charset-normalizer (>=3.3.2,<4.0.0)
18
18
  Requires-Dist: coloredlogs (>=15.0.1,<16.0.0)
@@ -21,10 +21,10 @@ Requires-Dist: flask (>=3.0.0,<4.0.0)
21
21
  Requires-Dist: geojson (>=3.0.1,<4.0.0)
22
22
  Requires-Dist: gpxpy (>=1.5.0,<2.0.0)
23
23
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
24
- Requires-Dist: matplotlib (>=3.6.3,<4.0.0)
25
- Requires-Dist: numpy (>=2.1.3,<3.0.0)
26
- Requires-Dist: pandas (>=2.2,<3.0)
27
- Requires-Dist: pyarrow (>=18.1.0,<19.0.0)
24
+ Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
25
+ Requires-Dist: numpy (>=2.2.3,<3.0.0)
26
+ Requires-Dist: pandas (>=2.2.3,<3.0.0)
27
+ Requires-Dist: pyarrow (>=19.0.1,<20.0.0)
28
28
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
29
29
  Requires-Dist: requests (>=2.28.1,<3.0.0)
30
30
  Requires-Dist: shapely (>=2.0.5,<3.0.0)
@@ -24,11 +24,14 @@ logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
26
  class ActivityMeta(TypedDict):
27
+ average_speed_elapsed_kmh: float
28
+ average_speed_moving_kmh: float
27
29
  calories: float
28
30
  commute: bool
29
31
  consider_for_achievements: bool
30
32
  distance_km: float
31
33
  elapsed_time: datetime.timedelta
34
+ elevation_gain: float
32
35
  end_latitude: float
33
36
  end_longitude: float
34
37
  equipment: str
@@ -110,6 +113,15 @@ def build_activity_meta() -> None:
110
113
 
111
114
  meta.sort_values("start", inplace=True)
112
115
 
116
+ meta.loc[meta["kind"] == "", "kind"] = "Unknown"
117
+ meta.loc[meta["equipment"] == "", "equipment"] = "Unknown"
118
+ meta["average_speed_moving_kmh"] = meta["distance_km"] / (
119
+ meta["moving_time"].dt.total_seconds() / 3_600
120
+ )
121
+ meta["average_speed_elapsed_kmh"] = meta["distance_km"] / (
122
+ meta["elapsed_time"].dt.total_seconds() / 3_600
123
+ )
124
+
113
125
  meta.to_parquet(activities_file())
114
126
 
115
127
 
@@ -45,7 +45,12 @@ class Config:
45
45
  time_diff_threshold_seconds: Optional[int] = 30
46
46
  upload_password: Optional[str] = None
47
47
  map_tile_url: str = "https://tile.openstreetmap.org/{zoom}/{x}/{y}.png"
48
- map_tile_attribution: str = '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | <a href="https://www.openstreetmap.org/fixthemap">Correct Map</a>'
48
+ map_tile_attribution: str = (
49
+ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | <a href="https://www.openstreetmap.org/fixthemap">Correct Map</a>'
50
+ )
51
+ search_queries_favorites: list[dict] = dataclasses.field(default_factory=list)
52
+ search_queries_last: list[dict] = dataclasses.field(default_factory=list)
53
+ search_queries_num_keep: int = 10
49
54
 
50
55
 
51
56
  class ConfigAccessor:
@@ -60,7 +65,6 @@ class ConfigAccessor:
60
65
  return self._config
61
66
 
62
67
  def save(self) -> None:
63
- print(self._config)
64
68
  with open(new_config_file(), "w") as f:
65
69
  json.dump(
66
70
  dataclasses.asdict(self._config),
@@ -119,6 +119,8 @@ def _get_metadata_from_timeseries(timeseries: pd.DataFrame) -> ActivityMeta:
119
119
  metadata["end_latitude"] = timeseries["latitude"].iloc[-1]
120
120
  metadata["start_longitude"] = timeseries["longitude"].iloc[0]
121
121
  metadata["end_longitude"] = timeseries["longitude"].iloc[-1]
122
+ if "elevation_gain_cum" in timeseries.columns:
123
+ metadata["elevation_gain"] = timeseries["elevation_gain_cum"].iloc[-1]
122
124
 
123
125
  return metadata
124
126
 
@@ -191,4 +193,11 @@ def _embellish_single_time_series(
191
193
  timeseries["x"] = x
192
194
  timeseries["y"] = y
193
195
 
196
+ if "altitude" in timeseries.columns:
197
+ altitude_diff = timeseries["altitude"].diff()
198
+ altitude_diff = altitude_diff.ewm(span=5, min_periods=5).mean()
199
+ altitude_diff.loc[altitude_diff.abs() > 30] = 0
200
+ altitude_diff.loc[altitude_diff < 0] = 0
201
+ timeseries["elevation_gain_cum"] = altitude_diff.cumsum()
202
+
194
203
  return timeseries
@@ -4,6 +4,7 @@ import re
4
4
  import urllib.parse
5
5
  from typing import Optional
6
6
 
7
+ import dateutil.parser
7
8
  import numpy as np
8
9
  import pandas as pd
9
10
 
@@ -17,6 +18,23 @@ class SearchQuery:
17
18
  start_begin: Optional[datetime.date] = None
18
19
  start_end: Optional[datetime.date] = None
19
20
 
21
+ def __str__(self) -> str:
22
+ bits = []
23
+ if self.name:
24
+ bits.append(f"name is “{self.name}”")
25
+ if self.equipment:
26
+ bits.append(
27
+ "equipment is "
28
+ + (" or ".join(f"“{equipment}”" for equipment in self.equipment))
29
+ )
30
+ if self.kind:
31
+ bits.append("kind is " + (" or ".join(f"“{kind}”" for kind in self.kind)))
32
+ if self.start_begin:
33
+ bits.append(f"after “{self.start_begin.isoformat()}”")
34
+ if self.start_end:
35
+ bits.append(f"until “{self.start_end.isoformat()}”")
36
+ return " and ".join(bits)
37
+
20
38
  @property
21
39
  def active(self) -> bool:
22
40
  return (
@@ -27,7 +45,7 @@ class SearchQuery:
27
45
  or self.start_end
28
46
  )
29
47
 
30
- def to_jinja(self) -> dict:
48
+ def to_primitives(self) -> dict:
31
49
  return {
32
50
  "equipment": self.equipment,
33
51
  "kind": self.kind,
@@ -35,9 +53,24 @@ class SearchQuery:
35
53
  "name_case_sensitive": self.name_case_sensitive,
36
54
  "start_begin": _format_optional_date(self.start_begin),
37
55
  "start_end": _format_optional_date(self.start_end),
38
- "active": self.active,
39
56
  }
40
57
 
58
+ @classmethod
59
+ def from_primitives(cls, d: dict) -> "SearchQuery":
60
+ return cls(
61
+ equipment=d.get("equipment", []),
62
+ kind=d.get("kind", []),
63
+ name=d.get("name", None),
64
+ name_case_sensitive=d.get("name_case_sensitive", False),
65
+ start_begin=_parse_date_or_none(d.get("start_begin", None)),
66
+ start_end=_parse_date_or_none(d.get("start_end", None)),
67
+ )
68
+
69
+ def to_jinja(self) -> dict:
70
+ result = self.to_primitives()
71
+ result["active"] = self.active
72
+ return result
73
+
41
74
  def to_url_str(self) -> str:
42
75
  variables = []
43
76
  for equipment in self.equipment:
@@ -58,13 +91,6 @@ class SearchQuery:
58
91
  )
59
92
 
60
93
 
61
- def _format_optional_date(date: Optional[datetime.date]) -> str:
62
- if date is None:
63
- return ""
64
- else:
65
- return date.isoformat()
66
-
67
-
68
94
  def apply_search_query(
69
95
  activity_meta: pd.DataFrame, search_query: SearchQuery
70
96
  ) -> pd.DataFrame:
@@ -100,6 +126,13 @@ def apply_search_query(
100
126
  return activity_meta.loc[mask]
101
127
 
102
128
 
129
+ def _format_optional_date(date: Optional[datetime.date]) -> str:
130
+ if date is None:
131
+ return ""
132
+ else:
133
+ return date.isoformat()
134
+
135
+
103
136
  def _make_mask(
104
137
  index: pd.Index,
105
138
  default: bool,
@@ -115,3 +148,10 @@ def _filter_column(column: pd.Series, values: list):
115
148
  for equipment in values:
116
149
  sub_mask |= column == equipment
117
150
  return sub_mask
151
+
152
+
153
+ def _parse_date_or_none(s: Optional[str]) -> Optional[datetime.date]:
154
+ if not s:
155
+ return None
156
+ else:
157
+ return dateutil.parser.parse(s).date()
@@ -27,4 +27,4 @@ def get_equipment_use_table(
27
27
  result["first_use"] = [date.date().isoformat() for date in result["first_use"]]
28
28
  result["last_use"] = [date.date().isoformat() for date in result["last_use"]]
29
29
 
30
- return result.reset_index().to_dict(orient="records")
30
+ return result.reset_index()
@@ -89,3 +89,12 @@ def test_make_mask() -> None:
89
89
  index = [1, 2]
90
90
  assert (_make_mask(index, True) == pd.Series([True, True], index=index)).all()
91
91
  assert (_make_mask(index, False) == pd.Series([False, False], index=index)).all()
92
+
93
+
94
+ def test_search_query_from_primitives() -> None:
95
+ search_query = SearchQuery.from_primitives(
96
+ {"start_end": "2025-01-04", "equipment": ["A", "B"]}
97
+ )
98
+ assert search_query.start_end == datetime.date(2025, 1, 4)
99
+ assert search_query.equipment == ["A", "B"]
100
+ assert search_query.kind == []
@@ -108,6 +108,8 @@ class ActivityController:
108
108
  result["heart_zones_plot"] = heart_rate_zone_plot(heart_zones)
109
109
  if "altitude" in time_series.columns:
110
110
  result["altitude_time_plot"] = altitude_time_plot(time_series)
111
+ if "elevation_gain_cum" in time_series.columns:
112
+ result["elevation_gain_cum_plot"] = elevation_gain_cum_plot(time_series)
111
113
  if "heartrate" in time_series.columns:
112
114
  result["heartrate_time_plot"] = heart_rate_time_plot(time_series)
113
115
  if "cadence" in time_series.columns:
@@ -323,6 +325,24 @@ def altitude_time_plot(time_series: pd.DataFrame) -> str:
323
325
  )
324
326
 
325
327
 
328
+ def elevation_gain_cum_plot(time_series: pd.DataFrame) -> str:
329
+ return (
330
+ alt.Chart(time_series, title="Altitude Gain")
331
+ .mark_line()
332
+ .encode(
333
+ alt.X("time", title="Time"),
334
+ alt.Y(
335
+ "elevation_gain_cum",
336
+ scale=alt.Scale(zero=False),
337
+ title="Altitude gain / m",
338
+ ),
339
+ alt.Color("segment_id:N", title="Segment"),
340
+ )
341
+ .interactive(bind_y=False)
342
+ .to_json(format="vega")
343
+ )
344
+
345
+
326
346
  def heart_rate_time_plot(time_series: pd.DataFrame) -> str:
327
347
  return (
328
348
  alt.Chart(time_series, title="Heart Rate")
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
  <div class="row mb-3">
12
- <div class="col-md-9">
12
+ <div class="col-12">
13
13
  <div id="activity-map" style="height: 500px;"></div>
14
14
  <script>
15
15
  var map = L.map('activity-map', {
@@ -26,15 +26,6 @@
26
26
  map.fitBounds(geojson.getBounds());
27
27
  </script>
28
28
  </div>
29
- <div class="col-md-3">
30
- <ol>
31
- {% for activity in activities %}
32
- <li><span style="color: {{ activity['color'] }};">█</span> <a
33
- href="{{ url_for('.show', id=activity.id) }}">{{
34
- activity.name }}</a></li>
35
- {% endfor %}
36
- </ol>
37
- </div>
38
29
  </div>
39
30
 
40
31
  <div class="row mb-3">
@@ -48,6 +39,7 @@
48
39
  <th>Date</th>
49
40
  <th>Distance / km</th>
50
41
  <th>Elapsed time</th>
42
+ <th>Speed / km/h</th>
51
43
  <th>Equipment</th>
52
44
  <th>Kind</th>
53
45
  </tr>
@@ -61,6 +53,7 @@
61
53
  <td>{{ activity.start|dt }}</td>
62
54
  <td>{{ activity.distance_km | round(1) }}</td>
63
55
  <td>{{ activity.elapsed_time|td }}</td>
56
+ <td>{{ activity.average_speed_moving_kmh|round(1) }}</td>
64
57
  <td>{{ activity["equipment"] }}</td>
65
58
  <td>{{ activity["kind"] }}</td>
66
59
  </tr>
@@ -57,6 +57,7 @@
57
57
  <th>Date</th>
58
58
  <th class="numeric-sort">Distance / km</th>
59
59
  <th>Elapsed time</th>
60
+ <th>Speed / km/h</th>
60
61
  <th>Equipment</th>
61
62
  <th>Kind</th>
62
63
  </tr>
@@ -70,6 +71,7 @@
70
71
  <td>{{ activity.start|dt }}</td>
71
72
  <td>{{ activity.distance_km | round(1) }}</td>
72
73
  <td>{{ activity.elapsed_time|td }}</td>
74
+ <td>{{ activity.average_speed_moving_kmh|round(1) }}</td>
73
75
  <td>{{ activity["equipment"] }}</td>
74
76
  <td>{{ activity["kind"] }}</td>
75
77
  </tr>
@@ -22,6 +22,12 @@
22
22
  <dd>{{ activity.elapsed_time|td }}</dd>
23
23
  <dt>Moving time</dt>
24
24
  <dd>{{ activity.moving_time|td }}</dd>
25
+ <dt>Average moving speed</dt>
26
+ <dd>{{ activity.average_speed_moving_kmh|round(1) }} km/h = {{
27
+ (60/activity.average_speed_moving_kmh)|round(1) }} min/km</dd>
28
+ <dt>Average elapsed speed</dt>
29
+ <dd>{{ activity.average_speed_elapsed_kmh|round(1) }} km/h = {{
30
+ (60/activity.average_speed_elapsed_kmh)|round(1) }} min/km</dd>
25
31
  <dt>Start time</dt>
26
32
  <dd><a href="{{ url_for('activity.day', year=date.year, month=date.month, day=date.day) }}">{{ date }}</a>
27
33
  {{ time }}
@@ -30,6 +36,10 @@
30
36
  <dd>{{ activity.calories }}</dd>
31
37
  <dt>Steps</dt>
32
38
  <dd>{{ activity.steps }}</dd>
39
+ {% if activity.elevation_gain is defined %}
40
+ <dt>Elevation gain</dt>
41
+ <dd>{{ activity.elevation_gain|round(0) }} m</dd>
42
+ {% endif %}
33
43
  <dt>Equipment</dt>
34
44
  <dd>{{ activity['equipment'] }}</dd>
35
45
  <dt>New Explorer Tiles</dt>
@@ -100,6 +110,7 @@
100
110
  </div>
101
111
  </div>
102
112
 
113
+ {% if altitude_time_plot is defined %}
103
114
  <div class="row mb-3">
104
115
  <div class="col">
105
116
  <h2>Altitude</h2>
@@ -110,7 +121,13 @@
110
121
  <div class="col-md-4">
111
122
  {{ vega_direct("altitude_time_plot", altitude_time_plot) }}
112
123
  </div>
124
+ {% if elevation_gain_cum_plot is defined %}
125
+ <div class="col-md-4">
126
+ {{ vega_direct("elevation_gain_cum_plot", elevation_gain_cum_plot) }}
127
+ </div>
128
+ {% endif %}
113
129
  </div>
130
+ {% endif %}
114
131
 
115
132
  {% if heartrate_time_plot is defined %}
116
133
  <h2>Heart rate</h2>
@@ -8,6 +8,7 @@ import urllib.parse
8
8
 
9
9
  from flask import Flask
10
10
  from flask import render_template
11
+ from flask import request
11
12
 
12
13
  from ..core.activities import ActivityRepository
13
14
  from ..core.config import Config
@@ -31,6 +32,7 @@ from .square_planner_blueprint import make_square_planner_blueprint
31
32
  from .summary_blueprint import make_summary_blueprint
32
33
  from .tile_blueprint import make_tile_blueprint
33
34
  from .upload_blueprint import make_upload_blueprint
35
+ from geo_activity_playground.webui.search_util import SearchQueryHistory
34
36
 
35
37
 
36
38
  def route_start(app: Flask, repository: ActivityRepository, config: Config) -> None:
@@ -79,6 +81,7 @@ def web_ui_main(
79
81
  return f"{h}:{m:02d}:{s:02d}"
80
82
 
81
83
  authenticator = Authenticator(config_accessor())
84
+ search_query_history = SearchQueryHistory(config_accessor, authenticator)
82
85
 
83
86
  config = config_accessor()
84
87
  activity_controller = ActivityController(repository, tile_visit_accessor, config)
@@ -98,16 +101,20 @@ def web_ui_main(
98
101
  make_calendar_blueprint(calendar_controller), url_prefix="/calendar"
99
102
  )
100
103
  app.register_blueprint(
101
- make_eddington_blueprint(repository), url_prefix="/eddington"
104
+ make_eddington_blueprint(repository, search_query_history),
105
+ url_prefix="/eddington",
102
106
  )
103
107
  app.register_blueprint(
104
108
  make_equipment_blueprint(repository, config), url_prefix="/equipment"
105
109
  )
106
110
  app.register_blueprint(
107
- make_explorer_blueprint(explorer_controller), url_prefix="/explorer"
111
+ make_explorer_blueprint(explorer_controller, authenticator),
112
+ url_prefix="/explorer",
108
113
  )
109
114
  app.register_blueprint(
110
- make_heatmap_blueprint(repository, tile_visit_accessor, config_accessor()),
115
+ make_heatmap_blueprint(
116
+ repository, tile_visit_accessor, config_accessor(), search_query_history
117
+ ),
111
118
  url_prefix="/heatmap",
112
119
  )
113
120
  app.register_blueprint(
@@ -119,11 +126,14 @@ def web_ui_main(
119
126
  url_prefix="/square-planner",
120
127
  )
121
128
  app.register_blueprint(
122
- make_search_blueprint(repository),
129
+ make_search_blueprint(
130
+ repository, search_query_history, authenticator, config_accessor
131
+ ),
123
132
  url_prefix="/search",
124
133
  )
125
134
  app.register_blueprint(
126
- make_summary_blueprint(repository, config), url_prefix="/summary"
135
+ make_summary_blueprint(repository, config, search_query_history),
136
+ url_prefix="/summary",
127
137
  )
128
138
 
129
139
  app.register_blueprint(make_tile_blueprint(config), url_prefix="/tile")
@@ -148,6 +158,9 @@ def web_ui_main(
148
158
  "version": _try_get_version(),
149
159
  "num_activities": len(repository),
150
160
  "map_tile_attribution": config_accessor().map_tile_attribution,
161
+ "search_query_favorites": search_query_history.prepare_favorites(),
162
+ "search_query_last": search_query_history.prepare_last(),
163
+ "request_url": urllib.parse.quote_plus(request.url),
151
164
  }
152
165
  if len(repository):
153
166
  variables["equipments_avail"] = sorted(
@@ -0,0 +1,190 @@
1
+ import datetime
2
+
3
+ import altair as alt
4
+ import numpy as np
5
+ import pandas as pd
6
+ from flask import Blueprint
7
+ from flask import render_template
8
+ from flask import request
9
+
10
+ from geo_activity_playground.core.activities import ActivityRepository
11
+ from geo_activity_playground.core.meta_search import apply_search_query
12
+ from geo_activity_playground.webui.search_util import search_query_from_form
13
+ from geo_activity_playground.webui.search_util import SearchQueryHistory
14
+
15
+
16
+ def make_eddington_blueprint(
17
+ repository: ActivityRepository, search_query_history: SearchQueryHistory
18
+ ) -> Blueprint:
19
+ blueprint = Blueprint("eddington", __name__, template_folder="templates")
20
+
21
+ @blueprint.route("/")
22
+ def index():
23
+ query = search_query_from_form(request.args)
24
+ search_query_history.register_query(query)
25
+ activities = (
26
+ apply_search_query(repository.meta, query)
27
+ .dropna(subset=["start", "distance_km"])
28
+ .copy()
29
+ )
30
+
31
+ activities["year"] = [start.year for start in activities["start"]]
32
+ activities["date"] = [start.date() for start in activities["start"]]
33
+ activities["isoyear"] = [
34
+ start.isocalendar().year for start in activities["start"]
35
+ ]
36
+ activities["isoweek"] = [
37
+ start.isocalendar().week for start in activities["start"]
38
+ ]
39
+
40
+ en_per_day, eddington_df_per_day = _get_distances_per_group(
41
+ activities.groupby("date")
42
+ )
43
+ en_per_week, eddington_df_per_week = _get_distances_per_group(
44
+ activities.groupby(["isoyear", "isoweek"])
45
+ )
46
+
47
+ return render_template(
48
+ "eddington/index.html.j2",
49
+ eddington_number=en_per_day,
50
+ logarithmic_plot=_make_eddington_plot(
51
+ eddington_df_per_day, en_per_day, "Days"
52
+ ),
53
+ eddington_per_week=en_per_week,
54
+ eddington_per_week_plot=_make_eddington_plot(
55
+ eddington_df_per_week, en_per_week, "Weeks"
56
+ ),
57
+ eddington_table=eddington_df_per_day.loc[
58
+ (eddington_df_per_day["distance_km"] > en_per_day)
59
+ & (eddington_df_per_day["distance_km"] <= en_per_day + 10)
60
+ ].to_dict(orient="records"),
61
+ eddington_table_weeks=eddington_df_per_week.loc[
62
+ (eddington_df_per_week["distance_km"] > en_per_week)
63
+ & (eddington_df_per_week["distance_km"] <= en_per_week + 10)
64
+ ].to_dict(orient="records"),
65
+ query=query.to_jinja(),
66
+ yearly_eddington=_get_yearly_eddington(activities),
67
+ eddington_number_history_plot=_get_eddington_number_history(activities),
68
+ )
69
+
70
+ return blueprint
71
+
72
+
73
+ def _get_distances_per_group(grouped) -> tuple[int, pd.DataFrame]:
74
+ sum_per_group = grouped.apply(
75
+ lambda group: int(sum(group["distance_km"])), include_groups=False
76
+ )
77
+ counts = dict(zip(*np.unique(sorted(sum_per_group), return_counts=True)))
78
+ eddington = pd.DataFrame(
79
+ {"distance_km": d, "count": counts.get(d, 0)}
80
+ for d in range(max(counts.keys()) + 1)
81
+ )
82
+ eddington["total"] = eddington["count"][::-1].cumsum()[::-1]
83
+ en = eddington.loc[eddington["total"] >= eddington["distance_km"]][
84
+ "distance_km"
85
+ ].iloc[-1]
86
+ eddington["missing"] = eddington["distance_km"] - eddington["total"]
87
+ return en, eddington
88
+
89
+
90
+ def _make_eddington_plot(eddington_df: pd.DataFrame, en: int, interval: str) -> dict:
91
+ x = list(range(1, max(eddington_df["distance_km"]) + 1))
92
+ return (
93
+ (
94
+ (
95
+ alt.Chart(
96
+ eddington_df,
97
+ height=500,
98
+ width=800,
99
+ title=f"Eddington Number {en}",
100
+ )
101
+ .mark_area(interpolate="step")
102
+ .encode(
103
+ alt.X(
104
+ "distance_km",
105
+ scale=alt.Scale(domainMin=0),
106
+ title="Distance / km",
107
+ ),
108
+ alt.Y(
109
+ "total",
110
+ scale=alt.Scale(domainMax=en + 10),
111
+ title=f"{interval} exceeding distance",
112
+ ),
113
+ [
114
+ alt.Tooltip("distance_km", title="Distance / km"),
115
+ alt.Tooltip("total", title=f"{interval} exceeding distance"),
116
+ alt.Tooltip("missing", title=f"{interval} missing for next"),
117
+ ],
118
+ )
119
+ )
120
+ + (
121
+ alt.Chart(pd.DataFrame({"distance_km": x, "total": x}))
122
+ .mark_line(color="red")
123
+ .encode(alt.X("distance_km"), alt.Y("total"))
124
+ )
125
+ )
126
+ .interactive(bind_x=False)
127
+ .to_json(format="vega")
128
+ )
129
+
130
+
131
+ def _get_eddington_number(distances: pd.Series) -> int:
132
+ if len(distances) == 1:
133
+ if distances.iloc[0] >= 1:
134
+ return 1
135
+ else:
136
+ 0
137
+
138
+ sorted_distances = sorted(distances, reverse=True)
139
+ for en, distance in enumerate(sorted_distances, 1):
140
+ if distance < en:
141
+ return en - 1
142
+
143
+
144
+ def _get_yearly_eddington(meta: pd.DataFrame) -> dict[int, int]:
145
+ meta = meta.dropna(subset=["start", "distance_km"]).copy()
146
+ meta["year"] = [start.year for start in meta["start"]]
147
+ meta["date"] = [start.date() for start in meta["start"]]
148
+
149
+ yearly_eddington = meta.groupby("year").apply(
150
+ lambda group: _get_eddington_number(
151
+ group.groupby("date").apply(
152
+ lambda group2: int(group2["distance_km"].sum()), include_groups=False
153
+ )
154
+ ),
155
+ include_groups=False,
156
+ )
157
+ return yearly_eddington.to_dict()
158
+
159
+
160
+ def _get_eddington_number_history(meta: pd.DataFrame) -> dict:
161
+
162
+ daily_distances = meta.groupby("date").apply(
163
+ lambda group2: int(group2["distance_km"].sum()), include_groups=False
164
+ )
165
+
166
+ eddington_number_history = {"date": [], "eddington_number": []}
167
+ top_days = []
168
+ for date, distance in daily_distances.items():
169
+ if len(top_days) == 0:
170
+ top_days.append(distance)
171
+ else:
172
+ if distance >= top_days[0]:
173
+ top_days.append(distance)
174
+ top_days.sort()
175
+ while top_days[0] < len(top_days):
176
+ top_days.pop(0)
177
+ eddington_number_history["date"].append(
178
+ datetime.datetime.combine(date, datetime.datetime.min.time())
179
+ )
180
+ eddington_number_history["eddington_number"].append(len(top_days))
181
+ history = pd.DataFrame(eddington_number_history)
182
+
183
+ return (
184
+ alt.Chart(history)
185
+ .mark_line(interpolate="step-after")
186
+ .encode(
187
+ alt.X("date", title="Date"),
188
+ alt.Y("eddington_number", title="Eddington number"),
189
+ )
190
+ ).to_json(format="vega")