geo-activity-playground 0.36.2__tar.gz → 0.38.0__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 (134) hide show
  1. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/activities.py +12 -0
  3. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/config.py +6 -2
  4. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/enrichment.py +9 -0
  5. geo_activity_playground-0.38.0/geo_activity_playground/core/meta_search.py +157 -0
  6. geo_activity_playground-0.38.0/geo_activity_playground/core/summary_stats.py +30 -0
  7. geo_activity_playground-0.38.0/geo_activity_playground/core/test_meta_search.py +100 -0
  8. geo_activity_playground-0.38.0/geo_activity_playground/core/test_summary_stats.py +108 -0
  9. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/controller.py +20 -0
  10. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -10
  11. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -0
  12. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +17 -0
  13. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/app.py +27 -10
  14. geo_activity_playground-0.38.0/geo_activity_playground/webui/eddington_blueprint.py +190 -0
  15. geo_activity_playground-0.36.2/geo_activity_playground/webui/equipment/controller.py → geo_activity_playground-0.38.0/geo_activity_playground/webui/equipment_blueprint.py +29 -42
  16. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/explorer/blueprint.py +4 -0
  17. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/heatmap/blueprint.py +10 -29
  18. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +45 -103
  19. geo_activity_playground-0.38.0/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +42 -0
  20. geo_activity_playground-0.38.0/geo_activity_playground/webui/search_blueprint.py +70 -0
  21. geo_activity_playground-0.38.0/geo_activity_playground/webui/search_util.py +64 -0
  22. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/summary_blueprint.py +40 -39
  23. geo_activity_playground-0.38.0/geo_activity_playground/webui/templates/eddington/index.html.j2 +128 -0
  24. {geo_activity_playground-0.36.2/geo_activity_playground/webui/equipment → geo_activity_playground-0.38.0/geo_activity_playground/webui}/templates/equipment/index.html.j2 +3 -5
  25. geo_activity_playground-0.38.0/geo_activity_playground/webui/templates/search/index.html.j2 +42 -0
  26. geo_activity_playground-0.38.0/geo_activity_playground/webui/templates/search_form.html.j2 +116 -0
  27. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/summary/index.html.j2 +5 -1
  28. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/pyproject.toml +4 -4
  29. geo_activity_playground-0.36.2/geo_activity_playground/webui/eddington_blueprint.py +0 -81
  30. geo_activity_playground-0.36.2/geo_activity_playground/webui/equipment/blueprint.py +0 -16
  31. geo_activity_playground-0.36.2/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  32. geo_activity_playground-0.36.2/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -74
  33. geo_activity_playground-0.36.2/geo_activity_playground/webui/search_blueprint.py +0 -101
  34. geo_activity_playground-0.36.2/geo_activity_playground/webui/templates/eddington/index.html.j2 +0 -56
  35. geo_activity_playground-0.36.2/geo_activity_playground/webui/templates/search/index.html.j2 +0 -95
  36. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/LICENSE +0 -0
  37. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/__init__.py +0 -0
  38. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/__main__.py +0 -0
  39. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/__init__.py +0 -0
  40. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/coordinates.py +0 -0
  41. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/heart_rate.py +0 -0
  42. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/paths.py +0 -0
  43. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  44. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/raster_map.py +0 -0
  45. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/similarity.py +0 -0
  46. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/tasks.py +0 -0
  47. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/test_tiles.py +0 -0
  48. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
  49. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/tiles.py +0 -0
  50. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/core/time_conversion.py +0 -0
  51. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/explorer/__init__.py +0 -0
  52. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  53. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
  54. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/explorer/video.py +0 -0
  55. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/heatmap_video.py +0 -0
  56. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/__init__.py +0 -0
  57. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
  58. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  59. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/directory.py +0 -0
  60. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/strava_api.py +0 -0
  61. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  62. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  63. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/test_directory.py +0 -0
  64. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  65. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/__init__.py +0 -0
  66. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
  67. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  68. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -0
  69. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  70. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/auth_blueprint.py +0 -0
  71. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/authenticator.py +0 -0
  72. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  73. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  74. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
  75. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  76. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  77. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  78. {geo_activity_playground-0.36.2/geo_activity_playground/webui/equipment → geo_activity_playground-0.38.0/geo_activity_playground/webui/explorer}/__init__.py +0 -0
  79. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
  80. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  81. {geo_activity_playground-0.36.2/geo_activity_playground/webui/explorer → geo_activity_playground-0.38.0/geo_activity_playground/webui/heatmap}/__init__.py +0 -0
  82. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/plot_util.py +0 -0
  83. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/blueprint.py +0 -0
  84. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/controller.py +0 -0
  85. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  86. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
  87. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  88. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  89. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +0 -0
  90. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -0
  91. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  92. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  93. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  94. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +0 -0
  95. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  96. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  97. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/square_planner_blueprint.py +0 -0
  98. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +0 -0
  99. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/MarkerCluster.Default.css +0 -0
  100. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/MarkerCluster.css +0 -0
  101. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  102. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  103. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  104. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  105. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/bootstrap.bundle.min.js +0 -0
  106. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/bootstrap.min.css +0 -0
  107. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  108. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  109. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  110. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  111. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  112. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/favicon.svg +0 -0
  113. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/fullscreen.png +0 -0
  114. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  115. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/leaflet.css +0 -0
  116. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/leaflet.fullscreen.css +0 -0
  117. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/leaflet.js +0 -0
  118. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/leaflet.markercluster.js +0 -0
  119. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  120. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  121. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/table-sort.min.js +0 -0
  122. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/vega-embed@6 +0 -0
  123. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/vega-lite@4 +0 -0
  124. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/vega@5 +0 -0
  125. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  126. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  127. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/auth/index.html.j2 +0 -0
  128. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/home.html.j2 +0 -0
  129. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  130. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/square_planner/index.html.j2 +0 -0
  131. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/upload/index.html.j2 +0 -0
  132. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/templates/upload/reload.html.j2 +0 -0
  133. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/geo_activity_playground/webui/tile_blueprint.py +0 -0
  134. {geo_activity_playground-0.36.2 → geo_activity_playground-0.38.0}/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.36.2
