geo-activity-playground 0.29.2__tar.gz → 0.30.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 (123) hide show
  1. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/config.py +2 -0
  3. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/enrichment.py +18 -7
  4. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/paths.py +0 -2
  5. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_api.py +1 -1
  6. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/controller.py +16 -0
  7. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +1 -1
  8. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
  9. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +13 -2
  10. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +1 -1
  11. geo_activity_playground-0.30.0/geo_activity_playground/webui/search/blueprint.py +101 -0
  12. geo_activity_playground-0.30.0/geo_activity_playground/webui/search/templates/search/index.html.j2 +91 -0
  13. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/blueprint.py +44 -0
  14. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +18 -0
  15. geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +25 -0
  16. geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +27 -0
  17. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  18. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  19. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.ico +0 -0
  20. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.svg +3 -0
  21. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/site.webmanifest +21 -0
  22. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  23. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  24. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
  25. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/home.html.j2 +1 -8
  26. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -3
  27. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/pyproject.toml +1 -1
  28. geo_activity_playground-0.29.2/geo_activity_playground/webui/search/blueprint.py +0 -20
  29. geo_activity_playground-0.29.2/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -38
  30. geo_activity_playground-0.29.2/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  31. geo_activity_playground-0.29.2/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  32. geo_activity_playground-0.29.2/geo_activity_playground/webui/static/favicon.ico +0 -0
  33. geo_activity_playground-0.29.2/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -121
  34. geo_activity_playground-0.29.2/geo_activity_playground/webui/static/site.webmanifest +0 -19
  35. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/LICENSE +0 -0
  36. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/__init__.py +0 -0
  37. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/__main__.py +0 -0
  38. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/__init__.py +0 -0
  39. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/activities.py +0 -0
  40. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/coordinates.py +0 -0
  41. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heart_rate.py +0 -0
  42. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heatmap.py +0 -0
  43. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  44. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/similarity.py +0 -0
  45. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tasks.py +0 -0
  46. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_tiles.py +0 -0
  47. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
  48. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tiles.py +0 -0
  49. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/time_conversion.py +0 -0
  50. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/__init__.py +0 -0
  51. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  52. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
  53. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/video.py +0 -0
  54. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/__init__.py +0 -0
  55. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
  56. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  57. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/directory.py +0 -0
  58. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  59. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  60. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_directory.py +0 -0
  61. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  62. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/__init__.py +0 -0
  63. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
  64. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  65. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  66. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/app.py +0 -0
  67. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  68. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  69. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/authenticator.py +0 -0
  70. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  71. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  72. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
  73. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  74. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  75. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  76. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  77. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
  78. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  79. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  80. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  81. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  82. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
  83. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  84. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  85. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
  86. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  87. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  88. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
  89. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -0
  90. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
  91. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/plot_util.py +0 -0
  92. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/controller.py +0 -0
  93. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  94. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
  95. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  96. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  97. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  98. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  99. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  100. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  101. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  102. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  103. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  104. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
  105. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
  106. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  107. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  108. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  109. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  110. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  111. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  112. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  113. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
  114. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
  115. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/controller.py +0 -0
  116. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
  117. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
  118. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/controller.py +0 -0
  119. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
  120. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
  121. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/controller.py +0 -0
  122. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
  123. {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.29.2
3
+ Version: 0.30.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -29,6 +29,7 @@ class Config:
29
29
  )
30
30
  heart_rate_resting: int = 0
31
31
  heart_rate_maximum: Optional[int] = None
32
+ kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
32
33
  kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
33
34
  metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
34
35
  num_processes: Optional[int] = 1
@@ -39,6 +40,7 @@ class Config:
39
40
  strava_client_id: int = 131693
40
41
  strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
41
42
  strava_client_code: Optional[str] = None
43
+ time_diff_threshold_seconds: Optional[int] = 30
42
44
  upload_password: Optional[str] = None
43
45
 
44
46
 
@@ -82,11 +82,15 @@ def enrich_activities(config: Config) -> None:
82
82
  )
83
83
  continue
84
84
 
