geo-activity-playground 0.29.0__tar.gz → 0.34.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/PKG-INFO +5 -6
  2. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/__main__.py +1 -1
  3. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/activities.py +18 -9
  4. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/config.py +7 -1
  5. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/enrichment.py +18 -7
  6. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/heatmap.py +61 -15
  7. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/paths.py +3 -2
  8. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/tiles.py +8 -5
  9. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/time_conversion.py +1 -1
  10. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/explorer/tile_visits.py +2 -2
  11. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/explorer/video.py +2 -1
  12. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/activity_parsers.py +28 -17
  13. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/csv_parser.py +1 -2
  14. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/directory.py +8 -3
  15. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/strava_api.py +2 -2
  16. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/strava_checkout.py +8 -2
  17. geo_activity_playground-0.34.1/geo_activity_playground/webui/activity/blueprint.py +116 -0
  18. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/controller.py +146 -44
  19. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +10 -6
  20. geo_activity_playground-0.34.1/geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +42 -0
  21. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +1 -1
  22. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +7 -6
  23. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +23 -9
  24. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/app.py +29 -6
  25. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/entry_controller.py +1 -1
  26. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/equipment/controller.py +1 -1
  27. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +1 -1
  28. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/explorer/controller.py +2 -2
  29. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +20 -44
  30. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/heatmap/blueprint.py +5 -2
  31. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/heatmap/heatmap_controller.py +22 -10
  32. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
  33. geo_activity_playground-0.34.1/geo_activity_playground/webui/search/blueprint.py +101 -0
  34. geo_activity_playground-0.34.1/geo_activity_playground/webui/search/templates/search/index.html.j2 +91 -0
  35. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/blueprint.py +90 -33
  36. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/controller.py +4 -3
  37. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +11 -2
  38. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +18 -0
  39. geo_activity_playground-0.34.1/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +25 -0
  40. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +10 -11
  41. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +1 -1
  42. geo_activity_playground-0.34.1/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +27 -0
  43. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/square_planner/controller.py +2 -0
  44. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +1 -1
  45. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +1 -0
  46. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/MarkerCluster.Default.css +60 -0
  47. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/MarkerCluster.css +14 -0
  48. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  49. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/bootstrap.min.css +6 -0
  50. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  51. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/favicon.ico +0 -0
  52. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/favicon.svg +3 -0
  53. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/fullscreen.png +0 -0
  54. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  55. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/leaflet.css +661 -0
  56. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/leaflet.fullscreen.css +40 -0
  57. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/leaflet.js +6 -0
  58. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/leaflet.markercluster.js +3 -0
  59. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/site.webmanifest +21 -0
  60. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/table-sort.min.js +8 -0
  61. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/vega-embed@6 +7 -0
  62. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/vega-lite@4 +2 -0
  63. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/vega@5 +2 -0
  64. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  65. geo_activity_playground-0.34.1/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  66. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/summary/controller.py +20 -25
  67. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +20 -7
  68. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/templates/home.html.j2 +7 -13
  69. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/templates/page.html.j2 +16 -18
  70. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/tile/blueprint.py +3 -2
  71. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/tile/controller.py +7 -3
  72. geo_activity_playground-0.29.0/geo_activity_playground/webui/upload/controller.py → geo_activity_playground-0.34.1/geo_activity_playground/webui/upload_blueprint.py +42 -36
  73. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/pyproject.toml +8 -8
  74. geo_activity_playground-0.29.0/geo_activity_playground/webui/activity/blueprint.py +0 -57
  75. geo_activity_playground-0.29.0/geo_activity_playground/webui/search/blueprint.py +0 -20
  76. geo_activity_playground-0.29.0/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -38
  77. geo_activity_playground-0.29.0/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  78. geo_activity_playground-0.29.0/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  79. geo_activity_playground-0.29.0/geo_activity_playground/webui/static/favicon.ico +0 -0
  80. geo_activity_playground-0.29.0/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -121
  81. geo_activity_playground-0.29.0/geo_activity_playground/webui/static/site.webmanifest +0 -19
  82. geo_activity_playground-0.29.0/geo_activity_playground/webui/upload/__init__.py +0 -0
  83. geo_activity_playground-0.29.0/geo_activity_playground/webui/upload/blueprint.py +0 -44
  84. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/LICENSE +0 -0
  85. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/__init__.py +0 -0
  86. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/__init__.py +0 -0
  87. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/coordinates.py +0 -0
  88. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/heart_rate.py +0 -0
  89. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/privacy_zones.py +0 -0
  90. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/similarity.py +0 -0
  91. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/tasks.py +0 -0
  92. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/test_tiles.py +0 -0
  93. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/core/test_time_conversion.py +0 -0
  94. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/explorer/__init__.py +0 -0
  95. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/explorer/grid_file.py +0 -0
  96. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/__init__.py +0 -0
  97. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  98. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/test_directory.py +0 -0
  99. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
  100. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/__init__.py +0 -0
  101. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/activity/__init__.py +0 -0
  102. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  103. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  104. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/authenticator.py +0 -0
  105. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  106. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  107. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/calendar/controller.py +0 -0
  108. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  109. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  110. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  111. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  112. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/eddington/controller.py +0 -0
  113. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  114. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  115. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  116. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  117. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  118. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  119. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/plot_util.py +0 -0
  120. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  121. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  122. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  123. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  124. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  125. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  126. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  127. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  128. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  129. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  130. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  131. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  132. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  133. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  134. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  135. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/summary/__init__.py +0 -0
  136. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/summary/blueprint.py +0 -0
  137. {geo_activity_playground-0.29.0/geo_activity_playground/webui/upload → geo_activity_playground-0.34.1/geo_activity_playground/webui}/templates/upload/index.html.j2 +0 -0
  138. {geo_activity_playground-0.29.0/geo_activity_playground/webui/upload → geo_activity_playground-0.34.1/geo_activity_playground/webui}/templates/upload/reload.html.j2 +0 -0
  139. {geo_activity_playground-0.29.0 → geo_activity_playground-0.34.1}/geo_activity_playground/webui/tile/__init__.py +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.29.0
