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.
Files changed (116) hide show
  1. geo_activity_playground/__main__.py +5 -47
  2. geo_activity_playground/alembic/README +1 -0
  3. geo_activity_playground/alembic/env.py +76 -0
  4. geo_activity_playground/alembic/script.py.mako +26 -0
  5. geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +33 -0
  6. geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +73 -0
  7. geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +28 -0
  8. geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +30 -0
  9. geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +28 -0
  10. geo_activity_playground/alembic/versions/script.py.mako +28 -0
  11. geo_activity_playground/core/activities.py +53 -136
  12. geo_activity_playground/core/config.py +3 -3
  13. geo_activity_playground/core/datamodel.py +257 -0
  14. geo_activity_playground/core/enrichment.py +90 -92
  15. geo_activity_playground/core/heart_rate.py +1 -2
  16. geo_activity_playground/core/parametric_plot.py +101 -0
  17. geo_activity_playground/core/paths.py +6 -7
  18. geo_activity_playground/core/raster_map.py +43 -4
  19. geo_activity_playground/core/similarity.py +1 -2
  20. geo_activity_playground/core/tasks.py +2 -2
  21. geo_activity_playground/core/test_meta_search.py +3 -3
  22. geo_activity_playground/core/test_summary_stats.py +1 -1
  23. geo_activity_playground/explorer/grid_file.py +2 -2
  24. geo_activity_playground/explorer/tile_visits.py +8 -10
  25. geo_activity_playground/heatmap_video.py +7 -8
  26. geo_activity_playground/importers/activity_parsers.py +2 -2
  27. geo_activity_playground/importers/directory.py +9 -10
  28. geo_activity_playground/importers/strava_api.py +9 -9
  29. geo_activity_playground/importers/strava_checkout.py +12 -13
  30. geo_activity_playground/importers/test_csv_parser.py +3 -3
  31. geo_activity_playground/importers/test_directory.py +1 -1
  32. geo_activity_playground/importers/test_strava_api.py +1 -1
  33. geo_activity_playground/webui/app.py +96 -86
  34. geo_activity_playground/webui/authenticator.py +1 -1
  35. geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
  36. geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
  37. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
  38. geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
  39. geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +5 -5
  40. geo_activity_playground/webui/blueprints/entry_views.py +68 -0
  41. geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +37 -4
  42. geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
  43. geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
  44. geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +43 -0
  45. geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
  46. geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
  47. geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
  48. geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +11 -23
  49. geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
  50. geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
  51. geo_activity_playground/webui/flasher.py +26 -0
  52. geo_activity_playground/webui/plot_util.py +1 -1
  53. geo_activity_playground/webui/search_util.py +4 -6
  54. geo_activity_playground/webui/static/images/layers-2x.png +0 -0
  55. geo_activity_playground/webui/static/images/layers.png +0 -0
  56. geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
  57. geo_activity_playground/webui/static/images/marker-icon.png +0 -0
  58. geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
  59. geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
  60. geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
  61. geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
  62. geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +57 -33
  63. geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
  64. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
  65. geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
  66. geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
  67. geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
  68. geo_activity_playground/webui/templates/home.html.j2 +6 -6
  69. geo_activity_playground/webui/templates/page.html.j2 +2 -1
  70. geo_activity_playground/webui/templates/plot_builder/index.html.j2 +44 -0
  71. geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
  72. geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
  73. geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
  74. geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
  75. geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
  76. geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
  77. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/METADATA +5 -1
  78. geo_activity_playground-0.39.1.dist-info/RECORD +136 -0
  79. geo_activity_playground/__init__.py +0 -0
  80. geo_activity_playground/core/__init__.py +0 -0
  81. geo_activity_playground/explorer/__init__.py +0 -0
  82. geo_activity_playground/importers/__init__.py +0 -0
  83. geo_activity_playground/webui/__init__.py +0 -0
  84. geo_activity_playground/webui/activity/__init__.py +0 -0
  85. geo_activity_playground/webui/activity/blueprint.py +0 -109
  86. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
  87. geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
  88. geo_activity_playground/webui/calendar/__init__.py +0 -0
  89. geo_activity_playground/webui/calendar/blueprint.py +0 -23
  90. geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
  91. geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
  92. geo_activity_playground/webui/entry_controller.py +0 -63
  93. geo_activity_playground/webui/explorer/__init__.py +0 -0
  94. geo_activity_playground/webui/explorer/blueprint.py +0 -62
  95. geo_activity_playground/webui/heatmap/__init__.py +0 -0
  96. geo_activity_playground/webui/heatmap/blueprint.py +0 -51
  97. geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
  98. geo_activity_playground/webui/settings/blueprint.py +0 -262
  99. geo_activity_playground/webui/settings/controller.py +0 -272
  100. geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
  101. geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
  102. geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
  103. geo_activity_playground/webui/tile_blueprint.py +0 -42
  104. geo_activity_playground-0.38.2.dist-info/RECORD +0 -129
  105. /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
  106. /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
  107. /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
  108. /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
  109. /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
  110. /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
  111. /geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +0 -0
  112. /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
  113. /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
  114. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/LICENSE +0 -0
  115. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/WHEEL +0 -0
  116. {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 geo_activity_playground.core.activities import ActivityMeta
12
- from geo_activity_playground.core.activities import make_activity_meta
13
- from geo_activity_playground.core.config import Config
14
- from geo_activity_playground.core.coordinates import get_distance
15
- from geo_activity_playground.core.paths import activity_enriched_meta_dir
16
- from geo_activity_playground.core.paths import activity_enriched_time_series_dir
17
- from geo_activity_playground.core.paths import activity_extracted_meta_dir
18
- from geo_activity_playground.core.paths import activity_extracted_time_series_dir
19
- from geo_activity_playground.core.tiles import compute_tile_float
20
- from geo_activity_playground.core.time_conversion import convert_to_datetime_ns
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 enrich_activities(config: Config) -> None:
26
- # Delete removed activities.
27
- for enriched_metadata_path in activity_enriched_meta_dir().glob("*.pickle"):
28
- if not (activity_extracted_meta_dir() / enriched_metadata_path.name).exists():
29
- logger.warning(f"Deleting {enriched_metadata_path}")
30
- enriched_metadata_path.unlink()
31
- for enriched_time_series_path in activity_enriched_time_series_dir().glob(
32
- "*.parquet"
33
- ):
34
- if not (
35
- activity_extracted_time_series_dir() / enriched_time_series_path.name
36
- ).exists():
37
- logger.warning(f"Deleting {enriched_time_series_path}")
38
- enriched_time_series_path.unlink()
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
- if (
47
- not enriched_metadata_path.exists()
48
- or enriched_metadata_path.stat().st_mtime
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"{activity_id}.parquet"
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 {metadata} doesn't have latitude/longitude information. Ignoring this one."
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, metadata.get("start", None), config.time_diff_threshold_seconds
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
- # Write enriched data.
98
- enriched_metadata_path = activity_enriched_meta_dir() / f"{activity_id}.pickle"
99
- enriched_time_series_path = (
100
- activity_enriched_time_series_dir() / f"{activity_id}.parquet"
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
- def _get_metadata_from_timeseries(timeseries: pd.DataFrame) -> ActivityMeta:
108
- metadata = ActivityMeta()
94
+ DB.session.add(activity)
95
+ DB.session.commit()
109
96
 
110
- # Extract some meta data from the time series.
111
- metadata["start"] = timeseries["time"].iloc[0]
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
- return metadata
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,10 +1,9 @@
1
1
  import datetime
2
2
  import math
3
- from typing import Optional
4
3
 
5
4
  import pandas as pd
6
5
 
7
- from geo_activity_playground.core.config import Config
6
+ from .config import Config
8
7
 
9
8
 
10
9
  class HeartRateZoneComputer:
@@ -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)