3
+ Version: 0.38.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -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),
@@ -120,6 +120,8 @@ def _get_metadata_from_timeseries(timeseries: pd.DataFrame) -> ActivityMeta:
120
120
  metadata["start_longitude"] = timeseries["longitude"].iloc[0]
121
121
  metadata["end_longitude"] = timeseries["longitude"].iloc[-1]
122
122
 
123
+ metadata["elevation_gain"] = timeseries["elevation_gain_cum"].iloc[-1]
124
+
123
125
  return metadata
124
126
 
125
127
 
@@ -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
@@ -0,0 +1,157 @@
1
+ import dataclasses
2
+ import datetime
3
+ import re
4
+ import urllib.parse
5
+ from typing import Optional
6
+
7
+ import dateutil.parser
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ @dataclasses.dataclass
13
+ class SearchQuery:
14
+ equipment: list[str] = dataclasses.field(default_factory=list)
15
+ kind: list[str] = dataclasses.field(default_factory=list)
16
+ name: Optional[str] = None
17
+ name_case_sensitive: bool = False
18
+ start_begin: Optional[datetime.date] = None
19
+ start_end: Optional[datetime.date] = None
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
+
38
+ @property
39
+ def active(self) -> bool:
40
+ return (
41
+ self.equipment
42
+ or self.kind
43
+ or self.name
44
+ or self.start_begin
45
+ or self.start_end
46
+ )
47
+
48
+ def to_primitives(self) -> dict:
49
+ return {
50
+ "equipment": self.equipment,
51
+ "kind": self.kind,
52
+ "name": self.name or "",
53
+ "name_case_sensitive": self.name_case_sensitive,
54
+ "start_begin": _format_optional_date(self.start_begin),
55
+ "start_end": _format_optional_date(self.start_end),
56
+ }
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
+
74
+ def to_url_str(self) -> str:
75
+ variables = []
76
+ for equipment in self.equipment:
77
+ variables.append(("equipment", equipment))
78
+ for kind in self.kind:
79
+ variables.append(("kind", kind))
80
+ if self.name:
81
+ variables.append(("name", self.name))
82
+ if self.name_case_sensitive:
83
+ variables.append(("name_case_sensitive", "true"))
84
+ if self.start_begin:
85
+ variables.append(("start_begin", self.start_begin.isoformat()))
86
+ if self.start_end:
87
+ variables.append(("start_end", self.start_end.isoformat()))
88
+
89
+ return "&".join(
90
+ f"{key}={urllib.parse.quote_plus(value)}" for key, value in variables
91
+ )
92
+
93
+
94
+ def apply_search_query(
95
+ activity_meta: pd.DataFrame, search_query: SearchQuery
96
+ ) -> pd.DataFrame:
97
+ mask = _make_mask(activity_meta.index, True)
98
+
99
+ if search_query.equipment:
100
+ mask &= _filter_column(activity_meta["equipment"], search_query.equipment)
101
+ if search_query.kind:
102
+ mask &= _filter_column(activity_meta["kind"], search_query.kind)
103
+ if search_query.name:
104
+ mask &= pd.Series(
105
+ [
106
+ bool(
107
+ re.search(
108
+ search_query.name,
109
+ activity_name,
110
+ 0 if search_query.name_case_sensitive else re.IGNORECASE,
111
+ )
112
+ )
113
+ for activity_name in activity_meta["name"]
114
+ ],
115
+ index=activity_meta.index,
116
+ )
117
+ if search_query.start_begin is not None:
118
+ start_begin = datetime.datetime.combine(
119
+ search_query.start_begin, datetime.time.min
120
+ )
121
+ mask &= start_begin <= activity_meta["start"]
122
+ if search_query.start_end is not None:
123
+ start_end = datetime.datetime.combine(search_query.start_end, datetime.time.max)
124
+ mask &= activity_meta["start"] <= start_end
125
+
126
+ return activity_meta.loc[mask]
127
+
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
+
136
+ def _make_mask(
137
+ index: pd.Index,
138
+ default: bool,
139
+ ) -> pd.Series:
140
+ if default:
141
+ return pd.Series(np.ones((len(index),), dtype=np.bool), index=index)
142
+ else:
143
+ return pd.Series(np.zeros((len(index),), dtype=np.bool), index=index)
144
+
145
+
146
+ def _filter_column(column: pd.Series, values: list):
147
+ sub_mask = _make_mask(column.index, False)
148
+ for equipment in values:
149
+ sub_mask |= column == equipment
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()
@@ -0,0 +1,30 @@
1
+ import pandas as pd
2
+
3
+
4
+ def get_equipment_use_table(
5
+ activity_meta: pd.DataFrame, offsets: dict[str, float]
6
+ ) -> pd.DataFrame:
7
+ result = (
8
+ activity_meta.groupby("equipment")
9
+ .apply(
10
+ lambda group: pd.Series(
11
+ {
12
+ "total_distance_km": group["distance_km"].sum(),
13
+ "first_use": group["start"].min(skipna=True),
14
+ "last_use": group["start"].max(skipna=True),
15
+ },
16
+ ),
17
+ include_groups=False,
18
+ )
19
+ .sort_values("last_use", ascending=False)
20
+ )
21
+ for equipment, offset in offsets.items():
22
+ result.loc[equipment, "total_distance_km"] += offset
23
+
24
+ result["total_distance_km"] = [
25
+ int(round(elem)) for elem in result["total_distance_km"]
26
+ ]
27
+ result["first_use"] = [date.date().isoformat() for date in result["first_use"]]
28
+ result["last_use"] = [date.date().isoformat() for date in result["last_use"]]
29
+
30
+ return result.reset_index()
@@ -0,0 +1,100 @@
1
+ import datetime
2
+
3
+ import pandas as pd
4
+
5
+ from geo_activity_playground.core.meta_search import _make_mask
6
+ from geo_activity_playground.core.meta_search import apply_search_query
7
+ from geo_activity_playground.core.meta_search import SearchQuery
8
+
9
+
10
+ def test_empty_query() -> None:
11
+ activity_meta = pd.DataFrame(
12
+ {
13
+ "equipment": pd.Series(["A", "B", "B"]),
14
+ "id": pd.Series([1, 2, 3]),
15
+ "kind": pd.Series(["X", "X", "Y"]),
16
+ "name": ["Test1", "Test2", "Test3"],
17
+ "start": [
18
+ datetime.datetime(2024, 12, 24, 10),
19
+ datetime.datetime(2025, 1, 1, 10),
20
+ None,
21
+ ],
22
+ }
23
+ )
24
+
25
+ search_query = SearchQuery()
26
+
27
+ actual = apply_search_query(activity_meta, search_query)
28
+ assert (actual["id"] == activity_meta["id"]).all()
29
+
30
+
31
+ def test_equipment_query() -> None:
32
+ activity_meta = pd.DataFrame(
33
+ {
34
+ "equipment": pd.Series(["A", "B", "B"]),
35
+ "id": pd.Series([1, 2, 3]),
36
+ "kind": pd.Series(["X", "X", "Y"]),
37
+ "name": ["Test1", "Test2", "Test3"],
38
+ "start": [
39
+ datetime.datetime(2024, 12, 24, 10),
40
+ datetime.datetime(2025, 1, 1, 10),
41
+ None,
42
+ ],
43
+ }
44
+ )
45
+ search_query = SearchQuery(equipment=["B"])
46
+ actual = apply_search_query(activity_meta, search_query)
47
+ assert set(actual["id"]) == {2, 3}
48
+
49
+
50
+ def test_date_query() -> None:
51
+ activity_meta = pd.DataFrame(
52
+ {
53
+ "equipment": pd.Series(["A", "B", "B"]),
54
+ "id": pd.Series([1, 2, 3]),
55
+ "kind": pd.Series(["X", "X", "Y"]),
56
+ "name": ["Test1", "Test2", "Test3"],
57
+ "start": [
58
+ datetime.datetime(2024, 12, 24, 10),
59
+ datetime.datetime(2025, 1, 1, 10),
60
+ None,
61
+ ],
62
+ }
63
+ )
64
+ search_query = SearchQuery(start_begin=datetime.date(2024, 12, 31))
65
+ actual = apply_search_query(activity_meta, search_query)
66
+ assert set(actual["id"]) == {2}
67
+
68
+
69
+ def test_name_query() -> None:
70
+ activity_meta = pd.DataFrame(
71
+ {
72
+ "equipment": pd.Series(["A", "B", "B"]),
73
+ "id": pd.Series([1, 2, 3]),
74
+ "kind": pd.Series(["X", "X", "Y"]),
75
+ "name": ["Test1", "Test2", "Test3"],
76
+ "start": [
77
+ datetime.datetime(2024, 12, 24, 10),
78
+ datetime.datetime(2025, 1, 1, 10),
79
+ None,
80
+ ],
81
+ }
82
+ )
83
+ search_query = SearchQuery(name="Test1")
84
+ actual = apply_search_query(activity_meta, search_query)
85
+ assert set(actual["id"]) == {1}
86
+
87
+
88
+ def test_make_mask() -> None:
89
+ index = [1, 2]
90
+ assert (_make_mask(index, True) == pd.Series([True, True], index=index)).all()
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 == []
@@ -0,0 +1,108 @@
1
+ import datetime
2
+
3
+ import pandas as pd
4
+ import pytest
5
+
6
+ from geo_activity_playground.core.summary_stats import get_equipment_use_table
7
+
8
+
9
+ @pytest.fixture
10
+ def activity_meta() -> pd.DataFrame:
11
+ """
12
+ calories: float
13
+ commute: bool
14
+ consider_for_achievements: bool
15
+ distance_km: float
16
+ elapsed_time: datetime.timedelta
17
+ end_latitude: float
18
+ end_longitude: float
19
+ equipment: str
20
+ id: int
21
+ kind: str
22
+ moving_time: datetime.timedelta
23
+ name: str
24
+ path: str
25
+ start_latitude: float
26
+ start_longitude: float
27
+ start: np.datetime64
28
+ steps: int
29
+ """
30
+ return pd.DataFrame(
31
+ {
32
+ "calories": pd.Series([None, 1000, 2000]),
33
+ "commute": pd.Series([True, False, True]),
34
+ "consider_for_achievements": pd.Series([True, True, False]),
35
+ "distance_km": pd.Series([9.8, 4.4, 4.3]),
36
+ "elapsed_time": pd.Series(
37
+ [
38
+ datetime.timedelta(minutes=0.34),
39
+ datetime.timedelta(minutes=0.67),
40
+ None,
41
+ ]
42
+ ),
43
+ "end_latitude": pd.Series([0.58, 0.5, 0.19]),
44
+ "end_longitude": pd.Series([0.2, 0.94, 0.69]),
45
+ "equipment": pd.Series(["A", "B", "B"]),
46
+ "id": pd.Series([1, 2, 3]),
47
+ "kind": pd.Series(["X", "X", "Y"]),
48
+ "moving_time": pd.Series(
49
+ [
50
+ datetime.timedelta(minutes=0.32),
51
+ datetime.timedelta(minutes=0.83),
52
+ None,
53
+ ]
54
+ ),
55
+ "name": pd.Series(["Test1", "Test2", "Test1"]),
56
+ "path": pd.Series(["Test1.fit", "Test2.gpx", "Test1.kml"]),
57
+ "start_latitude": pd.Series([0.22, 0.02, 0.35]),
58
+ "start_longitude": pd.Series([0.95, 0.95, 0.81]),
59
+ "start": pd.Series(
60
+ [
61
+ datetime.datetime(2024, 12, 24, 10),
62
+ datetime.datetime(2025, 1, 1, 10),
63
+ None,
64
+ ]
65
+ ),
66
+ "steps": pd.Series([1234, None, 5432]),
67
+ }
68
+ )
69
+
70
+
71
+ def test_activity_meta(activity_meta) -> None:
72
+ print()
73
+ print(activity_meta)
74
+
75
+
76
+ def test_equipment_use_table(activity_meta) -> None:
77
+ activity_meta = pd.DataFrame(
78
+ {
79
+ "distance_km": pd.Series([9.8, 4.4, 4.3]),
80
+ "equipment": pd.Series(["A", "B", "B"]),
81
+ "start": pd.Series(
82
+ [
83
+ datetime.datetime(2024, 12, 24, 10),
84
+ datetime.datetime(2025, 1, 1, 10),
85
+ None,
86
+ ]
87
+ ),
88
+ }
89
+ )
90
+
91
+ offsets = {"A": 4.0}
92
+
93
+ expected = [
94
+ {
95
+ "equipment": "B",
96
+ "total_distance_km": 9,
97
+ "first_use": "2025-01-01",
98
+ "last_use": "2025-01-01",
99
+ },
100
+ {
101
+ "equipment": "A",
102
+ "total_distance_km": 14,
103
+ "first_use": "2024-12-24",
104
+ "last_use": "2024-12-24",
105
+ },
106
+ ]
107
+ actual = get_equipment_use_table(activity_meta, offsets)
108
+ assert actual == expected
@@ -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>