geo-activity-playground 0.28.0__tar.gz → 0.29.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 (113) hide show
  1. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/PKG-INFO +3 -4
  2. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/activities.py +3 -6
  3. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/config.py +3 -0
  4. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/paths.py +10 -0
  5. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/tasks.py +5 -4
  6. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/time_conversion.py +1 -1
  7. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/tile_visits.py +43 -9
  8. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/activity_parsers.py +28 -17
  9. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/csv_parser.py +1 -2
  10. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/directory.py +2 -1
  11. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/controller.py +22 -1
  12. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +33 -0
  13. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/app.py +10 -20
  14. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/authenticator.py +0 -3
  15. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/entry_controller.py +8 -4
  16. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/controller.py +3 -2
  17. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
  18. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/heatmap_controller.py +1 -0
  19. geo_activity_playground-0.29.1/geo_activity_playground/webui/plot_util.py +9 -0
  20. geo_activity_playground-0.29.1/geo_activity_playground/webui/search/blueprint.py +20 -0
  21. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/blueprint.py +69 -0
  22. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/controller.py +4 -3
  23. geo_activity_playground-0.29.1/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
  24. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +9 -0
  25. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/controller.py +2 -0
  26. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/blueprint.py +3 -2
  27. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/controller.py +20 -13
  28. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/templates/home.html.j2 +1 -1
  29. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/templates/page.html.j2 +56 -28
  30. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/pyproject.toml +3 -3
  31. geo_activity_playground-0.28.0/geo_activity_playground/webui/search_controller.py +0 -19
  32. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/LICENSE +0 -0
  33. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/__init__.py +0 -0
  34. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/__main__.py +0 -0
  35. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/__init__.py +0 -0
  36. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/coordinates.py +0 -0
  37. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/enrichment.py +0 -0
  38. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/heart_rate.py +0 -0
  39. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/heatmap.py +0 -0
  40. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/privacy_zones.py +0 -0
  41. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/similarity.py +0 -0
  42. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/test_tiles.py +0 -0
  43. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/test_time_conversion.py +0 -0
  44. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/tiles.py +0 -0
  45. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/__init__.py +0 -0
  46. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/grid_file.py +0 -0
  47. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/video.py +0 -0
  48. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/__init__.py +0 -0
  49. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/strava_api.py +0 -0
  50. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/strava_checkout.py +0 -0
  51. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  52. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_directory.py +0 -0
  53. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
  54. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/__init__.py +0 -0
  55. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/__init__.py +0 -0
  56. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  57. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -0
  58. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  59. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +0 -0
  60. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  61. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  62. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  63. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  64. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/controller.py +0 -0
  65. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  66. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  67. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  68. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  69. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/controller.py +0 -0
  70. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  71. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  72. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  73. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/controller.py +0 -0
  74. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
  75. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  76. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  77. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  78. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
  79. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
  80. /geo_activity_playground-0.28.0/geo_activity_playground/webui/templates/search.html.j2 → /geo_activity_playground-0.29.1/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -0
  81. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  82. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  83. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  84. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  85. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  86. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  87. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  88. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  89. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  90. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  91. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
  92. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  93. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  94. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  95. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  96. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  97. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  98. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  99. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  100. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
  101. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  102. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  103. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  104. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/__init__.py +0 -0
  105. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +0 -0
  106. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/__init__.py +0 -0
  107. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/blueprint.py +0 -0
  108. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/controller.py +0 -0
  109. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/__init__.py +0 -0
  110. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/blueprint.py +0 -0
  111. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/controller.py +0 -0
  112. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
  113. {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.28.0
3
+ Version: 0.29.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.9,<3.13
8
+ Requires-Python: >=3.10,<3.13
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
@@ -30,7 +29,7 @@ Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
30
29
  Requires-Dist: requests (>=2.28.1,<3.0.0)
31
30
  Requires-Dist: scipy (>=1.8.1,<2.0.0)
32
31
  Requires-Dist: shapely (>=2.0.5,<3.0.0)
33
- Requires-Dist: stravalib (>=1.3.3,<2.0.0)
32
+ Requires-Dist: stravalib (>=2.0,<3.0)
34
33
  Requires-Dist: tcxreader (>=0.4.5,<0.5.0)
35
34
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
36
35
  Requires-Dist: tqdm (>=4.64.0,<5.0.0)
@@ -2,6 +2,7 @@ import datetime
2
2
  import functools
3
3
  import logging
4
4
  import pickle
5
+ from typing import Any
5
6
  from typing import Iterator
6
7
  from typing import Optional
7
8
  from typing import TypedDict
@@ -103,7 +104,7 @@ def build_activity_meta() -> None:
103
104
 
104
105
  class ActivityRepository:
105
106
  def __init__(self) -> None:
106
- self.meta = None
107
+ self.meta = pd.DataFrame()
107
108
 
108
109
  def __len__(self) -> int:
109
110
  return len(self.meta)
@@ -116,10 +117,6 @@ class ActivityRepository:
116
117
  if activity_id in self.meta["id"]:
117
118
  return True
118
119
 
119
- for activity_meta in self._loose_activities:
120
- if activity_meta["id"] == activity_id:
121
- return True
122
-
123
120
  return False
124
121
 
125
122
  def last_activity_date(self) -> Optional[datetime.datetime]:
@@ -198,7 +195,7 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
198
195
  return geojson.dumps(feature_collection)
199
196
 
200
197
 
201
- def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
198
+ def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
202
199
  speed_without_na = time_series["speed"].dropna()
203
200
  low = min(speed_without_na)
204
201
  high = max(speed_without_na)
@@ -21,6 +21,8 @@ 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"
25
+ color_scheme_for_kind: str = "category10"
24
26
  equipment_offsets: dict[str, float] = dataclasses.field(default_factory=dict)
25
27
  explorer_zoom_levels: list[int] = dataclasses.field(
26
28
  default_factory=lambda: [14, 17]
@@ -52,6 +54,7 @@ class ConfigAccessor:
52
54
  return self._config
53
55
 
54
56
  def save(self) -> None:
57
+ print(self._config)
55
58
  with open(new_config_file(), "w") as f:
56
59
  json.dump(
57
60
  dataclasses.asdict(self._config),
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Paths within the playground and cache.
3
3
  """
4
+ import contextlib
4
5
  import functools
5
6
  import pathlib
6
7
  import typing
@@ -24,6 +25,15 @@ def file_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
24
25
  return wrapper
25
26
 
26
27
 
28
+ @contextlib.contextmanager
29
+ def atomic_open(path: pathlib.Path, mode: str):
30
+ temp_path = path.with_stem(path.stem + "-temp")
31
+ with open(temp_path, mode) as f:
32
+ yield f
33
+ path.unlink(missing_ok=True)
34
+ temp_path.rename(path)
35
+
36
+
27
37
  _cache_dir = pathlib.Path("Cache")
28
38
 
29
39
  _activity_dir = _cache_dir / "Activity"
@@ -8,6 +8,7 @@ from typing import Generic
8
8
  from typing import Sequence
9
9
  from typing import TypeVar
10
10
 
11
+ from geo_activity_playground.core.paths import atomic_open
11
12
  from geo_activity_playground.core.paths import cache_dir
12
13
 
13
14
 
@@ -24,11 +25,8 @@ def stored_object(path: pathlib.Path, default):
24
25
 
25
26
  yield payload
26
27
 
27
- temp_location = path.with_suffix(".tmp")
28
- with open(temp_location, "wb") as f:
28
+ with atomic_open(path, "wb") as f:
29
29
  pickle.dump(payload, f)
30
- path.unlink(missing_ok=True)
31
- temp_location.rename(path)
32
30
 
33
31
 
34
32
  def work_tracker_path(name: str) -> pathlib.Path:
@@ -68,6 +66,9 @@ class WorkTracker:
68
66
  def discard(self, id) -> None:
69
67
  self._done.discard(id)
70
68
 
69
+ def reset(self) -> None:
70
+ self._done = set()
71
+
71
72
  def close(self) -> None:
72
73
  with open(self._path, "wb") as f:
73
74
  pickle.dump(self._done, f)
@@ -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)
@@ -14,6 +14,7 @@ from tqdm import tqdm
14
14
 
15
15
  from geo_activity_playground.core.activities import ActivityRepository
16
16
  from geo_activity_playground.core.config import Config
17
+ from geo_activity_playground.core.paths import atomic_open
17
18
  from geo_activity_playground.core.paths import tiles_per_time_series
18
19
  from geo_activity_playground.core.tasks import try_load_pickle
19
20
  from geo_activity_playground.core.tasks import work_tracker_path
@@ -58,7 +59,7 @@ class TileEvolutionState:
58
59
  class TileState(TypedDict):
59
60
  tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
60
61
  tile_history: dict[int, pd.DataFrame]
61
- activities_per_tile: dict[int, set[int]]
62
+ activities_per_tile: dict[int, dict[tuple[int, int], set[int]]]
62
63
  processed_activities: set[int]
63
64
  evolution_state: dict[int, TileEvolutionState]
64
65
  version: int
@@ -79,11 +80,12 @@ class TileVisitAccessor:
79
80
  self.tile_state = make_tile_state()
80
81
  # TODO: Reset work tracker
81
82
 
83
+ def reset(self) -> None:
84
+ self.tile_state = make_tile_state()
85
+
82
86
  def save(self) -> None:
83
- tmp_path = self.PATH.with_suffix(".tmp")
84
- with open(tmp_path, "wb") as f:
87
+ with atomic_open(self.PATH, "wb") as f:
85
88
  pickle.dump(self.tile_state, f)
86
- tmp_path.rename(self.PATH)
87
89
 
88
90
 
89
91
  def make_defaultdict_dict():
@@ -106,20 +108,52 @@ def make_tile_state() -> TileState:
106
108
  return tile_state
107
109
 
108
110
 
111
+ def _consistency_check(
112
+ repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
113
+ ) -> bool:
114
+ present_activity_ids = set(repository.get_activity_ids())
115
+
116
+ for zoom, activities_per_tile in tile_visit_accessor.tile_state[
117
+ "activities_per_tile"
118
+ ].items():
119
+ for tile, tile_activity_ids in activities_per_tile.items():
120
+ deleted_activity_ids = tile_activity_ids - present_activity_ids
121
+ if deleted_activity_ids:
122
+ logger.info(f"Activities {deleted_activity_ids} have been deleted.")
123
+ return False
124
+
125
+ for zoom, tile_visits in tile_visit_accessor.tile_state["tile_visits"].items():
126
+ for tile, meta in tile_visits.items():
127
+ if meta["first_id"] not in present_activity_ids:
128
+ logger.info(f"Activity {meta['first_id']} have been deleted.")
129
+ return False
130
+ if meta["last_id"] not in present_activity_ids:
131
+ logger.info(f"Activity {meta['last_id']} have been deleted.")
132
+ return False
133
+
134
+ return True
135
+
136
+
109
137
  def compute_tile_visits_new(
110
138
  repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
111
139
  ) -> None:
112
140
  work_tracker = WorkTracker(work_tracker_path("tile-state"))
141
+
142
+ if not _consistency_check(repository, tile_visit_accessor):
143
+ logger.warning("Need to recompute Explorer Tiles due to deleted activities.")
144
+ tile_visit_accessor.reset()
145
+ work_tracker.reset()
146
+
113
147
  for activity_id in tqdm(
114
- work_tracker.filter(repository.get_activity_ids()), desc="Tile visits (new)"
148
+ work_tracker.filter(repository.get_activity_ids()), desc="Tile visits"
115
149
  ):
116
- do_tile_stuff(repository, tile_visit_accessor.tile_state, activity_id)
150
+ _process_activity(repository, tile_visit_accessor.tile_state, activity_id)
117
151
  work_tracker.mark_done(activity_id)
118
152
  tile_visit_accessor.save()
119
153
  work_tracker.close()
120
154
 
121
155
 
122
- def do_tile_stuff(
156
+ def _process_activity(
123
157
  repository: ActivityRepository, tile_state: TileState, activity_id: int
124
158
  ) -> None:
125
159
  activity = repository.get_activity_by_id(activity_id)
@@ -131,7 +165,7 @@ def do_tile_stuff(
131
165
  for zoom in reversed(range(20)):
132
166
  activities_per_tile = tile_state["activities_per_tile"][zoom]
133
167
 
134
- new_tile_history_soa = {
168
+ new_tile_history_soa: dict[str, list] = {
135
169
  "activity_id": [],
136
170
  "time": [],
137
171
  "tile_x": [],
@@ -145,7 +179,7 @@ def do_tile_stuff(
145
179
  zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
146
180
  ):
147
181
  if activity["consider_for_achievements"]:
148
- if tile not in activities_per_tile:
182
+ if tile not in tile_state["tile_visits"][zoom]:
149
183
  new_tile_history_soa["activity_id"].append(activity_id)
150
184
  new_tile_history_soa["time"].append(time)
151
185
  new_tile_history_soa["tile_x"].append(tile[0])
@@ -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)
@@ -126,11 +126,12 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
126
126
  raise
127
127
 
128
128
  if len(timeseries) == 0:
129
- return
129
+ return None
130
130
 
131
131
  timeseries.to_parquet(timeseries_path)
132
132
  with open(file_metadata_path, "wb") as f:
133
133
  pickle.dump(activity_meta_from_file, f)
134
+ return None
134
135
 
135
136
 
136
137
  def get_file_hash(path: pathlib.Path) -> int:
@@ -12,6 +12,8 @@ import pandas as pd
12
12
  from PIL import Image
13
13
  from PIL import ImageDraw
14
14
 
15
+ from ...explorer.grid_file import make_grid_file_geojson
16
+ from ...explorer.grid_file import make_grid_points
15
17
  from geo_activity_playground.core.activities import ActivityMeta
16
18
  from geo_activity_playground.core.activities import ActivityRepository
17
19
  from geo_activity_playground.core.activities import make_geojson_color_line
@@ -66,9 +68,27 @@ class ActivityController:
66
68
  ]
67
69
  == activity["id"]
68
70
  )
69
- for zoom in [14, 17]
71
+ for zoom in sorted(self._config.explorer_zoom_levels)
70
72
  }
71
73
 
74
+ new_tiles_geojson = {}
75
+ for zoom in sorted(self._config.explorer_zoom_levels):
76
+ new_tiles = self._tile_visit_accessor.tile_state["tile_history"][zoom].loc[
77
+ self._tile_visit_accessor.tile_state["tile_history"][zoom][
78
+ "activity_id"
79
+ ]
80
+ == activity["id"]
81
+ ]
82
+ if len(new_tiles):
83
+ points = make_grid_points(
84
+ (
85
+ (row["tile_x"], row["tile_y"])
86
+ for index, row in new_tiles.iterrows()
87
+ ),
88
+ zoom,
89
+ )
90
+ new_tiles_geojson[zoom] = make_grid_file_geojson(points)
91
+
72
92
  result = {
73
93
  "activity": activity,
74
94
  "line_json": line_json,
@@ -81,6 +101,7 @@ class ActivityController:
81
101
  "date": activity["start"].date(),
82
102
  "time": activity["start"].time(),
83
103
  "new_tiles": new_tiles,
104
+ "new_tiles_geojson": new_tiles_geojson,
84
105
  }
85
106
  if (
86
107
  heart_zones := _extract_heart_rate_zones(
@@ -136,6 +136,39 @@
136
136
  <p>Not happy with the displayed data? <a href="{{ url_for('settings.sharepic') }}">Change share picture
137
137
  settings</a>.</p>
138
138
 
139
+ {% if new_tiles_geojson %}
140
+ <h2>New explorer tiles</h2>
141
+ <p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective zoom
142
+ levels.</p>
143
+ <script>
144
+ function add_map(id, geojson) {
145
+ let map = L.map(`map-${id}`, {
146
+ fullscreenControl: true
147
+ })
148
+ L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
149
+ maxZoom: 19,
150
+ attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
151
+ }).addTo(map)
152
+
153
+ let geojson_layer = L.geoJSON(geojson).addTo(map)
154
+ map.fitBounds(geojson_layer.getBounds())
155
+ return map
156
+ }
157
+ </script>
158
+
159
+ <div class="row mb-3">
160
+ {% for zoom, geojson in new_tiles_geojson.items() %}
161
+ <div class="col-md-6">
162
+ <h3>Zoom {{ zoom }}</h3>
163
+ <div id="map-{{ zoom }}" style="height: 300px; width: 100%;"></div>
164
+ <script>
165
+ let map{{ zoom }} = add_map("{{ zoom }}", {{ geojson | safe }})
166
+ </script>
167
+ </div>
168
+ {% endfor %}
169
+ </div>
170
+ {% endif %}
171
+
139
172
  {% if similar_activites|length > 0 %}
140
173
  <div class="row mb-3">
141
174
  <div class="col">
@@ -5,7 +5,6 @@ import secrets
5
5
 
6
6
  from flask import Flask
7
7
  from flask import render_template
8
- from flask import request
9
8
 
10
9
  from ..core.activities import ActivityRepository
11
10
  from ..explorer.tile_visits import TileVisitAccessor
@@ -16,31 +15,20 @@ from .entry_controller import EntryController
16
15
  from .equipment.blueprint import make_equipment_blueprint
17
16
  from .explorer.blueprint import make_explorer_blueprint
18
17
  from .heatmap.blueprint import make_heatmap_blueprint
19
- from .search_controller import SearchController
18
+ from .search.blueprint import make_search_blueprint
20
19
  from .square_planner.blueprint import make_square_planner_blueprint
21
20
  from .summary.blueprint import make_summary_blueprint
22
21
  from .tile.blueprint import make_tile_blueprint
23
22
  from .upload.blueprint import make_upload_blueprint
23
+ from geo_activity_playground.core.config import Config
24
24
  from geo_activity_playground.core.config import ConfigAccessor
25
25
  from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
26
26
  from geo_activity_playground.webui.authenticator import Authenticator
27
27
  from geo_activity_playground.webui.settings.blueprint import make_settings_blueprint
28
28
 
29
29
 
30
- def route_search(app: Flask, repository: ActivityRepository) -> None:
31
- search_controller = SearchController(repository)
32
-
33
- @app.route("/search", methods=["POST"])
34
- def search():
35
- form_input = request.form
36
- return render_template(
37
- "search.html.j2",
38
- **search_controller.render_search_results(form_input["name"])
39
- )
40
-
41
-
42
- def route_start(app: Flask, repository: ActivityRepository) -> None:
43
- entry_controller = EntryController(repository)
30
+ def route_start(app: Flask, repository: ActivityRepository, config: Config) -> None:
31
+ entry_controller = EntryController(repository, config)
44
32
 
45
33
  @app.route("/")
46
34
  def index():
@@ -66,7 +54,6 @@ def web_ui_main(
66
54
  host: str,
67
55
  port: int,
68
56
  ) -> None:
69
-
70
57
  repository.reload()
71
58
 
72
59
  app = Flask(__name__)
@@ -75,8 +62,7 @@ def web_ui_main(
75
62
 
76
63
  authenticator = Authenticator(config_accessor())
77
64
 
78
- route_search(app, repository)
79
- route_start(app, repository)
65
+ route_start(app, repository, config_accessor())
80
66
 
81
67
  app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
82
68
 
@@ -111,7 +97,11 @@ def web_ui_main(
111
97
  url_prefix="/square-planner",
112
98
  )
113
99
  app.register_blueprint(
114
- make_summary_blueprint(repository),
100
+ make_search_blueprint(repository),
101
+ url_prefix="/search",
102
+ )
103
+ app.register_blueprint(
104
+ make_summary_blueprint(repository, config_accessor()),
115
105
  url_prefix="/summary",
116
106
  )
117
107
  app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
@@ -14,9 +14,6 @@ class Authenticator:
14
14
  self._config = config
15
15
 
16
16
  def is_authenticated(self) -> bool:
17
- print(
18
- f"Password={self._config.upload_password}, Session={session.get('is_authenticated', False)}"
19
- )
20
17
  return not self._config.upload_password or session.get(
21
18
  "is_authenticated", False
22
19
  )
@@ -6,18 +6,22 @@ import pandas as pd
6
6
 
7
7
  from geo_activity_playground.core.activities import ActivityRepository
8
8
  from geo_activity_playground.core.activities import make_geojson_from_time_series
9
+ from geo_activity_playground.core.config import Config
10
+ from geo_activity_playground.webui.plot_util import make_kind_scale
9
11
 
10
12
 
11
13
  class EntryController:
12
- def __init__(self, repository: ActivityRepository) -> None:
14
+ def __init__(self, repository: ActivityRepository, config: Config) -> None:
13
15
  self._repository = repository
16
+ self._config = config
14
17
 
15
18
  def render(self) -> dict:
19
+ kind_scale = make_kind_scale(self._repository.meta, self._config)
16
20
  result = {"latest_activities": []}
17
21
 
18
22
  if len(self._repository):
19
23
  result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
20
- self._repository.meta
24
+ self._repository.meta, kind_scale
21
25
  )
22
26
 
23
27
  for activity in itertools.islice(
@@ -33,7 +37,7 @@ class EntryController:
33
37
  return result
34
38
 
35
39
 
36
- def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
40
+ def distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
37
41
  before_30_days = pd.to_datetime(
38
42
  datetime.datetime.now() - datetime.timedelta(days=31)
39
43
  )
@@ -48,7 +52,7 @@ def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
48
52
  .encode(
49
53
  alt.X("yearmonthdate(start)", title="Date"),
50
54
  alt.Y("sum(distance_km)", title="Distance / km"),
51
- alt.Color("kind", scale=alt.Scale(scheme="category10"), title="Kind"),
55
+ alt.Color("kind", scale=kind_scale, title="Kind"),
52
56
  [
53
57
  alt.Tooltip("yearmonthdate(start)", title="Date"),
54
58
  alt.Tooltip("kind", title="Kind"),
@@ -95,7 +95,7 @@ class ExplorerController:
95
95
  x2, y2 = compute_tile(south, east, zoom)
96
96
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
97
97
 
98
- tile_histories = self._tile_visit_accessor.histories
98
+ tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
99
99
  tiles = tile_histories[zoom]
100
100
  points = get_border_tiles(tiles, zoom, tile_bounds)
101
101
  if suffix == "geojson":
@@ -108,7 +108,7 @@ class ExplorerController:
108
108
  x2, y2 = compute_tile(south, east, zoom)
109
109
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
110
110
 
111
- tile_visits = self._tile_visit_accessor.visits
111
+ tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
112
112
  tiles = tile_visits[zoom]
113
113
  points = make_grid_points(
114
114
  (tile for tile in tiles.keys() if tile_bounds.contains(*tile)), zoom
@@ -161,6 +161,7 @@ def get_three_color_tiles(
161
161
  "last_visit": tile_data["last_time"].date().isoformat(),
162
162
  "num_visits": len(tile_data["activity_ids"]),
163
163
  "square": False,
164
+ "tile": f"({zoom}, {tile[0]}, {tile[1]})",
164
165
  }
165
166
 
166
167
  # Mark biggest square.
@@ -48,6 +48,8 @@
48
48
  function onEachFeature(feature, layer) {
49
49
  if (feature.properties && feature.properties.first_visit) {
50
50
  let lines = [
51
+ `<dt>Tile</dt>`,
52
+ `<dd>${feature.properties.tile}</dd>`,
51
53
  `<dt>First visit</dt>`,
52
54
  `<dd>${feature.properties.first_visit}</br><a href=/activity/${feature.properties.first_activity_id}>${feature.properties.first_activity_name}</a></dd>`,
53
55
  `<dt>Last visit</dt>`,
@@ -123,6 +123,7 @@ class HeatmapController:
123
123
  tile_counts += aim
124
124
  tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
125
125
  np.save(tmp_path, tile_counts)
126
+ tile_count_cache_path.unlink(missing_ok=True)
126
127
  tmp_path.rename(tile_count_cache_path)
127
128
  return tile_counts
128
129
 
@@ -0,0 +1,9 @@
1
+ import altair as alt
2
+ import pandas as pd
3
+
4
+ from geo_activity_playground.core.config import Config
5
+
6
+
7
+ def make_kind_scale(meta: pd.DataFrame, config: Config) -> alt.Scale:
8
+ kinds = sorted(meta["kind"].unique())
9
+ return alt.Scale(domain=kinds, scheme=config.color_scheme_for_kind)
@@ -0,0 +1,20 @@
1
+ from flask import Blueprint
2
+ from flask import render_template
3
+ from flask import request
4
+ from flask import Response
5
+
6
+ from ...core.activities import ActivityRepository
7
+
8
+
9
+ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
10
+ blueprint = Blueprint("search", __name__, template_folder="templates")
11
+
12
+ @blueprint.route("/", methods=["POST"])
13
+ def index():
14
+ activities = []
15
+ for _, row in repository.meta.iterrows():
16
+ if request.form["name"] in row["name"]:
17
+ activities.append(row)
18
+ return render_template("search/index.html.j2", activities=activities)
19
+
20
+ return blueprint