3
+ Version: 0.34.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
7
7
  Author-email: mu@martin-ueding.de
8
- Requires-Python: >=3.10,<3.13
8
+ Requires-Python: >=3.10,<3.14
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: Pillow (>=10.3.0,<11.0.0)
14
+ Requires-Dist: Pillow (>=11.0.0,<12.0.0)
15
15
  Requires-Dist: altair (>=5.1.2,<6.0.0)
16
16
  Requires-Dist: appdirs (>=1.4.4,<2.0.0)
17
17
  Requires-Dist: charset-normalizer (>=3.3.2,<4.0.0)
@@ -22,12 +22,11 @@ Requires-Dist: geojson (>=3.0.1,<4.0.0)
22
22
  Requires-Dist: gpxpy (>=1.5.0,<2.0.0)
23
23
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
24
24
  Requires-Dist: matplotlib (>=3.6.3,<4.0.0)
25
- Requires-Dist: numpy (>=1.22.4,<2.0.0)
25
+ Requires-Dist: numpy (>=2.1.3,<3.0.0)
26
26
  Requires-Dist: pandas (>=2.2,<3.0)
27
- Requires-Dist: pyarrow (>=16.1.0,<17.0.0)
27
+ Requires-Dist: pyarrow (>=18.1.0,<19.0.0)
28
28
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
29
29
  Requires-Dist: requests (>=2.28.1,<3.0.0)
30
- Requires-Dist: scipy (>=1.8.1,<2.0.0)
31
30
  Requires-Dist: shapely (>=2.0.5,<3.0.0)
32
31
  Requires-Dist: stravalib (>=2.0,<3.0)
33
32
  Requires-Dist: tcxreader (>=0.4.5,<0.5.0)
@@ -13,7 +13,7 @@ from geo_activity_playground.core.config import import_old_strava_config
13
13
  from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
