geo-activity-playground 0.27.1__tar.gz → 0.28.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 (111) hide show
  1. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/__main__.py +1 -2
  3. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/activities.py +3 -3
  4. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/config.py +1 -0
  5. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/tasks.py +2 -2
  6. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/explorer/tile_visits.py +135 -134
  7. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/controller.py +29 -13
  8. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +6 -11
  9. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/app.py +10 -2
  10. geo_activity_playground-0.28.0/geo_activity_playground/webui/auth/blueprint.py +27 -0
  11. geo_activity_playground-0.28.0/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +21 -0
  12. geo_activity_playground-0.28.0/geo_activity_playground/webui/authenticator.py +49 -0
  13. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/equipment/controller.py +2 -1
  14. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/explorer/controller.py +3 -3
  15. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +19 -6
  16. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/blueprint.py +34 -1
  17. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/controller.py +43 -0
  18. geo_activity_playground-0.28.0/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +19 -0
  19. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +18 -0
  20. geo_activity_playground-0.28.0/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +22 -0
  21. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/square_planner/controller.py +1 -1
  22. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/templates/page.html.j2 +1 -1
  23. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/upload/blueprint.py +7 -0
  24. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/upload/controller.py +4 -8
  25. geo_activity_playground-0.28.0/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +21 -0
  26. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/pyproject.toml +1 -1
  27. geo_activity_playground-0.27.1/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -37
  28. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/LICENSE +0 -0
  29. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/__init__.py +0 -0
  30. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/__init__.py +0 -0
  31. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/coordinates.py +0 -0
  32. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/enrichment.py +0 -0
  33. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/heart_rate.py +0 -0
  34. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/heatmap.py +0 -0
  35. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/paths.py +0 -0
  36. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  37. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/similarity.py +0 -0
  38. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/test_tiles.py +0 -0
  39. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
  40. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/tiles.py +0 -0
  41. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/core/time_conversion.py +0 -0
  42. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/explorer/__init__.py +0 -0
  43. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  44. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/explorer/video.py +0 -0
  45. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/__init__.py +0 -0
  46. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
  47. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  48. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/directory.py +0 -0
  49. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/strava_api.py +0 -0
  50. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  51. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  52. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/test_directory.py +0 -0
  53. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  54. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/__init__.py +0 -0
  55. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
  56. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  57. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -0
  58. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  59. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +0 -0
  60. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  61. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  62. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
  63. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  64. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  65. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  66. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  67. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
  68. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  69. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  70. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  71. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  72. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
  73. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  74. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  75. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  76. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  77. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
  78. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
  79. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/search_controller.py +0 -0
  80. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  81. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  82. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  83. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  84. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  85. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  86. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  87. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  88. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
  89. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  90. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  91. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  92. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  93. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  94. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  95. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  96. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  97. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  98. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  99. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  100. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  101. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
  102. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
  103. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/summary/controller.py +0 -0
  104. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +0 -0
  105. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/templates/home.html.j2 +0 -0
  106. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/templates/search.html.j2 +0 -0
  107. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
  108. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
  109. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/tile/controller.py +0 -0
  110. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
  111. {geo_activity_playground-0.27.1 → geo_activity_playground-0.28.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.27.1
3
+ Version: 0.28.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -109,10 +109,9 @@ def make_activity_repository(
109
109
 
110
110
 
111
111
  def main_cache(basedir: pathlib.Path) -> None:
112
- repository, tile_visit_accessor, config_accessor = make_activity_repository(
112
+ (repository, tile_visit_accessor, config_accessor) = make_activity_repository(
113
113
  basedir, False
114
114
  )
115
- scan_for_activities(repository, tile_visit_accessor, config_accessor())
116
115
 
117
116
 
118
117
  if __name__ == "__main__":
@@ -128,11 +128,11 @@ class ActivityRepository:
128
128
  else:
129
129
  return None
130
130
 
131
- def get_activity_ids(self, only_achievements: bool = False) -> set[int]:
131
+ def get_activity_ids(self, only_achievements: bool = False) -> list[int]:
132
132
  if only_achievements:
133
- return set(self.meta.loc[self.meta["consider_for_achievements"]].index)
133
+ return list(self.meta.loc[self.meta["consider_for_achievements"]].index)
134
134
  else:
135
- return set(self.meta.index)
135
+ return list(self.meta.index)
136
136
 
137
137
  def iter_activities(self, new_to_old=True, dropna=False) -> Iterator[ActivityMeta]:
138
138
  direction = -1 if new_to_old else 1
@@ -33,6 +33,7 @@ class Config:
33
33
  privacy_zones: dict[str, list[list[float]]] = dataclasses.field(
34
34
  default_factory=dict
35
35
  )
36
+ sharepic_suppressed_fields: list[str] = dataclasses.field(default_factory=list)
36
37
  strava_client_id: int = 131693
37
38
  strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
38
39
  strava_client_code: Optional[str] = None
@@ -59,8 +59,8 @@ class WorkTracker:
59
59
  else:
60
60
  self._done = set()
61
61
 
62
- def filter(self, ids: Iterable) -> set:
63
- return set(ids) - self._done
62
+ def filter(self, ids: Iterable) -> list:
63
+ return [elem for elem in ids if elem not in self._done]
64
64
 
65
65
  def mark_done(self, id: int) -> None:
66
66
  self._done.add(id)
@@ -25,139 +25,159 @@ from geo_activity_playground.core.tiles import interpolate_missing_tile
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
27
 
28
+ class TileInfo(TypedDict):
29
+ activity_ids: set[int]
30
+ first_time: datetime.datetime
31
+ first_id: int
32
+ last_time: datetime.datetime
33
+ last_id: int
34
+
35
+
28
36
  class TileHistoryRow(TypedDict):
37
+ activity_id: int
29
38
  time: datetime.datetime
30
39
  tile_x: int
31
40
  tile_y: int
32
41
 
33
42
 
43
+ class TileEvolutionState:
44
+ def __init__(self) -> None:
45
+ self.num_neighbors: dict[tuple[int, int], int] = {}
46
+ self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
47
+ self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
48
+ self.cluster_evolution = pd.DataFrame()
49
+ self.square_start = 0
50
+ self.cluster_start = 0
51
+ self.max_square_size = 0
52
+ self.visited_tiles: set[tuple[int, int]] = set()
53
+ self.square_evolution = pd.DataFrame()
54
+ self.square_x: Optional[int] = None
55
+ self.square_y: Optional[int] = None
56
+
57
+
58
+ class TileState(TypedDict):
59
+ tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
60
+ tile_history: dict[int, pd.DataFrame]
61
+ activities_per_tile: dict[int, set[int]]
62
+ processed_activities: set[int]
63
+ evolution_state: dict[int, TileEvolutionState]
64
+ version: int
65
+
66
+
67
+ TILE_STATE_VERSION = 2
68
+
69
+
34
70
  class TileVisitAccessor:
35
- TILE_EVOLUTION_STATES_PATH = pathlib.Path("Cache/tile-evolution-state.pickle")
36
- TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
37
- TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
38
- ACTIVITIES_PER_TILE_PATH = pathlib.Path(f"Cache/activities-per-tile.pickle")
71
+ PATH = pathlib.Path("Cache/tile-state-2.pickle")
39
72
 
40
73
  def __init__(self) -> None:
41
- self.visits: dict[int, dict[tuple[int, int], dict[str, Any]]] = try_load_pickle(
42
- self.TILE_VISITS_PATH
43
- ) or collections.defaultdict(dict)
44
- "zoom (tile_x, tile_y) tile_info"
45
-
46
- self.histories: dict[int, pd.DataFrame] = try_load_pickle(
47
- self.TILE_HISTORIES_PATH
48
- ) or collections.defaultdict(pd.DataFrame)
49
-
50
- self.states = try_load_pickle(
51
- self.TILE_EVOLUTION_STATES_PATH
52
- ) or collections.defaultdict(TileEvolutionState)
53
-
54
- self.activities_per_tile: dict[
55
- int, dict[tuple[int, int], set[int]]
56
- ] = try_load_pickle(self.ACTIVITIES_PER_TILE_PATH) or collections.defaultdict(
57
- dict
58
- )
74
+ self.tile_state: TileState = try_load_pickle(self.PATH)
75
+ if (
76
+ self.tile_state is None
77
+ or self.tile_state.get("version", None) != TILE_STATE_VERSION
78
+ ):
79
+ self.tile_state = make_tile_state()
80
+ # TODO: Reset work tracker
59
81
 
60
82
  def save(self) -> None:
61
- with open(self.TILE_VISITS_PATH, "wb") as f:
62
- pickle.dump(self.visits, f)
83
+ tmp_path = self.PATH.with_suffix(".tmp")
84
+ with open(tmp_path, "wb") as f:
85
+ pickle.dump(self.tile_state, f)
86
+ tmp_path.rename(self.PATH)
63
87
 
64
- with open(self.TILE_HISTORIES_PATH, "wb") as f:
65
- pickle.dump(self.histories, f)
66
88
 
67
- with open(self.TILE_EVOLUTION_STATES_PATH, "wb") as f:
68
- pickle.dump(self.states, f)
89
+ def make_defaultdict_dict():
90
+ return collections.defaultdict(dict)
69
91
 
70
- with open(self.ACTIVITIES_PER_TILE_PATH, "wb") as f:
71
- pickle.dump(self.activities_per_tile, f)
72
92
 
93
+ def make_defaultdict_set():
94
+ return collections.defaultdict(set)
95
+
96
+
97
+ def make_tile_state() -> TileState:
98
+ tile_state: TileState = {
99
+ "tile_visits": collections.defaultdict(make_defaultdict_dict),
100
+ "tile_history": collections.defaultdict(pd.DataFrame),
101
+ "activities_per_tile": collections.defaultdict(make_defaultdict_set),
102
+ "processed_activities": set(),
103
+ "evolution_state": collections.defaultdict(TileEvolutionState),
104
+ "version": TILE_STATE_VERSION,
105
+ }
106
+ return tile_state
73
107
 
74
- def compute_tile_visits(
75
- repository: ActivityRepository, tile_visits_accessor: TileVisitAccessor
76
- ) -> None:
77
- present_activity_ids = repository.get_activity_ids()
78
- work_tracker = WorkTracker(work_tracker_path("tile-visits"))
79
-
80
- changed_zoom_tile = collections.defaultdict(set)
81
-
82
- # Delete visits from removed activities.
83
- for zoom, activities_per_tile in tile_visits_accessor.activities_per_tile.items():
84
- for tile, activity_ids in activities_per_tile.items():
85
- deleted_ids = activity_ids - present_activity_ids
86
- if deleted_ids:
87
- logger.debug(
88
- f"Removing activities {deleted_ids} from tile {tile} at {zoom=}."
89
- )
90
- for activity_id in deleted_ids:
91
- activity_ids.remove(activity_id)
92
- work_tracker.discard(activity_id)
93
- changed_zoom_tile[zoom].add(tile)
94
108
 
95
- # Add visits from new activities.
96
- activity_ids_to_process = work_tracker.filter(repository.get_activity_ids())
109
+ def compute_tile_visits_new(
110
+ repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
111
+ ) -> None:
112
+ work_tracker = WorkTracker(work_tracker_path("tile-state"))
97
113
  for activity_id in tqdm(
98
- activity_ids_to_process, desc="Extract explorer tile visits"
114
+ work_tracker.filter(repository.get_activity_ids()), desc="Tile visits (new)"
99
115
  ):
100
- for zoom in range(20):
101
- for time, tile_x, tile_y in _tiles_from_points(
102
- repository.get_time_series(activity_id), zoom
103
- ):
104
- tile = (tile_x, tile_y)
105
- if tile not in tile_visits_accessor.activities_per_tile[zoom]:
106
- tile_visits_accessor.activities_per_tile[zoom][tile] = set()
107
- tile_visits_accessor.activities_per_tile[zoom][tile].add(activity_id)
108
- changed_zoom_tile[zoom].add(tile)
116
+ do_tile_stuff(repository, tile_visit_accessor.tile_state, activity_id)
109
117
  work_tracker.mark_done(activity_id)
110
-
111
- # Update tile visits structure.
112
- for zoom, changed_tiles in tqdm(
113
- changed_zoom_tile.items(), desc="Incorporate changes in tiles"
114
- ):
115
- soa = {"activity_id": [], "time": [], "tile_x": [], "tile_y": []}
116
-
117
- for tile in changed_tiles:
118
- activity_ids = tile_visits_accessor.activities_per_tile[zoom][tile]
119
- activities = [
120
- repository.get_activity_by_id(activity_id)
121
- for activity_id in activity_ids
122
- ]
123
- activities_to_consider = [
124
- activity
125
- for activity in activities
126
- if activity["consider_for_achievements"]
127
- ]
128
- activities_to_consider.sort(key=lambda activity: activity["start"])
129
-
130
- if activities_to_consider:
131
- tile_visits_accessor.visits[zoom][tile] = {
132
- "first_time": activities_to_consider[0]["start"],
133
- "first_id": activities_to_consider[0]["id"],
134
- "last_time": activities_to_consider[-1]["start"],
135
- "last_id": activities_to_consider[-1]["id"],
136
- "activity_ids": {
137
- activity["id"] for activity in activities_to_consider
138
- },
139
- }
140
-
141
- soa["activity_id"].append(activities_to_consider[0]["id"])
142
- soa["time"].append(activities_to_consider[0]["start"])
143
- soa["tile_x"].append(tile[0])
144
- soa["tile_y"].append(tile[1])
145
- else:
146
- if tile in tile_visits_accessor.visits[zoom]:
147
- del tile_visits_accessor.visits[zoom][tile]
148
-
149
- df = pd.DataFrame(soa)
150
- if len(df) > 0:
151
- df = pd.concat([tile_visits_accessor.histories[zoom], df])
152
- df.sort_values("time", inplace=True)
153
- tile_visits_accessor.histories[zoom] = df.groupby(
154
- ["tile_x", "tile_y"]
155
- ).head(1)
156
-
157
- tile_visits_accessor.save()
118
+ tile_visit_accessor.save()
158
119
  work_tracker.close()
159
120
 
160
121
 
122
+ def do_tile_stuff(
123
+ repository: ActivityRepository, tile_state: TileState, activity_id: int
124
+ ) -> None:
125
+ activity = repository.get_activity_by_id(activity_id)
126
+ time_series = repository.get_time_series(activity_id)
127
+
128
+ activity_tiles = pd.DataFrame(
129
+ _tiles_from_points(time_series, 19), columns=["time", "tile_x", "tile_y"]
130
+ )
131
+ for zoom in reversed(range(20)):
132
+ activities_per_tile = tile_state["activities_per_tile"][zoom]
133
+
134
+ new_tile_history_soa = {
135
+ "activity_id": [],
136
+ "time": [],
137
+ "tile_x": [],
138
+ "tile_y": [],
139
+ }
140
+
141
+ activity_tiles = activity_tiles.groupby(["tile_x", "tile_y"]).head(1)
142
+
143
+ for time, tile in zip(
144
+ activity_tiles["time"],
145
+ zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
146
+ ):
147
+ if activity["consider_for_achievements"]:
148
+ if tile not in activities_per_tile:
149
+ new_tile_history_soa["activity_id"].append(activity_id)
150
+ new_tile_history_soa["time"].append(time)
151
+ new_tile_history_soa["tile_x"].append(tile[0])
152
+ new_tile_history_soa["tile_y"].append(tile[1])
153
+
154
+ tile_visit = tile_state["tile_visits"][zoom][tile]
155
+ if not tile_visit:
156
+ tile_visit["activity_ids"] = {activity_id}
157
+ else:
158
+ tile_visit["activity_ids"].add(activity_id)
159
+
160
+ first_time = tile_visit.get("first_time", None)
161
+ last_time = tile_visit.get("last_time", None)
162
+ if first_time is None or time < first_time:
163
+ tile_visit["first_id"] = activity_id
164
+ tile_visit["first_time"] = time
165
+ if last_time is None or time > last_time:
166
+ tile_visit["last_id"] = activity_id
167
+ tile_visit["last_time"] = time
168
+
169
+ activities_per_tile[tile].add(activity_id)
170
+
171
+ if new_tile_history_soa["activity_id"]:
172
+ tile_state["tile_history"][zoom] = pd.concat(
173
+ [tile_state["tile_history"][zoom], pd.DataFrame(new_tile_history_soa)]
174
+ )
175
+
176
+ # Move up one layer in the quad-tree.
177
+ activity_tiles["tile_x"] //= 2
178
+ activity_tiles["tile_y"] //= 2
179
+
180
+
161
181
  def _tiles_from_points(
162
182
  time_series: pd.DataFrame, zoom: int
163
183
  ) -> Iterator[tuple[datetime.datetime, int, int]]:
@@ -181,38 +201,19 @@ def _tiles_from_points(
181
201
  yield (t1,) + interpolated
182
202
 
183
203
 
184
- class TileEvolutionState:
185
- def __init__(self) -> None:
186
- self.num_neighbors: dict[tuple[int, int], int] = {}
187
- self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
188
- self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
189
- self.cluster_evolution = pd.DataFrame()
190
- self.square_start = 0
191
- self.cluster_start = 0
192
- self.max_square_size = 0
193
- self.visited_tiles: set[tuple[int, int]] = set()
194
- self.square_evolution = pd.DataFrame()
195
- self.square_x: Optional[int] = None
196
- self.square_y: Optional[int] = None
197
-
198
-
199
- def compute_tile_evolution(
200
- tile_visits_accessor: TileVisitAccessor, config: Config
201
- ) -> None:
204
+ def compute_tile_evolution(tile_state: TileState, config: Config) -> None:
202
205
  for zoom in config.explorer_zoom_levels:
203
206
  _compute_cluster_evolution(
204
- tile_visits_accessor.histories[zoom],
205
- tile_visits_accessor.states[zoom],
207
+ tile_state["tile_history"][zoom],
208
+ tile_state["evolution_state"][zoom],
206
209
  zoom,
207
210
  )
208
211
  _compute_square_history(
209
- tile_visits_accessor.histories[zoom],
210
- tile_visits_accessor.states[zoom],
212
+ tile_state["tile_history"][zoom],
213
+ tile_state["evolution_state"][zoom],
211
214
  zoom,
212
215
  )
213
216
 
214
- tile_visits_accessor.save()
215
-
216
217
 
217
218
  def _compute_cluster_evolution(
218
219
  tiles: pd.DataFrame, s: TileEvolutionState, zoom: int
@@ -61,7 +61,9 @@ class ActivityController:
61
61
 
62
62
  new_tiles = {
63
63
  zoom: sum(
64
- self._tile_visit_accessor.histories[zoom]["activity_id"]
64
+ self._tile_visit_accessor.tile_state["tile_history"][zoom][
65
+ "activity_id"
66
+ ]
65
67
  == activity["id"]
66
68
  )
67
69
  for zoom in [14, 17]
@@ -100,7 +102,9 @@ class ActivityController:
100
102
  time_series = privacy_zone.filter_time_series(time_series)
101
103
  if len(time_series) == 0:
102
104
  time_series = self._repository.get_time_series(id)
103
- return make_sharepic(activity, time_series)
105
+ return make_sharepic(
106
+ activity, time_series, self._config.sharepic_suppressed_fields
107
+ )
104
108
 
105
109
  def render_day(self, year: int, month: int, day: int) -> dict:
106
110
  meta = self._repository.meta
@@ -411,7 +415,11 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
411
415
  return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
412
416
 
413
417
 
414
- def make_sharepic(activity: ActivityMeta, time_series: pd.DataFrame) -> bytes:
418
+ def make_sharepic(
419
+ activity: ActivityMeta,
420
+ time_series: pd.DataFrame,
421
+ sharepic_suppressed_fields: list[str],
422
+ ) -> bytes:
415
423
  lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
416
424
 
417
425
  geo_bounds = get_bounds(lat_lon_data)
@@ -448,19 +456,27 @@ def make_sharepic(activity: ActivityMeta, time_series: pd.DataFrame) -> bytes:
448
456
  draw.line(yx, fill="red", width=4)
449
457
 
450
458
  draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
451
- facts = [
452
- f"{activity['kind']}",
453
- f"{activity['start'].date()}",
454
- f"{activity['equipment']}",
455
- f"\n{activity['distance_km']:.1f} km",
456
- re.sub(r"^0 days ", "", f"{activity['elapsed_time']}"),
457
- ]
459
+
460
+ facts = {
461
+ "kind": f"{activity['kind']}",
462
+ "start": f"{activity['start'].date()}",
463
+ "equipment": f"{activity['equipment']}",
464
+ "distance_km": f"\n{activity['distance_km']:.1f} km",
465
+ "elapsed_time": re.sub(r"^0 days ", "", f"{activity['elapsed_time']}"),
466
+ }
467
+
458
468
  if activity.get("calories", 0) and not pd.isna(activity["calories"]):
459
- facts.append(f"{activity['calories']:.0f} kcal")
469
+ facts["calories"] = f"{activity['calories']:.0f} kcal"
460
470
  if activity.get("steps", 0) and not pd.isna(activity["steps"]):
461
- facts.append(f"{activity['steps']:.0f} steps")
471
+ facts["steps"] = f"{activity['steps']:.0f} steps"
472
+
473
+ facts = {
474
+ key: value
475
+ for key, value in facts.items()
476
+ if not key in sharepic_suppressed_fields
477
+ }
462
478
 
463
- draw.text((35, img.height - 70 + 10), " ".join(facts), font_size=20)
479
+ draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
464
480
 
465
481
  # img_array = np.array(img) / 255
466
482
 
@@ -111,11 +111,7 @@
111
111
  </div>
112
112
 
113
113
  {% if heartrate_time_plot is defined %}
114
- <div class="row mb-3">
115
- <div class="col">
116
- <h2>Heart rate</h2>
117
- </div>
118
- </div>
114
+ <h2>Heart rate</h2>
119
115
 
120
116
  <div class="row mb-3">
121
117
  <div class="col-md-4">
@@ -133,13 +129,12 @@
133
129
  </div>
134
130
  {% endif %}
135
131
 
136
- <div class="row mb-3">
137
- <div class="col">
138
- <h2>Share picture</h2>
132
+ <h2>Share picture</h2>
139
133
 
140
- <img src="{{ url_for('.sharepic', id=activity.id) }}" />
141
- </div>
142
- </div>
134
+ <p><img src="{{ url_for('.sharepic', id=activity.id) }}" /></p>
135
+
136
+ <p>Not happy with the displayed data? <a href="{{ url_for('settings.sharepic') }}">Change share picture
137
+ settings</a>.</p>
143
138
 
144
139
  {% if similar_activites|length > 0 %}
145
140
  <div class="row mb-3">
@@ -22,6 +22,8 @@ from .summary.blueprint import make_summary_blueprint
22
22
  from .tile.blueprint import make_tile_blueprint
23
23
  from .upload.blueprint import make_upload_blueprint
24
24
  from geo_activity_playground.core.config import ConfigAccessor
25
+ from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
26
+ from geo_activity_playground.webui.authenticator import Authenticator
25
27
  from geo_activity_playground.webui.settings.blueprint import make_settings_blueprint
26
28
 
27
29
 
@@ -71,9 +73,13 @@ def web_ui_main(
71
73
  app.config["UPLOAD_FOLDER"] = "Activities"
72
74
  app.secret_key = get_secret_key()
73
75
 
76
+ authenticator = Authenticator(config_accessor())
77
+
74
78
  route_search(app, repository)
75
79
  route_start(app, repository)
76
80
 
81
+ app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
82
+
77
83
  app.register_blueprint(
78
84
  make_activity_blueprint(
79
85
  repository,
@@ -97,7 +103,7 @@ def web_ui_main(
97
103
  make_heatmap_blueprint(repository, tile_visit_accessor), url_prefix="/heatmap"
98
104
  )
99
105
  app.register_blueprint(
100
- make_settings_blueprint(config_accessor),
106
+ make_settings_blueprint(config_accessor, authenticator),
101
107
  url_prefix="/settings",
102
108
  )
103
109
  app.register_blueprint(
@@ -110,7 +116,9 @@ def web_ui_main(
110
116
  )
111
117
  app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
112
118
  app.register_blueprint(
113
- make_upload_blueprint(repository, tile_visit_accessor, config_accessor()),
119
+ make_upload_blueprint(
120
+ repository, tile_visit_accessor, config_accessor(), authenticator
121
+ ),
114
122
  url_prefix="/upload",
115
123
  )
116
124
 
@@ -0,0 +1,27 @@
1
+ from flask import Blueprint
2
+ from flask import redirect
3
+ from flask import render_template
4
+ from flask import request
5
+ from flask import url_for
6
+
7
+ from geo_activity_playground.webui.authenticator import Authenticator
8
+
9
+
10
+ def make_auth_blueprint(authenticator: Authenticator) -> Blueprint:
11
+ blueprint = Blueprint("auth", __name__, template_folder="templates")
12
+
13
+ @blueprint.route("/", methods=["GET", "POST"])
14
+ def index():
15
+ if request.method == "POST":
16
+ authenticator.authenticate(request.form["password"])
17
+ return render_template(
18
+ "auth/index.html.j2",
19
+ is_authenticated=authenticator.is_authenticated(),
20
+ )
21
+
22
+ @blueprint.route("/logout")
23
+ def logout():
24
+ authenticator.logout()
25
+ return redirect(url_for(".index"))
26
+
27
+ return blueprint
@@ -0,0 +1,21 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <h1>Authentication</h1>
5
+
6
+ {% if is_authenticated %}
7
+ <p>You are either logged in or don't have a password set. You can do everything.</p>
8
+
9
+ <a class="btn btn-primary" href="{{ url_for('.logout') }}">Log Out</a>
10
+ {% else %}
11
+ <form method="POST">
12
+ <div class="mb-3">
13
+ <label for="password" class="form-label">Password</label>
14
+ <input type="password" class="form-control" id="password" name="password" />
15
+ </div>
16
+
17
+ <button type="submit" class="btn btn-primary">Log In</button>
18
+ </form>
19
+ {% endif %}
20
+
21
+ {% endblock %}
@@ -0,0 +1,49 @@
1
+ import functools
2
+ from typing import Callable
3
+
4
+ from flask import flash
5
+ from flask import redirect
6
+ from flask import session
7
+ from flask import url_for
8
+
9
+ from geo_activity_playground.core.config import Config
10
+
11
+
12
+ class Authenticator:
13
+ def __init__(self, config: Config) -> None:
14
+ self._config = config
15
+
16
+ def is_authenticated(self) -> bool:
17
+ print(
18
+ f"Password={self._config.upload_password}, Session={session.get('is_authenticated', False)}"
19
+ )
20
+ return not self._config.upload_password or session.get(
21
+ "is_authenticated", False
22
+ )
23
+
24
+ def authenticate(self, password: str) -> None:
25
+ if password == self._config.upload_password:
26
+ session["is_authenticated"] = True
27
+ session.permanent = True
28
+ flash("Login successful.", category="success")
29
+ else:
30
+ flash("Incorrect password.", category="warning")
31
+
32
+ def logout(self) -> None:
33
+ session["is_authenticated"] = False
34
+ flash("Logout successful.", category="success")
35
+
36
+
37
+ def needs_authentication(authenticator: Authenticator) -> Callable:
38
+ def decorator(route: Callable) -> Callable:
39
+ @functools.wraps(route)
40
+ def wrapped_route(*args, **kwargs):
41
+ if authenticator.is_authenticated():
42
+ return route(*args, **kwargs)
43
+ else:
44
+ flash("You need to be logged in to view that site.", category="Warning")
45
+ return redirect(url_for("auth.index"))
46
+
47
+ return wrapped_route
48
+
49
+ return decorator
@@ -103,7 +103,8 @@ class EquipmentController:
103
103
  }
104
104
 
105
105
  for equipment, offset in self._config.equipment_offsets.items():
106
- equipment_summary.loc[equipment, "total_distance_km"] += offset
106
+ if equipment in equipment_summary.index:
107
+ equipment_summary.loc[equipment, "total_distance_km"] += offset
107
108
 
108
109
  return {
109
110
  "equipment_variables": equipment_variables,
@@ -54,9 +54,9 @@ class ExplorerController:
54
54
  if zoom not in self._config_accessor().explorer_zoom_levels:
55
55
  return {"zoom_level_not_generated": zoom}
56
56
 
57
- tile_evolution_states = self._tile_visit_accessor.states
58
- tile_visits = self._tile_visit_accessor.visits
59
- tile_histories = self._tile_visit_accessor.histories
57
+ tile_evolution_states = self._tile_visit_accessor.tile_state["evolution_state"]
58
+ tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
59
+ tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
60
60
 
61
61
  medians = tile_histories[zoom].median()
62
62
  median_lat, median_lon = get_tile_upper_left_lat_lon(