geo-activity-playground 0.38.2__py3-none-any.whl → 0.39.1__py3-none-any.whl
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/__main__.py +5 -47
- geo_activity_playground/alembic/README +1 -0
- geo_activity_playground/alembic/env.py +76 -0
- geo_activity_playground/alembic/script.py.mako +26 -0
- geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +33 -0
- geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +73 -0
- geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +28 -0
- geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +30 -0
- geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +28 -0
- geo_activity_playground/alembic/versions/script.py.mako +28 -0
- geo_activity_playground/core/activities.py +53 -136
- geo_activity_playground/core/config.py +3 -3
- geo_activity_playground/core/datamodel.py +257 -0
- geo_activity_playground/core/enrichment.py +90 -92
- geo_activity_playground/core/heart_rate.py +1 -2
- geo_activity_playground/core/parametric_plot.py +101 -0
- geo_activity_playground/core/paths.py +6 -7
- geo_activity_playground/core/raster_map.py +43 -4
- geo_activity_playground/core/similarity.py +1 -2
- geo_activity_playground/core/tasks.py +2 -2
- geo_activity_playground/core/test_meta_search.py +3 -3
- geo_activity_playground/core/test_summary_stats.py +1 -1
- geo_activity_playground/explorer/grid_file.py +2 -2
- geo_activity_playground/explorer/tile_visits.py +8 -10
- geo_activity_playground/heatmap_video.py +7 -8
- geo_activity_playground/importers/activity_parsers.py +2 -2
- geo_activity_playground/importers/directory.py +9 -10
- geo_activity_playground/importers/strava_api.py +9 -9
- geo_activity_playground/importers/strava_checkout.py +12 -13
- geo_activity_playground/importers/test_csv_parser.py +3 -3
- geo_activity_playground/importers/test_directory.py +1 -1
- geo_activity_playground/importers/test_strava_api.py +1 -1
- geo_activity_playground/webui/app.py +96 -86
- geo_activity_playground/webui/authenticator.py +1 -1
- geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
- geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
- geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
- geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +5 -5
- geo_activity_playground/webui/blueprints/entry_views.py +68 -0
- geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +37 -4
- geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
- geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
- geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +43 -0
- geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
- geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
- geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
- geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +11 -23
- geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
- geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
- geo_activity_playground/webui/flasher.py +26 -0
- geo_activity_playground/webui/plot_util.py +1 -1
- geo_activity_playground/webui/search_util.py +4 -6
- geo_activity_playground/webui/static/images/layers-2x.png +0 -0
- geo_activity_playground/webui/static/images/layers.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon.png +0 -0
- geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
- geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
- geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
- geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
- geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +57 -33
- geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
- geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
- geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
- geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
- geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
- geo_activity_playground/webui/templates/home.html.j2 +6 -6
- geo_activity_playground/webui/templates/page.html.j2 +2 -1
- geo_activity_playground/webui/templates/plot_builder/index.html.j2 +44 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
- geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
- geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
- geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/METADATA +5 -1
- geo_activity_playground-0.39.1.dist-info/RECORD +136 -0
- geo_activity_playground/__init__.py +0 -0
- geo_activity_playground/core/__init__.py +0 -0
- geo_activity_playground/explorer/__init__.py +0 -0
- geo_activity_playground/importers/__init__.py +0 -0
- geo_activity_playground/webui/__init__.py +0 -0
- geo_activity_playground/webui/activity/__init__.py +0 -0
- geo_activity_playground/webui/activity/blueprint.py +0 -109
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
- geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
- geo_activity_playground/webui/calendar/__init__.py +0 -0
- geo_activity_playground/webui/calendar/blueprint.py +0 -23
- geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
- geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
- geo_activity_playground/webui/entry_controller.py +0 -63
- geo_activity_playground/webui/explorer/__init__.py +0 -0
- geo_activity_playground/webui/explorer/blueprint.py +0 -62
- geo_activity_playground/webui/heatmap/__init__.py +0 -0
- geo_activity_playground/webui/heatmap/blueprint.py +0 -51
- geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
- geo_activity_playground/webui/settings/blueprint.py +0 -262
- geo_activity_playground/webui/settings/controller.py +0 -272
- geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
- geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
- geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
- geo_activity_playground/webui/tile_blueprint.py +0 -42
- geo_activity_playground-0.38.2.dist-info/RECORD +0 -129
- /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
- /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
- /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,257 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
from typing import Any
|
4
|
+
from typing import TypedDict
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
import pandas as pd
|
8
|
+
import sqlalchemy
|
9
|
+
import sqlalchemy as sa
|
10
|
+
from flask_sqlalchemy import SQLAlchemy
|
11
|
+
from sqlalchemy import ForeignKey
|
12
|
+
from sqlalchemy import String
|
13
|
+
from sqlalchemy.orm import DeclarativeBase
|
14
|
+
from sqlalchemy.orm import Mapped
|
15
|
+
from sqlalchemy.orm import mapped_column
|
16
|
+
from sqlalchemy.orm import relationship
|
17
|
+
|
18
|
+
from .config import Config
|
19
|
+
from .paths import time_series_dir
|
20
|
+
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
class ActivityMeta(TypedDict):
|
26
|
+
average_speed_elapsed_kmh: float
|
27
|
+
average_speed_moving_kmh: float
|
28
|
+
calories: float
|
29
|
+
commute: bool
|
30
|
+
consider_for_achievements: bool
|
31
|
+
distance_km: float
|
32
|
+
elapsed_time: datetime.timedelta
|
33
|
+
elevation_gain: float
|
34
|
+
end_latitude: float
|
35
|
+
end_longitude: float
|
36
|
+
equipment: str
|
37
|
+
id: int
|
38
|
+
kind: str
|
39
|
+
moving_time: datetime.timedelta
|
40
|
+
name: str
|
41
|
+
path: str
|
42
|
+
start_latitude: float
|
43
|
+
start_longitude: float
|
44
|
+
start: np.datetime64
|
45
|
+
steps: int
|
46
|
+
|
47
|
+
|
48
|
+
class Base(DeclarativeBase):
|
49
|
+
pass
|
50
|
+
|
51
|
+
|
52
|
+
DB = SQLAlchemy(model_class=Base)
|
53
|
+
|
54
|
+
|
55
|
+
class Activity(DB.Model):
|
56
|
+
__tablename__ = "activities"
|
57
|
+
|
58
|
+
# Housekeeping data:
|
59
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
60
|
+
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
61
|
+
distance_km: Mapped[float] = mapped_column(sa.Float, nullable=False)
|
62
|
+
|
63
|
+
# Where it comes from:
|
64
|
+
path: Mapped[str] = mapped_column(sa.String, nullable=True)
|
65
|
+
upstream_id: Mapped[str] = mapped_column(sa.String, nullable=True)
|
66
|
+
|
67
|
+
# Crop data:
|
68
|
+
index_begin: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
69
|
+
index_end: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
70
|
+
|
71
|
+
# Temporal data:
|
72
|
+
start: Mapped[datetime.datetime] = mapped_column(sa.DateTime, nullable=True)
|
73
|
+
elapsed_time: Mapped[datetime.timedelta] = mapped_column(sa.Interval, nullable=True)
|
74
|
+
moving_time: Mapped[datetime.timedelta] = mapped_column(sa.Interval, nullable=True)
|
75
|
+
|
76
|
+
# Geographic data:
|
77
|
+
start_latitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
78
|
+
start_longitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
79
|
+
end_latitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
80
|
+
end_longitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
81
|
+
|
82
|
+
# Elevation data:
|
83
|
+
elevation_gain: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
84
|
+
start_elevation: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
85
|
+
end_elevation: Mapped[float] = mapped_column(sa.Float, nullable=True)
|
86
|
+
|
87
|
+
# Health data:
|
88
|
+
calories: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
89
|
+
steps: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
90
|
+
|
91
|
+
# Tile achievements:
|
92
|
+
num_new_tiles_14: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
93
|
+
num_new_tiles_17: Mapped[int] = mapped_column(sa.Integer, nullable=True)
|
94
|
+
|
95
|
+
# References to other tables:
|
96
|
+
equipment_id: Mapped[int] = mapped_column(
|
97
|
+
ForeignKey("equipments.id", name="equipment_id"), nullable=True
|
98
|
+
)
|
99
|
+
equipment: Mapped["Equipment"] = relationship(back_populates="activities")
|
100
|
+
kind_id: Mapped[int] = mapped_column(
|
101
|
+
ForeignKey("kinds.id", name="kind_id"), nullable=True
|
102
|
+
)
|
103
|
+
kind: Mapped["Kind"] = relationship(back_populates="activities")
|
104
|
+
|
105
|
+
def __getitem__(self, item) -> Any:
|
106
|
+
return self.to_dict()[item]
|
107
|
+
|
108
|
+
def __str__(self) -> str:
|
109
|
+
return f"{self.start} {self.name}"
|
110
|
+
|
111
|
+
@property
|
112
|
+
def average_speed_moving_kmh(self) -> float:
|
113
|
+
return self.distance_km / (self.moving_time.total_seconds() / 3_600)
|
114
|
+
|
115
|
+
@property
|
116
|
+
def average_speed_elapsed_kmh(self) -> float:
|
117
|
+
return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
|
118
|
+
|
119
|
+
@property
|
120
|
+
def raw_time_series(self) -> pd.DataFrame:
|
121
|
+
path = time_series_dir() / f"{self.id}.parquet"
|
122
|
+
try:
|
123
|
+
return pd.read_parquet(path)
|
124
|
+
except OSError as e:
|
125
|
+
logger.error(f"Error while reading {path}, deleting cache file …")
|
126
|
+
path.unlink(missing_ok=True)
|
127
|
+
raise
|
128
|
+
|
129
|
+
@property
|
130
|
+
def time_series(self) -> pd.DataFrame:
|
131
|
+
if self.index_begin or self.index_end:
|
132
|
+
return self.raw_time_series.iloc[
|
133
|
+
self.index_begin or 0 : self.index_end or -1
|
134
|
+
]
|
135
|
+
else:
|
136
|
+
return self.raw_time_series
|
137
|
+
|
138
|
+
def to_dict(self) -> ActivityMeta:
|
139
|
+
equipment = self.equipment.name if self.equipment is not None else "Unknown"
|
140
|
+
kind = self.kind.name if self.kind is not None else "Unknown"
|
141
|
+
consider_for_achievements = (
|
142
|
+
self.kind.consider_for_achievements if self.kind is not None else True
|
143
|
+
)
|
144
|
+
return ActivityMeta(
|
145
|
+
id=self.id,
|
146
|
+
name=self.name,
|
147
|
+
path=self.path,
|
148
|
+
distance_km=self.distance_km,
|
149
|
+
start=self.start,
|
150
|
+
elapsed_time=self.elapsed_time,
|
151
|
+
moving_time=self.moving_time,
|
152
|
+
start_latitude=self.start_latitude,
|
153
|
+
start_longitude=self.start_longitude,
|
154
|
+
end_latitude=self.end_latitude,
|
155
|
+
end_longitude=self.end_longitude,
|
156
|
+
elevation_gain=self.elevation_gain,
|
157
|
+
start_elevation=self.start_elevation,
|
158
|
+
end_elevation=self.end_elevation,
|
159
|
+
calories=self.calories,
|
160
|
+
steps=self.steps,
|
161
|
+
num_new_tiles_14=self.num_new_tiles_14,
|
162
|
+
num_new_tiles_17=self.num_new_tiles_17,
|
163
|
+
equipment=equipment,
|
164
|
+
kind=kind,
|
165
|
+
average_speed_moving_kmh=self.average_speed_moving_kmh,
|
166
|
+
average_speed_elapsed_kmh=self.average_speed_elapsed_kmh,
|
167
|
+
consider_for_achievements=consider_for_achievements,
|
168
|
+
)
|
169
|
+
|
170
|
+
|
171
|
+
class Equipment(DB.Model):
|
172
|
+
__tablename__ = "equipments"
|
173
|
+
|
174
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
175
|
+
|
176
|
+
name: Mapped[str] = mapped_column(String)
|
177
|
+
offset_km: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
|
178
|
+
|
179
|
+
activities: Mapped[list["Activity"]] = relationship(
|
180
|
+
back_populates="equipment", cascade="all, delete-orphan"
|
181
|
+
)
|
182
|
+
default_for_kinds: Mapped[list["Kind"]] = relationship(
|
183
|
+
back_populates="default_equipment", cascade="all, delete-orphan"
|
184
|
+
)
|
185
|
+
|
186
|
+
def __str__(self) -> str:
|
187
|
+
return f"{self.name} ({self.offset_km} km)"
|
188
|
+
|
189
|
+
__table_args__ = (sa.UniqueConstraint("name", name="equipments_name"),)
|
190
|
+
|
191
|
+
|
192
|
+
class Kind(DB.Model):
|
193
|
+
__tablename__ = "kinds"
|
194
|
+
|
195
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
196
|
+
|
197
|
+
name: Mapped[str] = mapped_column(String)
|
198
|
+
consider_for_achievements: Mapped[bool] = mapped_column(
|
199
|
+
sa.Boolean, default=True, nullable=False
|
200
|
+
)
|
201
|
+
|
202
|
+
activities: Mapped[list["Activity"]] = relationship(
|
203
|
+
back_populates="kind", cascade="all, delete-orphan"
|
204
|
+
)
|
205
|
+
default_equipment_id: Mapped[int] = mapped_column(
|
206
|
+
ForeignKey("equipments.id", name="default_equipment_id"), nullable=True
|
207
|
+
)
|
208
|
+
default_equipment: Mapped["Equipment"] = relationship(
|
209
|
+
back_populates="default_for_kinds"
|
210
|
+
)
|
211
|
+
|
212
|
+
__table_args__ = (sa.UniqueConstraint("name", name="kinds_name"),)
|
213
|
+
|
214
|
+
|
215
|
+
class SquarePlannerBookmark(DB.Model):
|
216
|
+
__tablename__ = "square_planner_bookmarks"
|
217
|
+
|
218
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
219
|
+
|
220
|
+
zoom: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
221
|
+
x: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
222
|
+
y: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
223
|
+
size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
224
|
+
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
225
|
+
|
226
|
+
__table_args__ = (sa.UniqueConstraint("zoom", "x", "y", "size", name="kinds_name"),)
|
227
|
+
|
228
|
+
|
229
|
+
def get_or_make_kind(name: str, config: Config) -> Kind:
|
230
|
+
kinds = DB.session.scalars(sqlalchemy.select(Kind).where(Kind.name == name)).all()
|
231
|
+
if kinds:
|
232
|
+
assert len(kinds) == 1, f"There must be only one kind with name '{name}'."
|
233
|
+
return kinds[0]
|
234
|
+
else:
|
235
|
+
kind = Kind(
|
236
|
+
name=name,
|
237
|
+
consider_for_achievements=name in config.kinds_without_achievements,
|
238
|
+
)
|
239
|
+
DB.session.add(kind)
|
240
|
+
return kind
|
241
|
+
|
242
|
+
|
243
|
+
def get_or_make_equipment(name: str, config: Config) -> Equipment:
|
244
|
+
equipments = DB.session.scalars(
|
245
|
+
sqlalchemy.select(Equipment).where(Equipment.name == name)
|
246
|
+
).all()
|
247
|
+
if equipments:
|
248
|
+
assert (
|
249
|
+
len(equipments) == 1
|
250
|
+
), f"There must be only one equipment with name '{name}'."
|
251
|
+
return equipments[0]
|
252
|
+
else:
|
253
|
+
equipment = Equipment(
|
254
|
+
name=name, offset_km=config.equipment_offsets.get(name, 0)
|
255
|
+
)
|
256
|
+
DB.session.add(equipment)
|
257
|
+
return equipment
|
@@ -1,128 +1,126 @@
|
|
1
1
|
import datetime
|
2
2
|
import logging
|
3
3
|
import pickle
|
4
|
-
from typing import Any
|
5
4
|
from typing import Optional
|
6
5
|
|
7
6
|
import numpy as np
|
8
7
|
import pandas as pd
|
8
|
+
import sqlalchemy
|
9
9
|
from tqdm import tqdm
|
10
10
|
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from
|
11
|
+
from .config import Config
|
12
|
+
from .coordinates import get_distance
|
13
|
+
from .datamodel import Activity
|
14
|
+
from .datamodel import ActivityMeta
|
15
|
+
from .datamodel import DB
|
16
|
+
from .datamodel import get_or_make_equipment
|
17
|
+
from .datamodel import get_or_make_kind
|
18
|
+
from .paths import activity_extracted_meta_dir
|
19
|
+
from .paths import activity_extracted_time_series_dir
|
20
|
+
from .paths import time_series_dir
|
21
|
+
from .tiles import compute_tile_float
|
22
|
+
from .time_conversion import convert_to_datetime_ns
|
21
23
|
|
22
24
|
logger = logging.getLogger(__name__)
|
23
25
|
|
24
26
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# Get new metadata paths.
|
41
|
-
new_extracted_metadata_paths = []
|
42
|
-
for extracted_metadata_path in activity_extracted_meta_dir().glob("*.pickle"):
|
43
|
-
enriched_metadata_path = (
|
44
|
-
activity_enriched_meta_dir() / extracted_metadata_path.name
|
27
|
+
def populate_database_from_extracted(config: Config) -> None:
|
28
|
+
available_ids = {
|
29
|
+
int(path.stem) for path in activity_extracted_meta_dir().glob("*.pickle")
|
30
|
+
}
|
31
|
+
present_ids = {
|
32
|
+
int(elem)
|
33
|
+
for elem in DB.session.scalars(sqlalchemy.select(Activity.upstream_id)).all()
|
34
|
+
if elem
|
35
|
+
}
|
36
|
+
new_ids = available_ids - present_ids
|
37
|
+
|
38
|
+
for upstream_id in tqdm(new_ids, desc="Importing new activities into database"):
|
39
|
+
extracted_metadata_path = (
|
40
|
+
activity_extracted_meta_dir() / f"{upstream_id}.pickle"
|
45
41
|
)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
< extracted_metadata_path.stat().st_mtime
|
50
|
-
):
|
51
|
-
extracted_time_series_path = (
|
52
|
-
activity_extracted_time_series_dir()
|
53
|
-
/ f"{extracted_metadata_path.stem}.parquet"
|
54
|
-
)
|
55
|
-
if extracted_time_series_path.exists():
|
56
|
-
new_extracted_metadata_paths.append(extracted_metadata_path)
|
57
|
-
else:
|
58
|
-
logger.error(
|
59
|
-
f"Extracted activity metadata {extracted_metadata_path} is lacking the corresponding time series path {extracted_time_series_path}. Likely that is an activity without location data. Deleting this."
|
60
|
-
)
|
61
|
-
extracted_metadata_path.unlink()
|
62
|
-
|
63
|
-
for extracted_metadata_path in tqdm(
|
64
|
-
new_extracted_metadata_paths, desc="Enrich new activity data"
|
65
|
-
):
|
66
|
-
# Read extracted data.
|
67
|
-
activity_id = extracted_metadata_path.stem
|
42
|
+
with open(extracted_metadata_path, "rb") as f:
|
43
|
+
extracted_metadata: ActivityMeta = pickle.load(f)
|
44
|
+
|
68
45
|
extracted_time_series_path = (
|
69
|
-
activity_extracted_time_series_dir() / f"{
|
46
|
+
activity_extracted_time_series_dir() / f"{upstream_id}.parquet"
|
70
47
|
)
|
71
48
|
time_series = pd.read_parquet(extracted_time_series_path)
|
72
|
-
with open(extracted_metadata_path, "rb") as f:
|
73
|
-
extracted_metadata = pickle.load(f)
|
74
|
-
|
75
|
-
metadata = make_activity_meta()
|
76
|
-
metadata.update(extracted_metadata)
|
77
49
|
|
78
50
|
# Skip activities that don't have geo information attached to them. This shouldn't happen, though.
|
79
51
|
if "latitude" not in time_series.columns:
|
80
52
|
logger.warning(
|
81
|
-
f"Activity {
|
53
|
+
f"Activity {upstream_id} doesn't have latitude/longitude information. Ignoring this one."
|
82
54
|
)
|
83
55
|
continue
|
84
56
|
|
85
|
-
# Rename kinds if needed.
|
86
|
-
if metadata["kind"] in config.kind_renames:
|
87
|
-
metadata["kind"] = config.kind_renames[metadata["kind"]]
|
88
|
-
|
89
|
-
# Enrich time series.
|
90
|
-
if metadata["kind"] in config.kinds_without_achievements:
|
91
|
-
metadata["consider_for_achievements"] = False
|
92
57
|
time_series = _embellish_single_time_series(
|
93
|
-
time_series,
|
58
|
+
time_series,
|
59
|
+
extracted_metadata.get("start", None),
|
60
|
+
config.time_diff_threshold_seconds,
|
94
61
|
)
|
95
|
-
metadata.update(_get_metadata_from_timeseries(time_series))
|
96
62
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
63
|
+
kind_name = extracted_metadata.get("kind", None)
|
64
|
+
if kind_name:
|
65
|
+
# Rename kinds if needed.
|
66
|
+
if kind_name in config.kind_renames:
|
67
|
+
kind_name = config.kind_renames[kind_name]
|
68
|
+
kind = get_or_make_kind(kind_name, config)
|
69
|
+
else:
|
70
|
+
kind = None
|
71
|
+
|
72
|
+
equipment_name = extracted_metadata.get("equipment", None)
|
73
|
+
if equipment_name:
|
74
|
+
equipment = get_or_make_equipment(equipment_name, config)
|
75
|
+
elif kind:
|
76
|
+
equipment = kind.default_equipment
|
77
|
+
else:
|
78
|
+
equipment = None
|
79
|
+
|
80
|
+
activity = Activity(
|
81
|
+
name=extracted_metadata.get("name", "Name Placeholder"),
|
82
|
+
distance_km=0,
|
83
|
+
equipment=equipment,
|
84
|
+
kind=kind,
|
85
|
+
calories=extracted_metadata.get("calories", None),
|
86
|
+
elevation_gain=extracted_metadata.get("elevation_gain", None),
|
87
|
+
steps=extracted_metadata.get("steps", None),
|
88
|
+
path=extracted_metadata.get("path", None),
|
89
|
+
upstream_id=upstream_id,
|
101
90
|
)
|
102
|
-
with open(enriched_metadata_path, "wb") as f:
|
103
|
-
pickle.dump(metadata, f)
|
104
|
-
time_series.to_parquet(enriched_time_series_path)
|
105
91
|
|
92
|
+
update_via_time_series(activity, time_series)
|
106
93
|
|
107
|
-
|
108
|
-
|
94
|
+
DB.session.add(activity)
|
95
|
+
DB.session.commit()
|
109
96
|
|
110
|
-
|
111
|
-
|
112
|
-
metadata["elapsed_time"] = timeseries["time"].iloc[-1] - timeseries["time"].iloc[0]
|
113
|
-
metadata["distance_km"] = timeseries["distance_km"].iloc[-1]
|
114
|
-
if "calories" in timeseries.columns:
|
115
|
-
metadata["calories"] = timeseries["calories"].iloc[-1]
|
116
|
-
metadata["moving_time"] = _compute_moving_time(timeseries)
|
97
|
+
enriched_time_series_path = time_series_dir() / f"{activity.id}.parquet"
|
98
|
+
time_series.to_parquet(enriched_time_series_path)
|
117
99
|
|
118
|
-
metadata["start_latitude"] = timeseries["latitude"].iloc[0]
|
119
|
-
metadata["end_latitude"] = timeseries["latitude"].iloc[-1]
|
120
|
-
metadata["start_longitude"] = timeseries["longitude"].iloc[0]
|
121
|
-
metadata["end_longitude"] = timeseries["longitude"].iloc[-1]
|
122
|
-
if "elevation_gain_cum" in timeseries.columns:
|
123
|
-
metadata["elevation_gain"] = timeseries["elevation_gain_cum"].iloc[-1]
|
124
100
|
|
125
|
-
|
101
|
+
def update_via_time_series(
|
102
|
+
activity: Activity, time_series: pd.DataFrame
|
103
|
+
) -> ActivityMeta:
|
104
|
+
activity.start = time_series["time"].iloc[0]
|
105
|
+
activity.elapsed_time = time_series["time"].iloc[-1] - time_series["time"].iloc[0]
|
106
|
+
activity.distance_km = (
|
107
|
+
time_series["distance_km"].iloc[-1] - time_series["distance_km"].iloc[0]
|
108
|
+
)
|
109
|
+
if "calories" in time_series.columns:
|
110
|
+
activity.calories = (
|
111
|
+
time_series["calories"].iloc[-1] - time_series["calories"].iloc[0]
|
112
|
+
)
|
113
|
+
activity.moving_time = _compute_moving_time(time_series)
|
114
|
+
|
115
|
+
activity.start_latitude = time_series["latitude"].iloc[0]
|
116
|
+
activity.end_latitude = time_series["latitude"].iloc[-1]
|
117
|
+
activity.start_longitude = time_series["longitude"].iloc[0]
|
118
|
+
activity.end_longitude = time_series["longitude"].iloc[-1]
|
119
|
+
if "elevation_gain_cum" in time_series.columns:
|
120
|
+
elevation_gain_cum = time_series["elevation_gain_cum"].fillna(0)
|
121
|
+
activity.elevation_gain = (
|
122
|
+
elevation_gain_cum.iloc[-1] - elevation_gain_cum.iloc[0]
|
123
|
+
)
|
126
124
|
|
127
125
|
|
128
126
|
def _compute_moving_time(time_series: pd.DataFrame) -> datetime.timedelta:
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import dataclasses
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import altair as alt
|
5
|
+
import pandas as pd
|
6
|
+
|
7
|
+
|
8
|
+
@dataclasses.dataclass
|
9
|
+
class ParametricPlotSpec:
|
10
|
+
mark: str
|
11
|
+
x: str
|
12
|
+
y: str
|
13
|
+
color: Optional[str]
|
14
|
+
shape: Optional[str]
|
15
|
+
size: Optional[str]
|
16
|
+
row: Optional[str]
|
17
|
+
column: Optional[str]
|
18
|
+
|
19
|
+
|
20
|
+
MARKS = {"point": "Point", "circle": "Circle", "area": "Area", "bar": "Bar"}
|
21
|
+
CONTINUOUS_VARIABLES = {
|
22
|
+
"distance_km": "Distance / km",
|
23
|
+
"sum(distance_km)": "Total distance / km",
|
24
|
+
"mean(distance_km)": "Average distance / km",
|
25
|
+
"start": "Date",
|
26
|
+
"hours": "Elapsed time / h",
|
27
|
+
"hours_moving": "Moving time / h",
|
28
|
+
"start_latitude": "Start latitude / °",
|
29
|
+
"start_longitude": "Start longitude / °",
|
30
|
+
"end_latitude": "End latitude / °",
|
31
|
+
"end_longitude": "End longitude / °",
|
32
|
+
"start_elevation": "Start elevation / m",
|
33
|
+
"end_elevation": "End elevation / m",
|
34
|
+
"elevation_gain": "Elevation gain / m",
|
35
|
+
"sum(elevation_gain)": "Total elevation gain / m",
|
36
|
+
"mean(elevation_gain)": "Average elevation gain / m",
|
37
|
+
"calories": "Energy / kcal",
|
38
|
+
"steps": "Steps",
|
39
|
+
"num_new_tiles_14": "New tiles 14",
|
40
|
+
"num_new_tiles_14": "New tiles 17",
|
41
|
+
"average_speed_moving_kmh": "Average moving speed / km/h",
|
42
|
+
"average_speed_elapsed_kmh": "Average elapsed speed / km/h",
|
43
|
+
}
|
44
|
+
DISCRETE_VARIABLES = {
|
45
|
+
"": "",
|
46
|
+
"year(start)": "Year",
|
47
|
+
"yearquarter(start)": "Year, Quarter",
|
48
|
+
"yearquartermonth(start)": "Year, Quarter, Month",
|
49
|
+
"yearmonth(start)": "Year, Month",
|
50
|
+
"quarter(start)": "Quarter",
|
51
|
+
"quartermonth(start)": "Quarter, Month",
|
52
|
+
"month(start)": "Month",
|
53
|
+
"date(start)": "Day of month",
|
54
|
+
"weekday(start)": "Day of week",
|
55
|
+
"iso_year": "ISO Year",
|
56
|
+
"week": "ISO Week",
|
57
|
+
"equipment": "Equipment",
|
58
|
+
"kind": "Activity kind",
|
59
|
+
"consider_for_achievements": "Consider for achievements",
|
60
|
+
}
|
61
|
+
ALL_VARIABLES = {**DISCRETE_VARIABLES, **CONTINUOUS_VARIABLES}
|
62
|
+
|
63
|
+
|
64
|
+
def make_parametric_plot(df: pd.DataFrame, spec: ParametricPlotSpec) -> str:
|
65
|
+
chart = alt.Chart(df)
|
66
|
+
|
67
|
+
match spec.mark:
|
68
|
+
case "point":
|
69
|
+
chart = chart.mark_point()
|
70
|
+
case "circle":
|
71
|
+
chart = chart.mark_circle()
|
72
|
+
case "area":
|
73
|
+
chart = chart.mark_area()
|
74
|
+
case "bar":
|
75
|
+
chart = chart.mark_bar()
|
76
|
+
|
77
|
+
encodings = [
|
78
|
+
alt.X(spec.x, title=ALL_VARIABLES[spec.x]),
|
79
|
+
alt.Y(spec.y, title=ALL_VARIABLES[spec.y]),
|
80
|
+
]
|
81
|
+
tooltips = [
|
82
|
+
alt.Tooltip(spec.x, title=ALL_VARIABLES[spec.x]),
|
83
|
+
alt.Tooltip(spec.y, title=ALL_VARIABLES[spec.y]),
|
84
|
+
]
|
85
|
+
if spec.color:
|
86
|
+
encodings.append(alt.Color(spec.color, title=ALL_VARIABLES[spec.color]))
|
87
|
+
tooltips.append(alt.Tooltip(spec.color, title=ALL_VARIABLES[spec.color]))
|
88
|
+
if spec.shape:
|
89
|
+
encodings.append(alt.Shape(spec.shape, title=ALL_VARIABLES[spec.shape]))
|
90
|
+
tooltips.append(alt.Tooltip(spec.shape, title=ALL_VARIABLES[spec.shape]))
|
91
|
+
if spec.size:
|
92
|
+
encodings.append(alt.Size(spec.size, title=ALL_VARIABLES[spec.size]))
|
93
|
+
tooltips.append(alt.Tooltip(spec.size, title=ALL_VARIABLES[spec.size]))
|
94
|
+
if spec.row:
|
95
|
+
encodings.append(alt.Row(spec.row, title=ALL_VARIABLES[spec.row]))
|
96
|
+
tooltips.append(alt.Tooltip(spec.row, title=ALL_VARIABLES[spec.row]))
|
97
|
+
if spec.column:
|
98
|
+
encodings.append(alt.Column(spec.column, title=ALL_VARIABLES[spec.column]))
|
99
|
+
tooltips.append(alt.Tooltip(spec.column, title=ALL_VARIABLES[spec.column]))
|
100
|
+
|
101
|
+
return chart.encode(*encodings).interactive().to_json(format="vega")
|
@@ -1,11 +1,12 @@
|
|
1
|
-
"""
|
2
|
-
Paths within the playground and cache.
|
3
|
-
"""
|
4
1
|
import contextlib
|
5
2
|
import functools
|
6
3
|
import pathlib
|
7
4
|
import typing
|
8
5
|
|
6
|
+
"""
|
7
|
+
Paths within the playground and cache.
|
8
|
+
"""
|
9
|
+
|
9
10
|
|
10
11
|
def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
11
12
|
def wrapper() -> pathlib.Path:
|
@@ -48,16 +49,13 @@ _tiles_per_time_series = _cache_dir / "Tiles" / "Tiles Per Time Series"
|
|
48
49
|
|
49
50
|
_strava_api_dir = pathlib.Path("Strava API")
|
50
51
|
_strava_dynamic_config_path = _strava_api_dir / "strava-client-id.json"
|
51
|
-
|
52
52
|
_strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
|
53
|
-
|
54
53
|
_new_config_file = pathlib.Path("config.json")
|
55
|
-
|
56
54
|
_activity_meta_override_dir = pathlib.Path("Metadata Override")
|
55
|
+
_time_series_dir = pathlib.Path("Time Series")
|
57
56
|
|
58
57
|
|
59
58
|
cache_dir = dir_wrapper(_cache_dir)
|
60
|
-
|
61
59
|
activity_extracted_dir = dir_wrapper(_activity_extracted_dir)
|
62
60
|
activity_extracted_meta_dir = dir_wrapper(_activity_extracted_meta_dir)
|
63
61
|
activity_extracted_time_series_dir = dir_wrapper(_activity_extracted_time_series_dir)
|
@@ -66,6 +64,7 @@ activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_d
|
|
66
64
|
tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
|
67
65
|
strava_api_dir = dir_wrapper(_strava_api_dir)
|
68
66
|
activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
|
67
|
+
time_series_dir = dir_wrapper(_time_series_dir)
|
69
68
|
|
70
69
|
activities_file = file_wrapper(_activities_file)
|
71
70
|
strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
|