14
14
  from geo_activity_playground.explorer.video import explorer_video_main
15
15
  from geo_activity_playground.webui.app import web_ui_main
16
- from geo_activity_playground.webui.upload.controller import scan_for_activities
16
+ from geo_activity_playground.webui.upload_blueprint import scan_for_activities
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -1,7 +1,9 @@
1
1
  import datetime
2
2
  import functools
3
+ import json
3
4
  import logging
4
5
  import pickle
6
+ from typing import Any
5
7
  from typing import Iterator
6
8
  from typing import Optional
7
9
  from typing import TypedDict
@@ -15,6 +17,7 @@ from tqdm import tqdm
15
17
  from geo_activity_playground.core.paths import activities_file
16
18
  from geo_activity_playground.core.paths import activity_enriched_meta_dir
17
19
  from geo_activity_playground.core.paths import activity_enriched_time_series_dir
20
+ from geo_activity_playground.core.paths import activity_meta_override_dir
18
21
 
19
22
  logger = logging.getLogger(__name__)
20
23
 
@@ -82,13 +85,21 @@ def build_activity_meta() -> None:
82
85
  rows = []
83
86
  for new_id in tqdm(new_ids, desc="Register new activities"):
84
87
  with open(activity_enriched_meta_dir() / f"{new_id}.pickle", "rb") as f:
85
- rows.append(pickle.load(f))
88
+ data = pickle.load(f)
89
+ override_file = activity_meta_override_dir() / f"{new_id}.json"
90
+ if override_file.exists():
91
+ with open(override_file) as f:
92
+ data.update(json.load(f))
93
+ rows.append(data)
86
94
 
87
95
  if rows:
88
96
  new_shard = pd.DataFrame(rows)
89
97
  new_shard.index = new_shard["id"]
90
98
  new_shard.index.name = "index"
91
- meta = pd.concat([meta, new_shard])
99
+ if len(meta):
100
+ meta = pd.concat([meta, new_shard])
101
+ else:
102
+ meta = new_shard
92
103
 
93
104
  if len(meta):
