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.
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/PKG-INFO +3 -4
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/activities.py +3 -6
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/config.py +3 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/paths.py +10 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/tasks.py +5 -4
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/time_conversion.py +1 -1
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/tile_visits.py +43 -9
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/activity_parsers.py +28 -17
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/csv_parser.py +1 -2
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/directory.py +2 -1
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/controller.py +22 -1
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +33 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/app.py +10 -20
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/authenticator.py +0 -3
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/entry_controller.py +8 -4
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/controller.py +3 -2
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/heatmap_controller.py +1 -0
- geo_activity_playground-0.29.1/geo_activity_playground/webui/plot_util.py +9 -0
- geo_activity_playground-0.29.1/geo_activity_playground/webui/search/blueprint.py +20 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/blueprint.py +69 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/controller.py +4 -3
- geo_activity_playground-0.29.1/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +9 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/controller.py +2 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/blueprint.py +3 -2
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/controller.py +20 -13
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/templates/home.html.j2 +1 -1
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/templates/page.html.j2 +56 -28
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/pyproject.toml +3 -3
- geo_activity_playground-0.28.0/geo_activity_playground/webui/search_controller.py +0 -19
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/LICENSE +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/__main__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/enrichment.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/heart_rate.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/heatmap.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/privacy_zones.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/test_time_conversion.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/core/tiles.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/strava_api.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/strava_checkout.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_directory.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/auth/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/controller.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/controller.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/controller.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
- /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
- {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
- {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
- {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
- {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
- {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
- {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
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
- {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
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/tile/controller.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/__init__.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/blueprint.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/controller.py +0 -0
- {geo_activity_playground-0.28.0 → geo_activity_playground-0.29.1}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
- {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.
|
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.
|
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 (>=
|
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 =
|
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,
|
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
|
-
|
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)
|
@@ -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
|
-
|
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
|
148
|
+
work_tracker.filter(repository.get_activity_ids()), desc="Tile visits"
|
115
149
|
):
|
116
|
-
|
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
|
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
|
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
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
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: '© <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 .
|
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
|
31
|
-
|
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
|
-
|
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
|
-
|
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=
|
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.
|
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.
|
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
|