geo-activity-playground 0.24.2__tar.gz → 0.25.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.
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/PKG-INFO +1 -1
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/__main__.py +0 -2
- geo_activity_playground-0.25.0/geo_activity_playground/core/activities.py +254 -0
- geo_activity_playground-0.25.0/geo_activity_playground/core/enrichment.py +164 -0
- geo_activity_playground-0.25.0/geo_activity_playground/core/paths.py +56 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/tasks.py +26 -3
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/explorer/tile_visits.py +78 -42
- {geo_activity_playground-0.24.2/geo_activity_playground/core → geo_activity_playground-0.25.0/geo_activity_playground/importers}/activity_parsers.py +7 -14
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/directory.py +36 -27
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/strava_api.py +45 -38
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/strava_checkout.py +24 -16
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/controller.py +2 -2
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +2 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/app.py +11 -31
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/entry_controller.py +5 -5
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +6 -0
- geo_activity_playground-0.25.0/geo_activity_playground/webui/strava/blueprint.py +33 -0
- geo_activity_playground-0.25.0/geo_activity_playground/webui/strava/controller.py +47 -0
- geo_activity_playground-0.24.2/geo_activity_playground/webui/templates/strava-connect.html.j2 → geo_activity_playground-0.25.0/geo_activity_playground/webui/strava/templates/strava/client-id.html.j2 +3 -7
- geo_activity_playground-0.25.0/geo_activity_playground/webui/strava/templates/strava/connected.html.j2 +14 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/templates/home.html.j2 +5 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -0
- geo_activity_playground-0.25.0/geo_activity_playground/webui/templates/settings.html.j2 +15 -0
- geo_activity_playground-0.25.0/geo_activity_playground/webui/upload/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/upload/controller.py +12 -16
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/pyproject.toml +1 -1
- geo_activity_playground-0.24.2/geo_activity_playground/core/activities.py +0 -332
- geo_activity_playground-0.24.2/geo_activity_playground/core/cache_migrations.py +0 -133
- geo_activity_playground-0.24.2/geo_activity_playground/core/paths.py +0 -37
- geo_activity_playground-0.24.2/geo_activity_playground/webui/strava_controller.py +0 -27
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/LICENSE +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/config.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/heatmap.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/privacy_zones.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/tiles.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/core/time_conversion.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/test_directory.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/search_controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
- {geo_activity_playground-0.24.2/geo_activity_playground/webui/summary → geo_activity_playground-0.25.0/geo_activity_playground/webui/strava}/__init__.py +0 -0
- {geo_activity_playground-0.24.2/geo_activity_playground/webui/tile → geo_activity_playground-0.25.0/geo_activity_playground/webui/summary}/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/summary/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/templates/search.html.j2 +0 -0
- {geo_activity_playground-0.24.2/geo_activity_playground/webui/upload → geo_activity_playground-0.25.0/geo_activity_playground/webui/tile}/__init__.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/tile/controller.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
- {geo_activity_playground-0.24.2 → geo_activity_playground-0.25.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
@@ -8,7 +8,6 @@ import coloredlogs
|
|
8
8
|
|
9
9
|
from .importers.strava_checkout import convert_strava_checkout
|
10
10
|
from geo_activity_playground.core.activities import ActivityRepository
|
11
|
-
from geo_activity_playground.core.cache_migrations import apply_cache_migrations
|
12
11
|
from geo_activity_playground.core.config import get_config
|
13
12
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
14
13
|
from geo_activity_playground.explorer.video import explorer_video_main
|
@@ -97,7 +96,6 @@ def make_activity_repository(
|
|
97
96
|
basedir: pathlib.Path, skip_strava: bool
|
98
97
|
) -> tuple[ActivityRepository, TileVisitAccessor, dict]:
|
99
98
|
os.chdir(basedir)
|
100
|
-
apply_cache_migrations()
|
101
99
|
config = get_config()
|
102
100
|
|
103
101
|
if not config.get("prefer_metadata_from_file", True):
|
@@ -0,0 +1,254 @@
|
|
1
|
+
import datetime
|
2
|
+
import functools
|
3
|
+
import logging
|
4
|
+
import pickle
|
5
|
+
from typing import Iterator
|
6
|
+
from typing import Optional
|
7
|
+
from typing import TypedDict
|
8
|
+
|
9
|
+
import geojson
|
10
|
+
import matplotlib
|
11
|
+
import numpy as np
|
12
|
+
import pandas as pd
|
13
|
+
from tqdm import tqdm
|
14
|
+
|
15
|
+
from geo_activity_playground.core.config import get_config
|
16
|
+
from geo_activity_playground.core.paths import activities_file
|
17
|
+
from geo_activity_playground.core.paths import activity_enriched_meta_dir
|
18
|
+
from geo_activity_playground.core.paths import activity_enriched_time_series_dir
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class ActivityMeta(TypedDict):
|
24
|
+
calories: float
|
25
|
+
commute: bool
|
26
|
+
consider_for_achievements: bool
|
27
|
+
distance_km: float
|
28
|
+
elapsed_time: datetime.timedelta
|
29
|
+
end_latitude: float
|
30
|
+
end_longitude: float
|
31
|
+
equipment: str
|
32
|
+
id: int
|
33
|
+
kind: str
|
34
|
+
moving_time: datetime.timedelta
|
35
|
+
name: str
|
36
|
+
path: str
|
37
|
+
start_latitude: float
|
38
|
+
start_longitude: float
|
39
|
+
start: datetime.datetime
|
40
|
+
steps: int
|
41
|
+
|
42
|
+
|
43
|
+
def make_activity_meta() -> ActivityMeta:
|
44
|
+
return ActivityMeta(
|
45
|
+
calories=None,
|
46
|
+
commute=False,
|
47
|
+
consider_for_achievements=True,
|
48
|
+
equipment="Unknown",
|
49
|
+
kind="Unknown",
|
50
|
+
steps=None,
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
def build_activity_meta() -> None:
|
55
|
+
if activities_file().exists():
|
56
|
+
meta = pd.read_parquet(activities_file())
|
57
|
+
present_ids = set(meta["id"])
|
58
|
+
else:
|
59
|
+
meta = pd.DataFrame(columns=["id"])
|
60
|
+
present_ids = set()
|
61
|
+
|
62
|
+
available_ids = {
|
63
|
+
int(path.stem) for path in activity_enriched_meta_dir().glob("*.pickle")
|
64
|
+
}
|
65
|
+
new_ids = available_ids - present_ids
|
66
|
+
deleted_ids = present_ids - available_ids
|
67
|
+
|
68
|
+
# Remove updated activities and read these again.
|
69
|
+
if activities_file().exists():
|
70
|
+
meta_mtime = activities_file().stat().st_mtime
|
71
|
+
updated_ids = {
|
72
|
+
int(path.stem)
|
73
|
+
for path in activity_enriched_meta_dir().glob("*.pickle")
|
74
|
+
if path.stat().st_mtime > meta_mtime
|
75
|
+
}
|
76
|
+
new_ids.update(updated_ids)
|
77
|
+
deleted_ids.update(updated_ids & present_ids)
|
78
|
+
|
79
|
+
if deleted_ids:
|
80
|
+
logger.debug(f"Removing activities {deleted_ids} from repository.")
|
81
|
+
meta.drop(sorted(deleted_ids), axis="index", inplace=True)
|
82
|
+
|
83
|
+
rows = []
|
84
|
+
for new_id in tqdm(new_ids, desc="Register new activities"):
|
85
|
+
with open(activity_enriched_meta_dir() / f"{new_id}.pickle", "rb") as f:
|
86
|
+
rows.append(pickle.load(f))
|
87
|
+
|
88
|
+
if rows:
|
89
|
+
new_shard = pd.DataFrame(rows)
|
90
|
+
new_shard.index = new_shard["id"]
|
91
|
+
new_shard.index.name = "index"
|
92
|
+
meta = pd.concat([meta, new_shard])
|
93
|
+
|
94
|
+
if len(meta):
|
95
|
+
assert pd.api.types.is_dtype_equal(meta["start"].dtype, "datetime64[ns]"), (
|
96
|
+
meta["start"].dtype,
|
97
|
+
meta["start"].iloc[0],
|
98
|
+
)
|
99
|
+
|
100
|
+
meta.sort_values("start", inplace=True)
|
101
|
+
|
102
|
+
meta.to_parquet(activities_file())
|
103
|
+
|
104
|
+
|
105
|
+
class ActivityRepository:
|
106
|
+
def __init__(self) -> None:
|
107
|
+
self.meta = None
|
108
|
+
|
109
|
+
def __len__(self) -> int:
|
110
|
+
return len(self.meta)
|
111
|
+
|
112
|
+
def reload(self) -> None:
|
113
|
+
self.meta = pd.read_parquet(activities_file())
|
114
|
+
|
115
|
+
def has_activity(self, activity_id: int) -> bool:
|
116
|
+
if len(self.meta):
|
117
|
+
if activity_id in self.meta["id"]:
|
118
|
+
return True
|
119
|
+
|
120
|
+
for activity_meta in self._loose_activities:
|
121
|
+
if activity_meta["id"] == activity_id:
|
122
|
+
return True
|
123
|
+
|
124
|
+
return False
|
125
|
+
|
126
|
+
def last_activity_date(self) -> Optional[datetime.datetime]:
|
127
|
+
if len(self.meta):
|
128
|
+
return self.meta.iloc[-1]["start"]
|
129
|
+
else:
|
130
|
+
return None
|
131
|
+
|
132
|
+
def get_activity_ids(self, only_achievements: bool = False) -> set[int]:
|
133
|
+
if only_achievements:
|
134
|
+
return set(self.meta.loc[self.meta["consider_for_achievements"]].index)
|
135
|
+
else:
|
136
|
+
return set(self.meta.index)
|
137
|
+
|
138
|
+
def iter_activities(self, new_to_old=True, dropna=False) -> Iterator[ActivityMeta]:
|
139
|
+
direction = -1 if new_to_old else 1
|
140
|
+
for index, row in self.meta[::direction].iterrows():
|
141
|
+
if not dropna or not pd.isna(row["start"]):
|
142
|
+
yield row
|
143
|
+
|
144
|
+
@functools.lru_cache()
|
145
|
+
def get_activity_by_id(self, id: int) -> ActivityMeta:
|
146
|
+
activity = self.meta.loc[id]
|
147
|
+
assert isinstance(activity["name"], str), activity["name"]
|
148
|
+
return activity
|
149
|
+
|
150
|
+
@functools.lru_cache(maxsize=3000)
|
151
|
+
def get_time_series(self, id: int) -> pd.DataFrame:
|
152
|
+
path = activity_enriched_time_series_dir() / f"{id}.parquet"
|
153
|
+
try:
|
154
|
+
df = pd.read_parquet(path)
|
155
|
+
except OSError as e:
|
156
|
+
logger.error(f"Error while reading {path}, deleting cache file …")
|
157
|
+
path.unlink(missing_ok=True)
|
158
|
+
raise
|
159
|
+
|
160
|
+
return df
|
161
|
+
|
162
|
+
|
163
|
+
def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
|
164
|
+
fc = geojson.FeatureCollection(
|
165
|
+
features=[
|
166
|
+
geojson.LineString(
|
167
|
+
[(lon, lat) for lat, lon in zip(group["latitude"], group["longitude"])]
|
168
|
+
)
|
169
|
+
for _, group in time_series.groupby("segment_id")
|
170
|
+
]
|
171
|
+
)
|
172
|
+
return geojson.dumps(fc)
|
173
|
+
|
174
|
+
|
175
|
+
def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
176
|
+
speed_without_na = time_series["speed"].dropna()
|
177
|
+
low = min(speed_without_na)
|
178
|
+
high = max(speed_without_na)
|
179
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
180
|
+
|
181
|
+
cmap = matplotlib.colormaps["viridis"]
|
182
|
+
features = [
|
183
|
+
geojson.Feature(
|
184
|
+
geometry=geojson.LineString(
|
185
|
+
coordinates=[
|
186
|
+
[row["longitude"], row["latitude"]],
|
187
|
+
[next["longitude"], next["latitude"]],
|
188
|
+
]
|
189
|
+
),
|
190
|
+
properties={
|
191
|
+
"speed": next["speed"] if np.isfinite(next["speed"]) else 0.0,
|
192
|
+
"color": matplotlib.colors.to_hex(cmap(clamp_speed(next["speed"]))),
|
193
|
+
},
|
194
|
+
)
|
195
|
+
for _, group in time_series.groupby("segment_id")
|
196
|
+
for (_, row), (_, next) in zip(group.iterrows(), group.iloc[1:].iterrows())
|
197
|
+
]
|
198
|
+
feature_collection = geojson.FeatureCollection(features)
|
199
|
+
return geojson.dumps(feature_collection)
|
200
|
+
|
201
|
+
|
202
|
+
def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
|
203
|
+
speed_without_na = time_series["speed"].dropna()
|
204
|
+
low = min(speed_without_na)
|
205
|
+
high = max(speed_without_na)
|
206
|
+
cmap = matplotlib.colormaps["viridis"]
|
207
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
208
|
+
colors = [
|
209
|
+
(f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
|
210
|
+
for speed in np.linspace(low, high, 10)
|
211
|
+
]
|
212
|
+
return {"low": low, "high": high, "colors": colors}
|
213
|
+
|
214
|
+
|
215
|
+
def extract_heart_rate_zones(time_series: pd.DataFrame) -> Optional[pd.DataFrame]:
|
216
|
+
if "heartrate" not in time_series:
|
217
|
+
return None
|
218
|
+
config = get_config()
|
219
|
+
try:
|
220
|
+
heart_config = config["heart"]
|
221
|
+
except KeyError:
|
222
|
+
logger.warning(
|
223
|
+
"Missing config entry `heart`, cannot determine heart rate zones."
|
224
|
+
)
|
225
|
+
return None
|
226
|
+
|
227
|
+
birthyear = heart_config.get("birthyear", None)
|
228
|
+
maximum = heart_config.get("maximum", None)
|
229
|
+
resting = heart_config.get("resting", None)
|
230
|
+
|
231
|
+
if not maximum and birthyear:
|
232
|
+
age = time_series["time"].iloc[0].year - birthyear
|
233
|
+
maximum = 220 - age
|
234
|
+
if not resting:
|
235
|
+
resting = 0
|
236
|
+
if not maximum:
|
237
|
+
logger.warning(
|
238
|
+
"Missing config entry `heart.maximum` or `heart.birthyear`, cannot determine heart rate zones."
|
239
|
+
)
|
240
|
+
return None
|
241
|
+
|
242
|
+
zones: pd.Series = (time_series["heartrate"] - resting) * 10 // (
|
243
|
+
maximum - resting
|
244
|
+
) - 4
|
245
|
+
zones.loc[zones < 0] = 0
|
246
|
+
zones.loc[zones > 5] = 5
|
247
|
+
df = pd.DataFrame({"heartzone": zones, "step": time_series["time"].diff()}).dropna()
|
248
|
+
duration_per_zone = df.groupby("heartzone").sum()["step"].dt.total_seconds() / 60
|
249
|
+
duration_per_zone.name = "minutes"
|
250
|
+
for i in range(6):
|
251
|
+
if i not in duration_per_zone:
|
252
|
+
duration_per_zone.loc[i] = 0.0
|
253
|
+
result = duration_per_zone.reset_index()
|
254
|
+
return result
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import pickle
|
4
|
+
from typing import Any
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import pandas as pd
|
9
|
+
from tqdm import tqdm
|
10
|
+
|
11
|
+
from geo_activity_playground.core.activities import ActivityMeta
|
12
|
+
from geo_activity_playground.core.activities import make_activity_meta
|
13
|
+
from geo_activity_playground.core.coordinates import get_distance
|
14
|
+
from geo_activity_playground.core.paths import activity_enriched_meta_dir
|
15
|
+
from geo_activity_playground.core.paths import activity_enriched_time_series_dir
|
16
|
+
from geo_activity_playground.core.paths import activity_extracted_meta_dir
|
17
|
+
from geo_activity_playground.core.paths import activity_extracted_time_series_dir
|
18
|
+
from geo_activity_playground.core.tiles import compute_tile_float
|
19
|
+
from geo_activity_playground.core.time_conversion import convert_to_datetime_ns
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
def enrich_activities(kind_defaults: dict[dict[str, Any]]) -> None:
|
25
|
+
# Delete removed activities.
|
26
|
+
for enriched_metadata_path in activity_enriched_meta_dir().glob("*.pickle"):
|
27
|
+
if not (activity_extracted_meta_dir() / enriched_metadata_path.name).exists():
|
28
|
+
logger.warning(f"Deleting {enriched_metadata_path}")
|
29
|
+
enriched_metadata_path.unlink()
|
30
|
+
for enriched_time_series_path in activity_enriched_time_series_dir().glob(
|
31
|
+
"*.parquet"
|
32
|
+
):
|
33
|
+
if not (
|
34
|
+
activity_extracted_time_series_dir() / enriched_time_series_path.name
|
35
|
+
).exists():
|
36
|
+
logger.warning(f"Deleting {enriched_time_series_path}")
|
37
|
+
enriched_time_series_path.unlink()
|
38
|
+
|
39
|
+
# Get new metadata paths.
|
40
|
+
new_extracted_metadata_paths = []
|
41
|
+
for extracted_metadata_path in activity_extracted_meta_dir().glob("*.pickle"):
|
42
|
+
enriched_metadata_path = (
|
43
|
+
activity_enriched_meta_dir() / extracted_metadata_path.name
|
44
|
+
)
|
45
|
+
if (
|
46
|
+
not enriched_metadata_path.exists()
|
47
|
+
or enriched_metadata_path.stat().st_mtime
|
48
|
+
< extracted_metadata_path.stat().st_mtime
|
49
|
+
):
|
50
|
+
new_extracted_metadata_paths.append(extracted_metadata_path)
|
51
|
+
|
52
|
+
for extracted_metadata_path in tqdm(
|
53
|
+
new_extracted_metadata_paths, desc="Enrich new activity data"
|
54
|
+
):
|
55
|
+
# Read extracted data.
|
56
|
+
activity_id = extracted_metadata_path.stem
|
57
|
+
extracted_time_series_path = (
|
58
|
+
activity_extracted_time_series_dir() / f"{activity_id}.parquet"
|
59
|
+
)
|
60
|
+
time_series = pd.read_parquet(extracted_time_series_path)
|
61
|
+
with open(extracted_metadata_path, "rb") as f:
|
62
|
+
extracted_metadata = pickle.load(f)
|
63
|
+
|
64
|
+
metadata = make_activity_meta()
|
65
|
+
metadata.update(extracted_metadata)
|
66
|
+
|
67
|
+
# Enrich time series.
|
68
|
+
metadata.update(kind_defaults.get(metadata["kind"], {}))
|
69
|
+
time_series = _embellish_single_time_series(
|
70
|
+
time_series, metadata.get("start", None)
|
71
|
+
)
|
72
|
+
metadata.update(_get_metadata_from_timeseries(time_series))
|
73
|
+
|
74
|
+
# Write enriched data.
|
75
|
+
enriched_metadata_path = activity_enriched_meta_dir() / f"{activity_id}.pickle"
|
76
|
+
enriched_time_series_path = (
|
77
|
+
activity_enriched_time_series_dir() / f"{activity_id}.parquet"
|
78
|
+
)
|
79
|
+
with open(enriched_metadata_path, "wb") as f:
|
80
|
+
pickle.dump(metadata, f)
|
81
|
+
time_series.to_parquet(enriched_time_series_path)
|
82
|
+
|
83
|
+
|
84
|
+
def _get_metadata_from_timeseries(timeseries: pd.DataFrame) -> ActivityMeta:
|
85
|
+
metadata = ActivityMeta()
|
86
|
+
|
87
|
+
# Extract some meta data from the time series.
|
88
|
+
metadata["start"] = timeseries["time"].iloc[0]
|
89
|
+
metadata["elapsed_time"] = timeseries["time"].iloc[-1] - timeseries["time"].iloc[0]
|
90
|
+
metadata["distance_km"] = timeseries["distance_km"].iloc[-1]
|
91
|
+
if "calories" in timeseries.columns:
|
92
|
+
metadata["calories"] = timeseries["calories"].iloc[-1]
|
93
|
+
metadata["moving_time"] = _compute_moving_time(timeseries)
|
94
|
+
|
95
|
+
metadata["start_latitude"] = timeseries["latitude"].iloc[0]
|
96
|
+
metadata["end_latitude"] = timeseries["latitude"].iloc[-1]
|
97
|
+
metadata["start_longitude"] = timeseries["longitude"].iloc[0]
|
98
|
+
metadata["end_longitude"] = timeseries["longitude"].iloc[-1]
|
99
|
+
|
100
|
+
return metadata
|
101
|
+
|
102
|
+
|
103
|
+
def _compute_moving_time(time_series: pd.DataFrame) -> datetime.timedelta:
|
104
|
+
def moving_time(group) -> datetime.timedelta:
|
105
|
+
selection = group["speed"] > 1.0
|
106
|
+
time_diff = group["time"].diff().loc[selection]
|
107
|
+
return time_diff.sum()
|
108
|
+
|
109
|
+
return (
|
110
|
+
time_series.groupby("segment_id").apply(moving_time, include_groups=False).sum()
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
def _embellish_single_time_series(
|
115
|
+
timeseries: pd.DataFrame, start: Optional[datetime.datetime] = None
|
116
|
+
) -> pd.DataFrame:
|
117
|
+
if start is not None and pd.api.types.is_dtype_equal(
|
118
|
+
timeseries["time"].dtype, "int64"
|
119
|
+
):
|
120
|
+
time = timeseries["time"]
|
121
|
+
del timeseries["time"]
|
122
|
+
timeseries["time"] = [
|
123
|
+
convert_to_datetime_ns(start + datetime.timedelta(seconds=t)) for t in time
|
124
|
+
]
|
125
|
+
timeseries["time"] = convert_to_datetime_ns(timeseries["time"])
|
126
|
+
assert pd.api.types.is_dtype_equal(timeseries["time"].dtype, "datetime64[ns]"), (
|
127
|
+
timeseries["time"].dtype,
|
128
|
+
timeseries["time"].iloc[0],
|
129
|
+
)
|
130
|
+
|
131
|
+
distances = get_distance(
|
132
|
+
timeseries["latitude"].shift(1),
|
133
|
+
timeseries["longitude"].shift(1),
|
134
|
+
timeseries["latitude"],
|
135
|
+
timeseries["longitude"],
|
136
|
+
).fillna(0.0)
|
137
|
+
time_diff_threshold_seconds = 30
|
138
|
+
time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
|
139
|
+
jump_indices = time_diff >= time_diff_threshold_seconds
|
140
|
+
distances.loc[jump_indices] = 0.0
|
141
|
+
|
142
|
+
if "distance_km" not in timeseries.columns:
|
143
|
+
timeseries["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
|
144
|
+
|
145
|
+
if "speed" not in timeseries.columns:
|
146
|
+
timeseries["speed"] = (
|
147
|
+
timeseries["distance_km"].diff()
|
148
|
+
/ (timeseries["time"].diff().dt.total_seconds() + 1e-3)
|
149
|
+
* 3600
|
150
|
+
)
|
151
|
+
|
152
|
+
potential_jumps = (timeseries["speed"] > 40) & (timeseries["speed"].diff() > 10)
|
153
|
+
if np.any(potential_jumps):
|
154
|
+
timeseries = timeseries.loc[~potential_jumps].copy()
|
155
|
+
|
156
|
+
if "segment_id" not in timeseries.columns:
|
157
|
+
timeseries["segment_id"] = np.cumsum(jump_indices)
|
158
|
+
|
159
|
+
if "x" not in timeseries.columns:
|
160
|
+
x, y = compute_tile_float(timeseries["latitude"], timeseries["longitude"], 0)
|
161
|
+
timeseries["x"] = x
|
162
|
+
timeseries["y"] = y
|
163
|
+
|
164
|
+
return timeseries
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""
|
2
|
+
Paths within the playground and cache.
|
3
|
+
"""
|
4
|
+
import functools
|
5
|
+
import pathlib
|
6
|
+
import typing
|
7
|
+
|
8
|
+
|
9
|
+
def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
10
|
+
@functools.cache
|
11
|
+
def wrapper() -> pathlib.Path:
|
12
|
+
path.mkdir(exist_ok=True, parents=True)
|
13
|
+
return path
|
14
|
+
|
15
|
+
return wrapper
|
16
|
+
|
17
|
+
|
18
|
+
def file_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
19
|
+
@functools.cache
|
20
|
+
def wrapper() -> pathlib.Path:
|
21
|
+
path.parent.mkdir(exist_ok=True, parents=True)
|
22
|
+
return path
|
23
|
+
|
24
|
+
return wrapper
|
25
|
+
|
26
|
+
|
27
|
+
_cache_dir = pathlib.Path("Cache")
|
28
|
+
|
29
|
+
_activity_dir = _cache_dir / "Activity"
|
30
|
+
_activity_extracted_dir = _activity_dir / "Extracted"
|
31
|
+
_activity_extracted_meta_dir = _activity_extracted_dir / "Meta"
|
32
|
+
_activity_extracted_time_series_dir = _activity_extracted_dir / "Time Series"
|
33
|
+
|
34
|
+
_activity_enriched_dir = _activity_dir / "Enriched"
|
35
|
+
_activity_enriched_meta_dir = _activity_enriched_dir / "Meta"
|
36
|
+
_activity_enriched_time_series_dir = _activity_enriched_dir / "Time Series"
|
37
|
+
_activities_file = _activity_dir / "activities.parquet"
|
38
|
+
|
39
|
+
_tiles_per_time_series = _cache_dir / "Tiles" / "Tiles Per Time Series"
|
40
|
+
|
41
|
+
_strava_api_dir = pathlib.Path("Strava API")
|
42
|
+
_strava_dynamic_config_path = _strava_api_dir / "strava-client-id.json"
|
43
|
+
|
44
|
+
|
45
|
+
cache_dir = dir_wrapper(_cache_dir)
|
46
|
+
|
47
|
+
activity_extracted_dir = dir_wrapper(_activity_extracted_dir)
|
48
|
+
activity_extracted_meta_dir = dir_wrapper(_activity_extracted_meta_dir)
|
49
|
+
activity_extracted_time_series_dir = dir_wrapper(_activity_extracted_time_series_dir)
|
50
|
+
activity_enriched_meta_dir = dir_wrapper(_activity_enriched_meta_dir)
|
51
|
+
activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_dir)
|
52
|
+
tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
|
53
|
+
strava_api_dir = dir_wrapper(_strava_api_dir)
|
54
|
+
|
55
|
+
activities_file = file_wrapper(_activities_file)
|
56
|
+
strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
|
@@ -50,8 +50,8 @@ def work_tracker(path: pathlib.Path):
|
|
50
50
|
|
51
51
|
|
52
52
|
class WorkTracker:
|
53
|
-
def __init__(self,
|
54
|
-
self._path =
|
53
|
+
def __init__(self, path: pathlib.Path) -> None:
|
54
|
+
self._path = path
|
55
55
|
|
56
56
|
if self._path.exists():
|
57
57
|
with open(self._path, "rb") as f:
|
@@ -59,12 +59,15 @@ class WorkTracker:
|
|
59
59
|
else:
|
60
60
|
self._done = set()
|
61
61
|
|
62
|
-
def filter(self, ids: Iterable
|
62
|
+
def filter(self, ids: Iterable) -> set:
|
63
63
|
return set(ids) - self._done
|
64
64
|
|
65
65
|
def mark_done(self, id: int) -> None:
|
66
66
|
self._done.add(id)
|
67
67
|
|
68
|
+
def discard(self, id) -> None:
|
69
|
+
self._done.discard(id)
|
70
|
+
|
68
71
|
def close(self) -> None:
|
69
72
|
with open(self._path, "wb") as f:
|
70
73
|
pickle.dump(self._done, f)
|
@@ -77,3 +80,23 @@ def try_load_pickle(path: pathlib.Path) -> Any:
|
|
77
80
|
return pickle.load(f)
|
78
81
|
except ModuleNotFoundError:
|
79
82
|
pass
|
83
|
+
|
84
|
+
|
85
|
+
class TransformVersion:
|
86
|
+
def __init__(self, path: pathlib.Path, code_version: int) -> None:
|
87
|
+
self._path = path
|
88
|
+
self._code_version = code_version
|
89
|
+
|
90
|
+
with open(path) as f:
|
91
|
+
self._actual_version = json.load(f)
|
92
|
+
|
93
|
+
assert (
|
94
|
+
self._actual_version <= self._code_version
|
95
|
+
), "You attempt to use a more modern playground with an older code version, that is not supported."
|
96
|
+
|
97
|
+
def outdated(self) -> bool:
|
98
|
+
return self._actual_version < self._code_version
|
99
|
+
|
100
|
+
def write(self) -> None:
|
101
|
+
with open(self._path, "w") as f:
|
102
|
+
json.dump(self._code_version, f)
|