94
105
  assert pd.api.types.is_dtype_equal(meta["start"].dtype, "datetime64[ns]"), (
@@ -103,7 +114,7 @@ def build_activity_meta() -> None:
103
114
 
104
115
  class ActivityRepository:
105
116
  def __init__(self) -> None:
106
- self.meta = None
117
+ self.meta = pd.DataFrame()
107
118
 
108
119
  def __len__(self) -> int:
109
120
  return len(self.meta)
@@ -116,10 +127,6 @@ class ActivityRepository:
116
127
  if activity_id in self.meta["id"]:
117
128
  return True
118
129
 
119
- for activity_meta in self._loose_activities:
120
- if activity_meta["id"] == activity_id:
121
- return True
122
-
123
130
  return False
124
131
 
125
132
  def last_activity_date(self) -> Optional[datetime.datetime]:
@@ -140,7 +147,6 @@ class ActivityRepository:
140
147
  if not dropna or not pd.isna(row["start"]):
141
148
  yield row
142
149
 
143
- @functools.lru_cache()
144
150
  def get_activity_by_id(self, id: int) -> ActivityMeta:
145
151
  activity = self.meta.loc[id]
146
152
  assert isinstance(activity["name"], str), activity["name"]
@@ -158,6 +164,9 @@ class ActivityRepository:
158
164
 
159
165
  return df
160
166
 
167
+ def save(self) -> None:
168
+ self.meta.to_parquet(activities_file())
169
+
161
170
 
162
171
  def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
163
172
  fc = geojson.FeatureCollection(
@@ -198,7 +207,7 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
198
207
  return geojson.dumps(feature_collection)
199
208
 
200
209
 
201
- def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
210
+ def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
202
211
  speed_without_na = time_series["speed"].dropna()
203
212
  low = min(speed_without_na)
204
213
  high = max(speed_without_na)
@@ -21,14 +21,17 @@ logger = logging.getLogger(__name__)
21
21
  @dataclasses.dataclass
22
22
  class Config:
23
23
  birth_year: Optional[int] = None
24
- color_scheme_for_counts: str = "viridis"
24
+ color_scheme_for_counts: str = "teals"
25
25
  color_scheme_for_kind: str = "category10"
26
+ color_scheme_for_heatmap: str = "hot"
26
27
  equipment_offsets: dict[str, float] = dataclasses.field(default_factory=dict)
27
28
  explorer_zoom_levels: list[int] = dataclasses.field(
28
29
  default_factory=lambda: [14, 17]
29
30
  )
30
31
  heart_rate_resting: int = 0
31
32
  heart_rate_maximum: Optional[int] = None
33
+ ignore_suffixes: list[str] = dataclasses.field(default_factory=list)
34
+ kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
32
35
  kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
33
36
  metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
34
37
  num_processes: Optional[int] = 1
@@ -39,7 +42,10 @@ class Config:
39
42
  strava_client_id: int = 131693
40
43
  strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
41
44
  strava_client_code: Optional[str] = None
45
+ time_diff_threshold_seconds: Optional[int] = 30
42
46
  upload_password: Optional[str] = None
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>'
43
49
 
44
50
 
45
51
  class ConfigAccessor:
@@ -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)
@@ -6,6 +6,7 @@ import logging
6
6
 
7
7
  import numpy as np
8
8
 
9
+ from geo_activity_playground.core.config import Config
9
10
  from geo_activity_playground.core.tiles import compute_tile_float
10
11
  from geo_activity_playground.core.tiles import get_tile
11
12
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
@@ -123,21 +124,66 @@ def get_sensible_zoom_level(
123
124
  )
124
125
 
125
126
 
126
- def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
127
- background = np.zeros((*PixelBounds.from_tile_bounds(tile_bounds).shape, 3))
128
-
129
- for x in range(tile_bounds.x_tile_min, tile_bounds.x_tile_max):
130
- for y in range(tile_bounds.y_tile_min, tile_bounds.y_tile_max):
131
- tile = np.array(get_tile(tile_bounds.zoom, x, y)) / 255
132
-
133
- i = y - tile_bounds.y_tile_min
134
- j = x - tile_bounds.x_tile_min
135
-
136
- background[
137
- i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
138
- j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
139
- :,
140
- ] = tile[:, :, :3]
127
+ def build_map_from_tiles_around_center(
128
+ center: tuple[float, float],
129
+ zoom: int,
130
+ target: tuple[int, int],
131
+ inner_target: tuple[int, int],
132
+ config: Config,
133
+ ) -> np.ndarray:
134
+ background = np.zeros((target[1], target[0], 3))
135
+
136
+ # We will work with the center point and have it in terms of tiles `t` and also in terms of pixels `p`. At the start we know that the tile center must be in the middle of the image.
137
+ t = np.array(center)
138
+ p = np.array([inner_target[0] / 2, inner_target[1] / 2])
139
+
140
+ # Shift both such that they are in the top-left corner of an even tile.
141
+ t_offset = np.array([center[0] % 1, center[1] % 1])
142
+ t -= t_offset
143
+ p -= t_offset * OSM_TILE_SIZE
144
+
145
+ # Shift until we have left the image.
146
+ shift = np.ceil(p / OSM_TILE_SIZE)
147
+ p -= shift * OSM_TILE_SIZE
148
+ t -= shift
149
+
150
+ num_tiles = np.ceil(np.array(target) / OSM_TILE_SIZE) + 1
151
+
152
+ for x in range(int(t[0]), int(t[0] + num_tiles[0])):
153
+ for y in range(int(t[1]), int(t[1]) + int(num_tiles[1])):
154
+ source_x_min = 0
155
+ source_y_min = 0
156
+ source_x_max = source_x_min + OSM_TILE_SIZE
157
+ source_y_max = source_y_min + OSM_TILE_SIZE
158
+
159
+ target_x_min = (x - int(t[0])) * OSM_TILE_SIZE + int(p[0])
160
+ target_y_min = (y - int(t[1])) * OSM_TILE_SIZE + int(p[1])
161
+ target_x_max = target_x_min + OSM_TILE_SIZE
162
+ target_y_max = target_y_min + OSM_TILE_SIZE
163
+
164
+ if target_x_min < 0:
165
+ source_x_min -= target_x_min
166
+ target_x_min = 0
167
+ if target_y_min < 0:
168
+ source_y_min -= target_y_min
169
+ target_y_min = 0
170
+ if target_x_max > target[0]:
171
+ a = target_x_max - target[0]
172
+ target_x_max -= a
173
+ source_x_max -= a
174
+ if target_y_max > target[1]:
175
+ a = target_y_max - target[1]
176
+ target_y_max -= a
177
+ source_y_max -= a
178
+
179
+ if source_x_max < 0 or source_y_max < 0:
180
+ continue
181
+
182
+ tile = np.array(get_tile(zoom, x, y, config.map_tile_url)) / 255
183
+
184
+ background[target_y_min:target_y_max, target_x_min:target_x_max] = tile[
185
+ source_y_min:source_y_max, source_x_min:source_x_max, :3
186
+ ]
141
187
 
142
188
  return background
143
189
 
@@ -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
@@ -55,6 +53,8 @@ _strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
55
53
 
56
54
  _new_config_file = pathlib.Path("config.json")
57
55
 
56
+ _activity_meta_override_dir = pathlib.Path("Metadata Override")
57
+
58
58
 
59
59
  cache_dir = dir_wrapper(_cache_dir)
60
60
 
@@ -65,6 +65,7 @@ activity_enriched_meta_dir = dir_wrapper(_activity_enriched_meta_dir)
65
65
  activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_dir)
