geo-activity-playground 0.29.1__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.1 → geo_activity_playground-0.30.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/activities.py +4 -1
  3. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/config.py +2 -0
  4. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/enrichment.py +18 -7
  5. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/paths.py +0 -2
  6. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_api.py +2 -2
  7. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/controller.py +19 -1
  8. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +1 -1
  9. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
  10. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +14 -2
  11. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +1 -1
  12. geo_activity_playground-0.30.0/geo_activity_playground/webui/search/blueprint.py +101 -0
  13. geo_activity_playground-0.30.0/geo_activity_playground/webui/search/templates/search/index.html.j2 +91 -0
  14. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/blueprint.py +44 -0
  15. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +18 -0
  16. geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +25 -0
  17. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +10 -11
  18. geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +27 -0
  19. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  20. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  21. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.ico +0 -0
  22. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.svg +3 -0
  23. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/site.webmanifest +21 -0
  24. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  25. geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  26. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
  27. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/home.html.j2 +1 -8
  28. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -3
  29. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/controller.py +1 -1
  30. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/pyproject.toml +1 -1
  31. geo_activity_playground-0.29.1/geo_activity_playground/webui/search/blueprint.py +0 -20
  32. geo_activity_playground-0.29.1/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -38
  33. geo_activity_playground-0.29.1/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  34. geo_activity_playground-0.29.1/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  35. geo_activity_playground-0.29.1/geo_activity_playground/webui/static/favicon.ico +0 -0
  36. geo_activity_playground-0.29.1/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -121
  37. geo_activity_playground-0.29.1/geo_activity_playground/webui/static/site.webmanifest +0 -19
  38. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/LICENSE +0 -0
  39. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/__init__.py +0 -0
  40. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/__main__.py +0 -0
  41. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/__init__.py +0 -0
  42. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/coordinates.py +0 -0
  43. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heart_rate.py +0 -0
  44. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heatmap.py +0 -0
  45. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  46. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/similarity.py +0 -0
  47. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tasks.py +0 -0
  48. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_tiles.py +0 -0
  49. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
  50. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tiles.py +0 -0
  51. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/time_conversion.py +0 -0
  52. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/__init__.py +0 -0
  53. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  54. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
  55. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/video.py +0 -0
  56. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/__init__.py +0 -0
  57. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
  58. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  59. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/directory.py +0 -0
  60. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  61. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  62. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_directory.py +0 -0
  63. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  64. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/__init__.py +0 -0
  65. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
  66. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  67. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  68. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/app.py +0 -0
  69. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  70. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  71. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/authenticator.py +0 -0
  72. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  73. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  74. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
  75. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  76. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  77. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  78. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  79. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
  80. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  81. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  82. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  83. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  84. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
  85. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  86. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  87. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
  88. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  89. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  90. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
  91. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -0
  92. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
  93. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/plot_util.py +0 -0
  94. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/controller.py +0 -0
  95. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  96. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
  97. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  98. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  99. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  100. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  101. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  102. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  103. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  104. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  105. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
  106. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
  107. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  108. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  109. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  110. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  111. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  112. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  113. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  114. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
  115. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
  116. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/controller.py +0 -0
  117. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
  118. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
  119. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/controller.py +0 -0
  120. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
  121. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
  122. {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
  123. {geo_activity_playground-0.29.1 → 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.1
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
@@ -89,7 +89,10 @@ def build_activity_meta() -> None:
89
89
  new_shard = pd.DataFrame(rows)
90
90
  new_shard.index = new_shard["id"]
91
91
  new_shard.index.name = "index"
92
- meta = pd.concat([meta, new_shard])
92
+ if len(meta):
93
+ meta = pd.concat([meta, new_shard])
94
+ else:
95
+ meta = new_shard
93
96
 
94
97
  if len(meta):
95
98
  assert pd.api.types.is_dtype_equal(meta["start"].dtype, "datetime64[ns]"), (
@@ -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
@@ -137,9 +137,9 @@ def try_import_strava(config: Config) -> bool:
137
137
  **{
138
138
  "id": activity.id,
139
139
  "commute": activity.commute,
140
- "distance_km": activity.distance.magnitude / 1000,
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],
@@ -72,6 +72,7 @@ class ActivityController:
72
72
  }
73
73
 
74
74
  new_tiles_geojson = {}
75
+ new_tiles_per_zoom = {}
75
76
  for zoom in sorted(self._config.explorer_zoom_levels):
76
77
  new_tiles = self._tile_visit_accessor.tile_state["tile_history"][zoom].loc[
77
78
  self._tile_visit_accessor.tile_state["tile_history"][zoom][
@@ -88,6 +89,7 @@ class ActivityController:
88
89
  zoom,
89
90
  )
90
91
  new_tiles_geojson[zoom] = make_grid_file_geojson(points)
92
+ new_tiles_per_zoom[zoom] = len(new_tiles)
91
93
 
92
94
  result = {
93
95
  "activity": activity,
@@ -100,7 +102,7 @@ class ActivityController:
100
102
  "speed_color_bar": make_speed_color_bar(time_series),
101
103
  "date": activity["start"].date(),
102
104
  "time": activity["start"].time(),
103
- "new_tiles": new_tiles,
105
+ "new_tiles": new_tiles_per_zoom,
104
106
  "new_tiles_geojson": new_tiles_geojson,
105
107
  }
106
108
  if (
@@ -113,6 +115,8 @@ class ActivityController:
113
115
  result["altitude_time_plot"] = altitude_time_plot(time_series)
114
116
  if "heartrate" in time_series.columns:
115
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)
116
120
  return result
117
121
 
118
122
  def render_sharepic(self, id: int) -> bytes:
@@ -322,6 +326,20 @@ def heart_rate_time_plot(time_series: pd.DataFrame) -> str:
322
326
  )
323
327
 
324
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
+
325
343
  def heart_rate_zone_plot(heart_zones: pd.DataFrame) -> str:
326
344
  return (
327
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>
@@ -160,6 +171,7 @@
160
171
  {% for zoom, geojson in new_tiles_geojson.items() %}
161
172
  <div class="col-md-6">
162
173
  <h3>Zoom {{ zoom }}</h3>
174
+ <p>There are {{ new_tiles[zoom] }} new tiles:</p>
163
175
  <div id="map-{{ zoom }}" style="height: 300px; width: 100%;"></div>
164
176
  <script>
165
177
  let map{{ zoom }} = add_map("{{ zoom }}", {{ geojson | safe }})
@@ -176,11 +188,11 @@
176
188
 
177
189
  <p><a href="{{ url_for('.name', name=activity['name']) }}">Overview over these activities</a></p>
178
190
 
179
- <table class="table">
191
+ <table class="table table-sort table-arrows">
180
192
  <thead>
181
193
  <tr>
182
194
  <th>Date</th>
183
- <th>Distance / km</th>
195
+ <th class="numeric-sort">Distance / km</th>
184
196
  <th>Elapsed time</th>
185
197
  <th>Equipment</th>
186
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 %}
@@ -4,8 +4,7 @@
4
4
 
5
5
  <h1 class="mb-3">Metadata Extraction</h1>
6
6
 
7
- <p>There are a few metadata fields that can be populated with information from the path of the activity file or by a
8
- default value. These are:</p>
7
+ <p>If the current activity metadata is not to your liking, you can adjust how these fields are populated:</p>
9
8
 
10
9
  <ul>
11
10
  <li><tt>kind</tt>: The kind of the activity, like "Ride" or "Run".</li>
@@ -13,8 +12,9 @@
13
12
  <li><tt>name</tt>: Name for the activity, like "Ride with Friends".</li>
14
13
  </ul>
15
14
 
16
- <p>By default these fields are populated with information from within the activity files. In case one hasn't set these
17
- values correctly, it can be nice to override this data.</p>
15
+ <p>By default these fields are extracted from files that contain this data. Otherwise the filename is set as <tt>name</tt>.
16
+ You can overwrite this by setting up a directory structure with corresponding regular expressions using named capture groups.</p>
17
+
18
18
 
19
19
  <form method="POST">
20
20
  <div class="row">
@@ -32,19 +32,18 @@
32
32
  </div>
33
33
 
34
34
  <div class="col-md-6">
35
- <p>To give an example of what is possible, consider a directory structure where we have
36
- <tt>{kind}/{equipment}/{date} {name}.{ext}</tt>. Such an activity could be <tt>Ride/Red
37
- Roadbike/2024-08-10 11-45-00 Ride with Friends.fit</tt>. In order to extract this, we could use the
38
- following regular expression:
39
- </p>
35
+ <p>Consider a directory structure <tt>Ride/Red Roadbike/2024-08-10 11-45-00 Ride with Friends.fit</tt> under Activities.
36
+ You can extract <tt>kind</tt>: "Ride", <tt>equipment</tt>: "Red Roadbike", <tt>name</tt>: "Ride with Friends"
37
+ with the following regular expression:</p>
40
38
 
41
39
  <div class="code">
42
40
  <pre
43
41
  class="code literal-block">(?P&lt;kind&gt;[^/]+)/(?P&lt;equipment&gt;[^/]+)/[-\d_ .]+(?P&lt;name&gt;[^/\.]+)</pre>
44
42
  </div>
45
43
 
46
- <p>This uses "capture groups". Have a look at the <a href="https://docs.python.org/3/library/re.html">Python
47
- regex documentation</a> if you want to build your own.</p>
44
+ <p>Have a look at the documentation
45
+ <a href="https://martin-ueding.github.io/geo-activity-playground/getting-started/advanced-metadata-extraction">Advanced Metadata Extraction</a>
46
+ for explanations and examples.</p>
48
47
  </div>
49
48
  </div>
50
49
 
@@ -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 %}