85
+ # Rename kinds if needed.
86
+ if metadata["kind"] in config.kind_renames:
87
+ metadata["kind"] = config.kind_renames[metadata["kind"]]
88
+
85
89
  # Enrich time series.
86
90
  if metadata["kind"] in config.kinds_without_achievements:
87
91
  metadata["consider_for_achievements"] = False
88
92
  time_series = _embellish_single_time_series(
89
- time_series, metadata.get("start", None)
93
+ time_series, metadata.get("start", None), config.time_diff_threshold_seconds
90
94
  )
91
95
  metadata.update(_get_metadata_from_timeseries(time_series))
92
96
 
@@ -131,7 +135,9 @@ def _compute_moving_time(time_series: pd.DataFrame) -> datetime.timedelta:
131
135
 
132
136
 
133
137
  def _embellish_single_time_series(
134
- timeseries: pd.DataFrame, start: Optional[datetime.datetime] = None
138
+ timeseries: pd.DataFrame,
139
+ start: Optional[datetime.datetime],
140
+ time_diff_threshold_seconds: int,
135
141
  ) -> pd.DataFrame:
136
142
  if start is not None and pd.api.types.is_dtype_equal(
137
143
  timeseries["time"].dtype, "int64"
@@ -153,10 +159,12 @@ def _embellish_single_time_series(
153
159
  timeseries["latitude"],
154
160
  timeseries["longitude"],
155
161
  ).fillna(0.0)
156
- time_diff_threshold_seconds = 30
157
- time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
158
- jump_indices = time_diff >= time_diff_threshold_seconds
159
- distances.loc[jump_indices] = 0.0
162
+ if time_diff_threshold_seconds:
163
+ time_diff = (
164
+ timeseries["time"] - timeseries["time"].shift(1)
165
+ ).dt.total_seconds()
166
+ jump_indices = time_diff >= time_diff_threshold_seconds
167
+ distances.loc[jump_indices] = 0.0
160
168
 
161
169
  if "distance_km" not in timeseries.columns:
162
170
  timeseries["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
@@ -173,7 +181,10 @@ def _embellish_single_time_series(
173
181
  timeseries = timeseries.loc[~potential_jumps].copy()
174
182
 
175
183
  if "segment_id" not in timeseries.columns:
176
- timeseries["segment_id"] = np.cumsum(jump_indices)
184
+ if time_diff_threshold_seconds:
185
+ timeseries["segment_id"] = np.cumsum(jump_indices)
186
+ else:
187
+ timeseries["segment_id"] = 0
177
188
 
178
189
  if "x" not in timeseries.columns:
179
190
  x, y = compute_tile_float(timeseries["latitude"], timeseries["longitude"], 0)
@@ -8,7 +8,6 @@ import typing
8
8
 
9
9
 
10
10
  def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
11
- @functools.cache
12
11
  def wrapper() -> pathlib.Path:
13
12
  path.mkdir(exist_ok=True, parents=True)
14
13
  return path
@@ -17,7 +16,6 @@ def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
17
16
 
18
17
 
19
18
  def file_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
20
- @functools.cache
21
19
  def wrapper() -> pathlib.Path:
22
20
  path.parent.mkdir(exist_ok=True, parents=True)
23
21
  return path
@@ -139,7 +139,7 @@ def try_import_strava(config: Config) -> bool:
139
139
  "commute": activity.commute,
140
140
  "distance_km": activity.distance / 1000,
141
141
  "name": activity.name,
142
- "kind": str(activity.type),
142
+ "kind": str(activity.type.root),
143
143
  "start": convert_to_datetime_ns(activity.start_date),
144
144
  "elapsed_time": activity.elapsed_time,
145
145
  "equipment": gear_names[activity.gear_id],
@@ -115,6 +115,8 @@ class ActivityController:
115
115
  result["altitude_time_plot"] = altitude_time_plot(time_series)
116
116
  if "heartrate" in time_series.columns:
117
117
  result["heartrate_time_plot"] = heart_rate_time_plot(time_series)
118
+ if "cadence" in time_series.columns:
119
+ result["cadence_time_plot"] = cadence_time_plot(time_series)
118
120
  return result
119
121
 
120
122
  def render_sharepic(self, id: int) -> bytes:
@@ -324,6 +326,20 @@ def heart_rate_time_plot(time_series: pd.DataFrame) -> str:
324
326
  )
325
327
 
326
328
 
329
+ def cadence_time_plot(time_series: pd.DataFrame) -> str:
330
+ return (
331
+ alt.Chart(time_series, title="Cadence")
332
+ .mark_line()
333
+ .encode(
334
+ alt.X("time", title="Time"),
335
+ alt.Y("cadence", title="Cadence"),
336
+ alt.Color("segment_id:N", title="Segment"),
337
+ )
338
+ .interactive(bind_y=False)
339
+ .to_json(format="vega")
340
+ )
341
+
342
+
327
343
  def heart_rate_zone_plot(heart_zones: pd.DataFrame) -> str:
328
344
  return (
329
345
  alt.Chart(heart_zones, title="Heart Rate Zones")
@@ -41,7 +41,7 @@
41
41
  <div class="col">
42
42
  <h2>Activities</h2>
43
43
 
44
- <table class="table">
44
+ <table class="table table-sort table-arrows">
45
45
  <thead>
46
46
  <tr>
47
47
  <th>Name</th>
@@ -50,12 +50,12 @@
50
50
  <div class="col">
51
51
  <h2>Activities</h2>
52
52
 
53
- <table class="table">
53
+ <table class="table table-sort table-arrows">
54
54
  <thead>
55
55
  <tr>
56
56
  <th>Name</th>
57
57
  <th>Date</th>
58
- <th>Distance / km</th>
58
+ <th class="numeric-sort">Distance / km</th>
59
59
  <th>Elapsed time</th>
60
60
  <th>Equipment</th>
61
61
  <th>Kind</th>
@@ -129,6 +129,17 @@
129
129
  </div>
130
130
  {% endif %}
131
131
 
132
+ {% if cadence_time_plot is defined %}
133
+ <h2>Cadence</h2>
134
+
135
+ <div class="row mb-3">
136
+ <div class="col-md-4">
137
+ {{ vega_direct("cadence_time_plot", cadence_time_plot) }}
138
+ </div>
139
+ </div>
140
+ {% endif %}
141
+
142
+
132
143
  <h2>Share picture</h2>
133
144
 
134
145
  <p><img src="{{ url_for('.sharepic', id=activity.id) }}" /></p>
@@ -177,11 +188,11 @@
177
188
 
178
189
  <p><a href="{{ url_for('.name', name=activity['name']) }}">Overview over these activities</a></p>
179
190
 
180
- <table class="table">
191
+ <table class="table table-sort table-arrows">
181
192
  <thead>
182
193
  <tr>
183
194
  <th>Date</th>
184
- <th>Distance / km</th>
195
+ <th class="numeric-sort">Distance / km</th>
185
196
  <th>Elapsed time</th>
186
197
  <th>Equipment</th>
187
198
  <th>Kind</th>
@@ -9,7 +9,7 @@
9
9
 
10
10
  <div class="row mb-3">
11
11
  <div class="col">
12
- <table class="table">
12
+ <table class="table table-sort table-arrows">
13
13
  <thead>
14
14
  <tr>
15
15
  <th>Equipment</th>
@@ -0,0 +1,101 @@
1
+ from functools import reduce
2
+
3
+ import dateutil.parser
4
+ from flask import Blueprint
5
+ from flask import flash
6
+ from flask import render_template
7
+ from flask import request
8
+ from flask import Response
9
+
10
+ from ...core.activities import ActivityRepository
11
+
12
+
13
+ def reduce_or(selections):
14
+ return reduce(lambda a, b: a | b, selections)
15
+
16
+
17
+ def reduce_and(selections):
18
+ return reduce(lambda a, b: a & b, selections)
19
+
20
+
21
+ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
22
+ blueprint = Blueprint("search", __name__, template_folder="templates")
23
+
24
+ @blueprint.route("/")
25
+ def index():
26
+ kinds_avail = repository.meta["kind"].unique()
27
+ equipments_avail = repository.meta["equipment"].unique()
28
+
29
+ print(request.args)
30
+
31
+ activities = repository.meta
32
+
33
+ if equipments := request.args.getlist("equipment"):
34
+ selection = reduce_or(
35
+ activities["equipment"] == equipment for equipment in equipments
36
+ )
37
+ activities = activities.loc[selection]
38
+
39
+ if kinds := request.args.getlist("kind"):
40
+ selection = reduce_or(activities["kind"] == kind for kind in kinds)
41
+ activities = activities.loc[selection]
42
+
43
+ name_exact = bool(request.args.get("name_exact", False))
44
+ name_casing = bool(request.args.get("name_casing", False))
45
+ if name := request.args.get("name", ""):
46
+ if name_casing:
47
+ haystack = activities["name"]
48
+ needle = name
49
+ else:
50
+ haystack = activities["name"].str.lower()
51
+ needle = name.lower()
52
+ if name_exact:
53
+ selection = haystack == needle
54
+ else:
55
+ selection = [needle in an for an in haystack]
56
+ activities = activities.loc[selection]
57
+
58
+ begin = request.args.get("begin", "")
59
+ end = request.args.get("end", "")
60
+
61
+ if begin:
62
+ try:
63
+ begin_dt = dateutil.parser.parse(begin)
64
+ except ValueError:
65
+ flash(
66
+ f"Cannot parse date `{begin}`, please use a different format.",
67
+ category="danger",
68
+ )
69
+ else:
70
+ selection = begin_dt <= activities["start"]
71
+ activities = activities.loc[selection]
72
+
73
+ if end:
74
+ try:
75
+ end_dt = dateutil.parser.parse(end)
76
+ except ValueError:
77
+ flash(
78
+ f"Cannot parse date `{end}`, please use a different format.",
79
+ category="danger",
80
+ )
81
+ else:
82
+ selection = activities["start"] < end_dt
83
+ activities = activities.loc[selection]
84
+
85
+ activities = activities.sort_values("start", ascending=False)
86
+
87
+ return render_template(
88
+ "search/index.html.j2",
89
+ activities=list(activities.iterrows()),
90
+ equipments=request.args.getlist("equipment"),
91
+ equipments_avail=sorted(equipments_avail),
92
+ kinds=request.args.getlist("kind"),
93
+ kinds_avail=sorted(kinds_avail),
94
+ name=name,
95
+ name_exact=name_exact,
96
+ name_casing=name_casing,
97
+ begin=begin,
98
+ end=end,
99
+ )
100
+
101
+ return blueprint
@@ -0,0 +1,91 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="row mb-3">Activities Overview & Search</h1>
6
+
7
+ <div class="row mb-3">
8
+ <div class="col-md-2">
9
+ <form>
10
+ <div class="mb-3">
11
+ <label for="name" class="form-label">Name</label>
12
+ <input type="text" class="form-control" id="name" name="name" value="{{ name }}">
13
+ <div class="form-check">
14
+ <input class="form-check-input" type="checkbox" name="name_exact" value="true" id="name_exact" {% if
15
+ name_exact %} checked {% endif %}>
16
+ <label class="form-check-label" for="name_exact">
17
+ Exact match
18
+ </label>
19
+ </div>
20
+ <div class="form-check">
21
+ <input class="form-check-input" type="checkbox" name="name_casing" value="true" id="name_casing" {%
22
+ if name_casing %} checked {% endif %}>
23
+ <label class="form-check-label" for="name_casing">
24
+ Case sensitive
25
+ </label>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="mb-3">
30
+ <label for="begin" class="form-label">After</label>
31
+ <input type="text" class="form-control" id="begin" name="begin" value="{{ begin }}">
32
+ <label for="end" class="form-label">Until</label>
33
+ <input type="text" class="form-control" id="end" name="end" value="{{ end }}">
34
+ </div>
35
+
36
+ <div class="mb-3">
37
+ <label for="" class="form-label">Kind</label>
38
+ {% for kind in kinds_avail %}
39
+ <div class="form-check">
40
+ <input class="form-check-input" type="checkbox" name="kind" value="{{ kind }}" id="kind_{{ kind }}"
41
+ {% if kind in kinds %} checked {% endif %}>
42
+ <label class="form-check-label" for="kind_{{ kind }}">
43
+ {{ kind }}
44
+ </label>
45
+ </div>
46
+ {% endfor %}
47
+ </div>
48
+
49
+ <div class="mb-3">
50
+ <label for="" class="form-label">Equipment</label>
51
+ {% for equipment in equipments_avail %}
52
+ <div class="form-check">
53
+ <input class="form-check-input" type="checkbox" name="equipment" value="{{ equipment }}"
54
+ id="equipment_{{ equipment }}" {% if equipment in equipments %} checked {% endif %}>
55
+ <label class="form-check-label" for="equipment_{{ equipment }}">
56
+ {{ equipment }}
57
+ </label>
58
+ </div>
59
+ {% endfor %}
60
+ </div>
61
+
62
+ <button type="submit" class="btn btn-primary">Search</button>
63
+ </form>
64
+ </div>
65
+
66
+ <div class="col-md-10">
67
+ <table class="table table-sort table-arrows">
68
+ <thead>
69
+ <tr>
70
+ <th>Name</th>
71
+ <th>Start</th>
72
+ <th>Kind</th>
73
+ <th class="numeric-sort">Distance</th>
74
+ <th>Elapsed time</th>
75
+ </tr>
76
+ </thead>
77
+ <tbody>
78
+ {% for index, activity in activities %}
79
+ <tr>
80
+ <td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
81
+ <td>{{ activity['start'] }}</td>
82
+ <td>{{ activity['kind'] }}</td>
83
+ <td>{{ '%.1f' % activity["distance_km"] }} km</td>
84
+ <td>{{ activity.elapsed_time }}</td>
85
+ </tr>
86
+ {% endfor %}
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ </div>
91
+ {% endblock %}
@@ -1,3 +1,4 @@
1
+ import shutil
1
2
  from typing import Optional
2
3
 
3
4
  from flask import Blueprint
@@ -8,6 +9,7 @@ from flask import request
8
9
  from flask import url_for
9
10
 
10
11
  from geo_activity_playground.core.config import ConfigAccessor
12
+ from geo_activity_playground.core.paths import _activity_enriched_dir
11
13
  from geo_activity_playground.webui.authenticator import Authenticator
12
14
  from geo_activity_playground.webui.authenticator import needs_authentication
13
15
  from geo_activity_playground.webui.settings.controller import SettingsController
@@ -139,6 +141,33 @@ def make_settings_blueprint(
139
141
  "settings/heart-rate.html.j2", **settings_controller.render_heart_rate()
140
142
  )
141
143
 
144
+ @blueprint.route("/kind-renames", methods=["GET", "POST"])
145
+ @needs_authentication(authenticator)
146
+ def kind_renames():
147
+ if request.method == "POST":
148
+ rules_str = request.form["rules_str"]
149
+ rules = {}
150
+ try:
151
+ for line in rules_str.strip().split("\n"):
152
+ first, second = line.split(" => ")
153
+ rules[first.strip()] = second.strip()
154
+ config_accessor().kind_renames = rules
155
+ config_accessor.save()
156
+ flash(f"Kind renames updated.", category="success")
157
+ shutil.rmtree(_activity_enriched_dir)
158
+ return redirect(url_for("upload.reload"))
159
+ except ValueError as e:
160
+ flash(f"Cannot parse this. Please try again.", category="danger")
161
+ else:
162
+ rules_str = "\n".join(
163
+ f"{key} =&gt; {value}"
164
+ for key, value in config_accessor().kind_renames.items()
165
+ )
166
+ return render_template(
167
+ "settings/kind-renames.html.j2",
168
+ rules_str=rules_str,
169
+ )
170
+
142
171
  @blueprint.route("/kinds-without-achievements", methods=["GET", "POST"])
143
172
  @needs_authentication(authenticator)
144
173
  def kinds_without_achievements():
@@ -173,6 +202,21 @@ def make_settings_blueprint(
173
202
  **settings_controller.render_privacy_zones(),
174
203
  )
175
204
 
205
+ @blueprint.route("/segmentation", methods=["GET", "POST"])
206
+ @needs_authentication(authenticator)
207
+ def segmentation():
208
+ if request.method == "POST":
209
+ threshold = int(request.form.get("threshold", 0))
210
+ config_accessor().time_diff_threshold_seconds = threshold
211
+ config_accessor.save()
212
+ flash(f"Threshold set to {threshold}.", category="success")
213
+ shutil.rmtree(_activity_enriched_dir)
214
+ return redirect(url_for("upload.reload"))
215
+ return render_template(
216
+ "settings/segmentation.html.j2",
217
+ threshold=config_accessor().time_diff_threshold_seconds,
218
+ )
219
+
176
220
  @blueprint.route("/sharepic", methods=["GET", "POST"])
177
221
  @needs_authentication(authenticator)
178
222
  def sharepic():
@@ -47,6 +47,15 @@
47
47
  </div>
48
48
  </div>
49
49
  </div>
50
+ <div class="col">
51
+ <div class="card">
52
+ <div class="card-body">
53
+ <h5 class="card-title">Kind renames</h5>
54
+ <p class="card-text">Bulk rename activity kinds</p>
55
+ <a href="{{ url_for('.kind_renames') }}" class="btn btn-primary">Set up kind renames</a>
56
+ </div>
57
+ </div>
58
+ </div>
50
59
  <div class="col">
51
60
  <div class="card">
52
61
  <div class="card-body">
@@ -96,6 +105,15 @@
96
105
  </div>
97
106
  </div>
98
107
  </div>
108
+ <div class="col">
109
+ <div class="card">
110
+ <div class="card-body">
111
+ <h5 class="card-title">Track segmentation</h5>
112
+ <p class="card-text">Split tracks into multiple segments if there are breaks or jumps.</p>
113
+ <a href="{{ url_for('.segmentation') }}" class="btn btn-primary">Set up track segmentations</a>
114
+ </div>
115
+ </div>
116
+ </div>
99
117
  </div>
100
118
  </div>
101
119
  {% endblock %}
@@ -0,0 +1,25 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Kind renaming</h1>
6
+
7
+ <p>If you have used different apps for tracking, you might have that your bike rides have <i>kind</i> "ride", "Ride",
8
+ "Radfahrt" and so on. In order to unify these, you can specify mappings from old to new names.</p>
9
+
10
+ <p>If you want to unify these to "Ride", enter the following:</p>
11
+
12
+ <pre><code>
13
+ ride =&gt; Ride
14
+ Radfahrt =&gt; Ride
15
+ </code></pre>
16
+
17
+ <form method="POST">
18
+ <div class="mb-3">
19
+ <label for="rules" class="form-label">Rules</label>
20
+ <textarea class="form-control" id=rules" cols="80" rows="10" name="rules_str">{{ rules_str }}</textarea>
21
+ </div>
22
+ <button type="submit" class="btn btn-primary">Save</button>
23
+ </form>
24
+
25
+ {% endblock %}
@@ -0,0 +1,27 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Track segmentation</h1>
6
+
7
+ <p>Some activity tracking apps or devices automatically pause the recording when there is no movement for a while. Other
8
+ trackers do not record new points when one goes in a straight line. And some users manually pause the activity,
9
+ forget to resume and resume after having moved for a while.</p>
10
+
11
+ <p>Depending on the usage patterns one wants to segment the tracks into segments or really keep them as one long track.
12
+ In order to cater for different use cases, this can be changed with a setting.</p>
13
+
14
+ <p>In the following you can enter a threshold. If these many seconds elapse between two subsequent points in the track,
15
+ these will be considered different segments and not be connected with a straight line. Entering "0" disabled
16
+ segmentation.</p>
17
+
18
+ <form method="POST">
19
+ <div class="mb-3">
20
+ <label for="threshold" class="form-label">Threshold / s</label>
21
+ <input type="text" class="form-control" id="threshold" name="threshold" value="{{ threshold }}" />
22
+ </div>
23
+ <button type="submit" class="btn btn-primary">Save</button>
24
+ </form>
25
+
26
+
27
+ {% endblock %}