66
66
  tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
67
67
  strava_api_dir = dir_wrapper(_strava_api_dir)
68
+ activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
68
69
 
69
70
  activities_file = file_wrapper(_activities_file)
70
71
  strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
@@ -3,6 +3,7 @@ import logging
3
3
  import math
4
4
  import pathlib
5
5
  import time
6
+ import urllib.parse
6
7
  from typing import Iterator
7
8
  from typing import Optional
8
9
 
@@ -13,8 +14,10 @@ from PIL import Image
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
16
- def osm_tile_path(x: int, y: int, zoom: int) -> pathlib.Path:
17
- path = pathlib.Path("Open Street Map Tiles") / f"{zoom}/{x}/{y}.png"
17
+ def osm_tile_path(x: int, y: int, zoom: int, url_template: str) -> pathlib.Path:
18
+ base_dir = pathlib.Path("Open Street Map Tiles")
19
+ dir_for_source = base_dir / urllib.parse.quote_plus(url_template)
20
+ path = dir_for_source / f"{zoom}/{x}/{y}.png"
18
21
  path.parent.mkdir(parents=True, exist_ok=True)
19
22
  return path
20
23
 
@@ -62,11 +65,11 @@ def download_file(url: str, destination: pathlib.Path):
62
65
 
63
66
 
64
67
  @functools.lru_cache()
65
- def get_tile(zoom: int, x: int, y: int) -> Image.Image:
66
- destination = osm_tile_path(x, y, zoom)
68
+ def get_tile(zoom: int, x: int, y: int, url_template: str) -> Image.Image:
69
+ destination = osm_tile_path(x, y, zoom, url_template)
67
70
  if not destination.exists():
68
71
  logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
69
- url = f"https://tile.openstreetmap.org/{zoom}/{x}/{y}.png"
72
+ url = url_template.format(x=x, y=y, zoom=zoom)
70
73
  download_file(url, destination)
71
74
  with Image.open(destination) as image:
72
75
  image.load()
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pandas as pd
3
3
 
4
4
 
5
- def convert_to_datetime_ns(date) -> np.datetime64:
5
+ def convert_to_datetime_ns(date) -> np.datetime64 | pd.Series:
6
6
  if isinstance(date, pd.Series):
