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.
Files changed (113) 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 +50 -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/paths.py +6 -7
  17. geo_activity_playground/core/raster_map.py +43 -4
  18. geo_activity_playground/core/similarity.py +1 -2
  19. geo_activity_playground/core/tasks.py +2 -2
  20. geo_activity_playground/core/test_meta_search.py +3 -3
  21. geo_activity_playground/core/test_summary_stats.py +1 -1
  22. geo_activity_playground/explorer/grid_file.py +2 -2
  23. geo_activity_playground/explorer/tile_visits.py +8 -10
  24. geo_activity_playground/heatmap_video.py +7 -8
  25. geo_activity_playground/importers/activity_parsers.py +2 -2
  26. geo_activity_playground/importers/directory.py +9 -10
  27. geo_activity_playground/importers/strava_api.py +9 -9
  28. geo_activity_playground/importers/strava_checkout.py +12 -13
  29. geo_activity_playground/importers/test_csv_parser.py +3 -3
  30. geo_activity_playground/importers/test_directory.py +1 -1
  31. geo_activity_playground/importers/test_strava_api.py +1 -1
  32. geo_activity_playground/webui/app.py +94 -86
  33. geo_activity_playground/webui/authenticator.py +1 -1
  34. geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
  35. geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
  36. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
  37. geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
  38. geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +5 -5
  39. geo_activity_playground/webui/blueprints/entry_views.py +68 -0
  40. geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +37 -4
  41. geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
  42. geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
  43. geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
  44. geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
  45. geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
  46. geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +11 -23
  47. geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
  48. geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
  49. geo_activity_playground/webui/flasher.py +26 -0
  50. geo_activity_playground/webui/plot_util.py +1 -1
  51. geo_activity_playground/webui/search_util.py +4 -6
  52. geo_activity_playground/webui/static/images/layers-2x.png +0 -0
  53. geo_activity_playground/webui/static/images/layers.png +0 -0
  54. geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
  55. geo_activity_playground/webui/static/images/marker-icon.png +0 -0
  56. geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
  57. geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
  58. geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
  59. geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
  60. geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +57 -33
  61. geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
  62. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
  63. geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
  64. geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
  65. geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
  66. geo_activity_playground/webui/templates/home.html.j2 +6 -6
  67. geo_activity_playground/webui/templates/page.html.j2 +2 -1
  68. geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
  69. geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
  70. geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
  71. geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
  72. geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
  73. geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
  74. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/METADATA +5 -1
  75. geo_activity_playground-0.39.0.dist-info/RECORD +133 -0
  76. geo_activity_playground/__init__.py +0 -0
  77. geo_activity_playground/core/__init__.py +0 -0
  78. geo_activity_playground/explorer/__init__.py +0 -0
  79. geo_activity_playground/importers/__init__.py +0 -0
  80. geo_activity_playground/webui/__init__.py +0 -0
  81. geo_activity_playground/webui/activity/__init__.py +0 -0
  82. geo_activity_playground/webui/activity/blueprint.py +0 -109
  83. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
  84. geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
  85. geo_activity_playground/webui/calendar/__init__.py +0 -0
  86. geo_activity_playground/webui/calendar/blueprint.py +0 -23
  87. geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
  88. geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
  89. geo_activity_playground/webui/entry_controller.py +0 -63
  90. geo_activity_playground/webui/explorer/__init__.py +0 -0
  91. geo_activity_playground/webui/explorer/blueprint.py +0 -62
  92. geo_activity_playground/webui/heatmap/__init__.py +0 -0
  93. geo_activity_playground/webui/heatmap/blueprint.py +0 -51
  94. geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
  95. geo_activity_playground/webui/settings/blueprint.py +0 -262
  96. geo_activity_playground/webui/settings/controller.py +0 -272
  97. geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
  98. geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
  99. geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
  100. geo_activity_playground/webui/tile_blueprint.py +0 -42
  101. geo_activity_playground-0.38.2.dist-info/RECORD +0 -129
  102. /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
  103. /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
  104. /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
  105. /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
  106. /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
  107. /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
  108. /geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +0 -0
  109. /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
  110. /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
  111. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/LICENSE +0 -0
  112. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
  113. {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 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:
@@ -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 collections
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 geo_activity_playground.core.config import Config
14
- from geo_activity_playground.core.tiles import compute_tile_float
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 geo_activity_playground.core.tasks import stored_object
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 geo_activity_playground.core.paths import atomic_open
12
- from geo_activity_playground.core.paths import cache_dir
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 geo_activity_playground.core.meta_search import _make_mask
6
- from geo_activity_playground.core.meta_search import apply_search_query
7
- from geo_activity_playground.core.meta_search import SearchQuery
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:
@@ -3,7 +3,7 @@ import datetime
3
3
  import pandas as pd
4
4
  import pytest
5
5
 
6
- from geo_activity_playground.core.summary_stats import get_equipment_use_table
6
+ from .summary_stats import get_equipment_use_table
7
7
 
8
8
 
9
9
  @pytest.fixture
@@ -7,8 +7,8 @@ import geojson
7
7
  import gpxpy
8
8
  import pandas as pd
9
9
 
10
- from geo_activity_playground.core.coordinates import Bounds
11
- from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
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__)