geo-activity-playground 0.38.2__py3-none-any.whl → 0.39.0__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 +50 -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/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 +94 -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/{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/{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.0.dist-info}/METADATA +5 -1
- geo_activity_playground-0.39.0.dist-info/RECORD +133 -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.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.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=config.kinds_without_achievements.get(name, True),
|
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:
|
@@ -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)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import
|
1
|
+
import abc
|
2
2
|
import dataclasses
|
3
3
|
import functools
|
4
4
|
import logging
|
@@ -10,9 +10,8 @@ import numpy as np
|
|
10
10
|
import requests
|
11
11
|
from PIL import Image
|
12
12
|
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
13
|
+
from .config import Config
|
14
|
+
from .tiles import compute_tile_float
|
16
15
|
|
17
16
|
|
18
17
|
logger = logging.getLogger(__name__)
|
@@ -244,3 +243,43 @@ def download_file(url: str, destination: pathlib.Path):
|
|
244
243
|
with open(destination, "wb") as f:
|
245
244
|
f.write(r.content)
|
246
245
|
time.sleep(0.1)
|
246
|
+
|
247
|
+
|
248
|
+
class TileGetter:
|
249
|
+
def __init__(self, map_tile_url: str):
|
250
|
+
self._map_tile_url = map_tile_url
|
251
|
+
|
252
|
+
def get_tile(
|
253
|
+
self,
|
254
|
+
z: int,
|
255
|
+
x: int,
|
256
|
+
y: int,
|
257
|
+
):
|
258
|
+
return get_tile(z, x, y, self._map_tile_url)
|
259
|
+
|
260
|
+
|
261
|
+
class ImageTransform:
|
262
|
+
@abc.abstractmethod
|
263
|
+
def transform_image(self, image: np.ndarray) -> np.ndarray:
|
264
|
+
pass
|
265
|
+
|
266
|
+
|
267
|
+
class IdentityImageTransform(ImageTransform):
|
268
|
+
def transform_image(self, image: np.ndarray) -> np.ndarray:
|
269
|
+
return image
|
270
|
+
|
271
|
+
|
272
|
+
class GrayscaleImageTransform(ImageTransform):
|
273
|
+
def transform_image(self, image: np.ndarray) -> np.ndarray:
|
274
|
+
image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2) # to grayscale
|
275
|
+
return np.dstack((image, image, image)) # to rgb
|
276
|
+
|
277
|
+
|
278
|
+
class PastelImageTransform(ImageTransform):
|
279
|
+
def __init__(self, factor: float = 0.7):
|
280
|
+
self._factor = factor
|
281
|
+
|
282
|
+
def transform_image(self, image: np.ndarray) -> np.ndarray:
|
283
|
+
averaged_tile = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
|
284
|
+
grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
|
285
|
+
return self._factor * grayscale_tile + (1 - self._factor) * image
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import pathlib
|
2
|
-
import pickle
|
3
2
|
|
4
3
|
import imagehash
|
5
4
|
import numpy as np
|
@@ -10,7 +9,7 @@ from tqdm import tqdm
|
|
10
9
|
|
11
10
|
from .activities import ActivityRepository
|
12
11
|
from .coordinates import get_distance
|
13
|
-
from
|
12
|
+
from .tasks import stored_object
|
14
13
|
|
15
14
|
|
16
15
|
fingerprint_path = pathlib.Path("Cache/activity_fingerprints.pickle")
|
@@ -8,8 +8,8 @@ from typing import Generic
|
|
8
8
|
from typing import Sequence
|
9
9
|
from typing import TypeVar
|
10
10
|
|
11
|
-
from
|
12
|
-
from
|
11
|
+
from .paths import atomic_open
|
12
|
+
from .paths import cache_dir
|
13
13
|
|
14
14
|
|
15
15
|
T = TypeVar("T")
|
@@ -2,9 +2,9 @@ import datetime
|
|
2
2
|
|
3
3
|
import pandas as pd
|
4
4
|
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
5
|
+
from .meta_search import _make_mask
|
6
|
+
from .meta_search import apply_search_query
|
7
|
+
from .meta_search import SearchQuery
|
8
8
|
|
9
9
|
|
10
10
|
def test_empty_query() -> None:
|
@@ -7,8 +7,8 @@ import geojson
|
|
7
7
|
import gpxpy
|
8
8
|
import pandas as pd
|
9
9
|
|
10
|
-
from
|
11
|
-
from
|
10
|
+
from ..core.coordinates import Bounds
|
11
|
+
from ..core.tiles import get_tile_upper_left_lat_lon
|
12
12
|
|
13
13
|
|
14
14
|
logger = logging.getLogger(__name__)
|