7
7
  ts = pd.to_datetime(date)
8
8
  ts = ts.dt.tz_localize(None)
@@ -59,7 +59,7 @@ class TileEvolutionState:
59
59
  class TileState(TypedDict):
60
60
  tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
61
61
  tile_history: dict[int, pd.DataFrame]
62
- activities_per_tile: dict[int, set[int]]
62
+ activities_per_tile: dict[int, dict[tuple[int, int], set[int]]]
63
63
  processed_activities: set[int]
64
64
  evolution_state: dict[int, TileEvolutionState]
65
65
  version: int
@@ -165,7 +165,7 @@ def _process_activity(
165
165
  for zoom in reversed(range(20)):
166
166
  activities_per_tile = tile_state["activities_per_tile"][zoom]
167
167
 
168
- new_tile_history_soa = {
168
+ new_tile_history_soa: dict[str, list] = {
169
169
  "activity_id": [],
170
170
  "time": [],
171
171
  "tile_x": [],
@@ -9,13 +9,14 @@ from typing import Tuple
9
9
 
10
10
  import numpy as np
11
11
  import pandas as pd
12
- import scipy.interpolate
13
12
  from PIL import Image
14
13
  from PIL import ImageEnhance
15
14
  from tqdm import tqdm
16
15
 
17
16
  from ..core.tiles import get_tile
18
17
 
18
+ # import scipy.interpolate
19
+
19
20
 
20
21
  def build_image(
21
22
  center_x: float,
@@ -3,10 +3,10 @@ import gzip
3
3
  import logging
4
4
  import pathlib
5
5
  import xml
6
+ from collections.abc import Iterator
6
7
 
7
8
  import charset_normalizer
8
9
  import dateutil.parser
9
- import fitdecode
10
10
  import fitdecode.exceptions
11
11
  import gpxpy
12
12
  import pandas as pd
@@ -246,26 +246,37 @@ def read_kml_activity(path: pathlib.Path, opener) -> pd.DataFrame:
246
246
  with opener(path, "rb") as f:
247
247
  kml_dict = xmltodict.parse(f)
248
248
  doc = kml_dict["kml"]["Document"]
249
- keypoint_folder = doc["Folder"]
250
- placemark = keypoint_folder["Placemark"]
251
- track = placemark["gx:Track"]
252
249
  rows = []
253
- for when, where in zip(track["when"], track["gx:coord"]):
254
- time = dateutil.parser.parse(when)
255
- time = convert_to_datetime_ns(time)
256
- parts = where.split(" ")
257
- if len(parts) == 2:
258
- lon, lat = parts
259
- alt = None
260
- if len(parts) == 3:
261
- lon, lat, alt = parts
262
- row = {"time": time, "latitude": float(lat), "longitude": float(lon)}
263
- if alt is not None:
264
- row["altitude"] = float(alt)
265
- rows.append(row)
250
+ for keypoint_folder in _list_or_scalar(doc["Folder"]):
251
+ for placemark in _list_or_scalar(keypoint_folder["Placemark"]):
252
+ for track in _list_or_scalar(placemark.get("gx:Track", [])):
253
+ for when, where in zip(track["when"], track["gx:coord"]):
254
+ time = dateutil.parser.parse(when)
255
+ time = convert_to_datetime_ns(time)
256
+ parts = where.split(" ")
257
+ if len(parts) == 2:
258
+ lon, lat = parts
259
+ alt = None
260
+ if len(parts) == 3:
261
+ lon, lat, alt = parts
262
+ row = {
263
+ "time": time,
264
+ "latitude": float(lat),
265
+ "longitude": float(lon),
266
+ }
267
+ if alt is not None:
268
+ row["altitude"] = float(alt)
269
+ rows.append(row)
266
270
  return pd.DataFrame(rows)
267
271
 
268
272
 
273
+ def _list_or_scalar(thing) -> Iterator:
274
+ if isinstance(thing, list):
275
+ yield from thing
276
+ else:
277
+ yield thing
278
+
279
+
269
280
  def read_simra_activity(path: pathlib.Path, opener) -> pd.DataFrame:
270
281
  data = pd.read_csv(path, header=1)
271
282
  data["time"] = data["timeStamp"].apply(
@@ -20,9 +20,8 @@ This module implements a "recursive descent parser" that parses this grammar.
20
20
 
21
21
  def parse_csv(text: str) -> list[list]:
22
22
  text = text.strip() + "\n"
23
- result = {}
24
23
  index = 0
25
- result = []
24
+ result: list[list] = []
26
25
  while index < len(text):
27
26
  line, index = _parse_line(text, index)
28
27
  result.append(line)
@@ -10,6 +10,7 @@ from typing import Optional
10
10
  from tqdm import tqdm
11
11
 
12
12
  from geo_activity_playground.core.activities import ActivityMeta
13
+ from geo_activity_playground.core.config import Config
13
14
  from geo_activity_playground.core.paths import activity_extracted_dir
14
15
  from geo_activity_playground.core.paths import activity_extracted_meta_dir
15
16
  from geo_activity_playground.core.paths import activity_extracted_time_series_dir
@@ -24,13 +25,16 @@ ACTIVITY_DIR = pathlib.Path("Activities")
24
25
 
25
26
 
26
27
  def import_from_directory(
27
- metadata_extraction_regexes: list[str], num_processes: Optional[int]
28
+ metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
28
29
  ) -> None:
29
30
 
30
31
  activity_paths = [
31
32
  path
32
33
  for path in ACTIVITY_DIR.rglob("*.*")
33
- if path.is_file() and path.suffixes and not path.stem.startswith(".")
34
+ if path.is_file()
35
+ and path.suffixes
36
+ and not path.stem.startswith(".")
37
+ and not path.suffix in config.ignore_suffixes
34
38
  ]
35
39
  work_tracker = WorkTracker(activity_extracted_dir() / "work-tracker-extract.pickle")
36
40
  new_activity_paths = work_tracker.filter(activity_paths)
@@ -126,11 +130,12 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
126
130
  raise
127
131
 
128
132
  if len(timeseries) == 0:
129
- return
133
+ return None
130
134
 
131
135
  timeseries.to_parquet(timeseries_path)
132
136
  with open(file_metadata_path, "wb") as f:
133
137
  pickle.dump(activity_meta_from_file, f)
138
+ return None
134
139
 
135
140
 
136
141
  def get_file_hash(path: pathlib.Path) -> int:
@@ -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],
@@ -146,7 +146,7 @@ def float_with_comma_or_period(x: str) -> Optional[float]:
146
146
 
147
147
  def import_from_strava_checkout() -> None:
148
148
  checkout_path = pathlib.Path("Strava Export")
149
- with open(checkout_path / "activities.csv") as f:
149
+ with open(checkout_path / "activities.csv", encoding="utf-8") as f:
150
150
  rows = parse_csv(f.read())
151
151
  header = rows[0]
152
152
 
@@ -159,9 +159,15 @@ def import_from_strava_checkout() -> None:
159
159
 
160
160
  if header[0] == EXPECTED_COLUMNS[0]:
161
161
  dayfirst = False
162
- if header[0] == "Aktivitäts-ID":
162
+ elif header[0] == "Aktivitäts-ID":
163
163
  header = EXPECTED_COLUMNS
164
164
  dayfirst = True
165
+ else:
166
+ logger.error(
167
+ f"You are trying to import a Strava checkout where the `activities.csv` contains an unexpected header format. In order to import this, we need to map these to the English ones. Unfortunately Strava often changes the number of columns. This means that the program needs to be updated to match the new Strava export format. Please go to https://github.com/martin-ueding/geo-activity-playground/issues and open a new issue and share the following output in the ticket:"
168
+ )
169
+ print(header)
170
+ sys.exit(1)
165
171
 
166
172
  table = {
167
173
  header[i]: [rows[r][i] for r in range(1, len(rows))] for i in range(len(header))