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
@@ -9,47 +9,93 @@ import geojson
9
9
  import matplotlib
10
10
  import numpy as np
11
11
  import pandas as pd
12
+ import sqlalchemy
13
+ from flask import abort
14
+ from flask import Blueprint
15
+ from flask import redirect
16
+ from flask import render_template
17
+ from flask import request
18
+ from flask import Response
19
+ from flask import url_for
12
20
  from PIL import Image
13
21
  from PIL import ImageDraw
14
22
 
15
- from geo_activity_playground.core.activities import ActivityMeta
16
- from geo_activity_playground.core.activities import ActivityRepository
17
- from geo_activity_playground.core.activities import make_geojson_color_line
18
- from geo_activity_playground.core.activities import make_geojson_from_time_series
19
- from geo_activity_playground.core.activities import make_speed_color_bar
20
- from geo_activity_playground.core.config import Config
21
- from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
22
- from geo_activity_playground.core.privacy_zones import PrivacyZone
23
- from geo_activity_playground.core.raster_map import map_image_from_tile_bounds
24
- from geo_activity_playground.core.raster_map import OSM_MAX_ZOOM
25
- from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
26
- from geo_activity_playground.core.raster_map import tile_bounds_around_center
27
- from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
28
- from geo_activity_playground.explorer.grid_file import make_grid_points
29
- from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
23
+ from ...core.activities import ActivityRepository
24
+ from ...core.activities import make_geojson_color_line
25
+ from ...core.activities import make_geojson_from_time_series
26
+ from ...core.activities import make_speed_color_bar
27
+ from ...core.config import Config
28
+ from ...core.datamodel import Activity
29
+ from ...core.datamodel import DB
30
+ from ...core.datamodel import Equipment
31
+ from ...core.datamodel import Kind
32
+ from ...core.enrichment import update_via_time_series
33
+ from ...core.heart_rate import HeartRateZoneComputer
34
+ from ...core.privacy_zones import PrivacyZone
35
+ from ...core.raster_map import map_image_from_tile_bounds
36
+ from ...core.raster_map import OSM_MAX_ZOOM
37
+ from ...core.raster_map import OSM_TILE_SIZE
38
+ from ...core.raster_map import tile_bounds_around_center
39
+ from ...explorer.grid_file import make_grid_file_geojson
40
+ from ...explorer.grid_file import make_grid_points
41
+ from ...explorer.tile_visits import TileVisitAccessor
42
+ from ..authenticator import Authenticator
43
+ from ..authenticator import needs_authentication
30
44
 
31
45
  logger = logging.getLogger(__name__)
32
46
 
33
47
 
34
- class ActivityController:
35
- def __init__(
36
- self,
37
- repository: ActivityRepository,
38
- tile_visit_accessor: TileVisitAccessor,
39
- config: Config,
40
- ) -> None:
41
- self._repository = repository
42
- self._tile_visit_accessor = tile_visit_accessor
43
- self._config = config
44
- self._heart_rate_zone_computer = HeartRateZoneComputer(config)
48
+ def make_activity_blueprint(
49
+ repository: ActivityRepository,
50
+ authenticator: Authenticator,
51
+ tile_visit_accessor: TileVisitAccessor,
52
+ config: Config,
53
+ heart_rate_zone_computer: HeartRateZoneComputer,
54
+ ) -> Blueprint:
55
+ blueprint = Blueprint("activity", __name__, template_folder="templates")
45
56
 
46
- def render_activity(self, id: int) -> dict:
47
- activity = self._repository.get_activity_by_id(id)
57
+ @blueprint.route("/all")
58
+ def all():
59
+ cmap = matplotlib.colormaps["Dark2"]
60
+ fc = geojson.FeatureCollection(
61
+ features=[
62
+ geojson.Feature(
63
+ geometry=geojson.MultiLineString(
64
+ coordinates=[
65
+ [
66
+ [lon, lat]
67
+ for lat, lon in zip(
68
+ group["latitude"], group["longitude"]
69
+ )
70
+ ]
71
+ for _, group in repository.get_time_series(
72
+ activity["id"]
73
+ ).groupby("segment_id")
74
+ ]
75
+ ),
76
+ properties={
77
+ "color": matplotlib.colors.to_hex(cmap(i % 8)),
78
+ "activity_name": activity["name"],
79
+ "activity_id": str(activity["id"]),
80
+ },
81
+ )
82
+ for i, activity in enumerate(repository.iter_activities())
83
+ ]
84
+ )
85
+
86
+ context = {
87
+ "geojson": geojson.dumps(fc),
88
+ }
89
+ return render_template("activity/lines.html.j2", **context)
48
90
 
49
- time_series = self._repository.get_time_series(id)
91
+ @blueprint.route("/<int:id>")
92
+ def show(id: str):
93
+ activity = repository.get_activity_by_id(id)
94
+
95
+ time_series = repository.get_time_series(id)
50
96
  line_json = make_geojson_from_time_series(time_series)
51
97
 
52
- meta = self._repository.meta
98
+ meta = repository.meta
53
99
  similar_activities = meta.loc[
54
100
  (meta.name == activity["name"]) & (meta.id != activity["id"])
55
101
  ]
@@ -58,21 +104,17 @@ class ActivityController:
58
104
 
59
105
  new_tiles = {
60
106
  zoom: sum(
61
- self._tile_visit_accessor.tile_state["tile_history"][zoom][
62
- "activity_id"
63
- ]
107
+ tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
64
108
  == activity["id"]
65
109
  )
66
- for zoom in sorted(self._config.explorer_zoom_levels)
110
+ for zoom in sorted(config.explorer_zoom_levels)
67
111
  }
68
112
 
69
113
  new_tiles_geojson = {}
70
114
  new_tiles_per_zoom = {}
71
- for zoom in sorted(self._config.explorer_zoom_levels):
72
- new_tiles = self._tile_visit_accessor.tile_state["tile_history"][zoom].loc[
73
- self._tile_visit_accessor.tile_state["tile_history"][zoom][
74
- "activity_id"
75
- ]
115
+ for zoom in sorted(config.explorer_zoom_levels):
116
+ new_tiles = tile_visit_accessor.tile_state["tile_history"][zoom].loc[
117
+ tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
76
118
  == activity["id"]
77
119
  ]
78
120
  if len(new_tiles):
@@ -86,7 +128,7 @@ class ActivityController:
86
128
  new_tiles_geojson[zoom] = make_grid_file_geojson(points)
87
129
  new_tiles_per_zoom[zoom] = len(new_tiles)
88
130
 
89
- result = {
131
+ context = {
90
132
  "activity": activity,
91
133
  "line_json": line_json,
92
134
  "distance_time_plot": distance_time_plot(time_series),
@@ -102,39 +144,45 @@ class ActivityController:
102
144
  }
103
145
  if (
104
146
  heart_zones := _extract_heart_rate_zones(
105
- time_series, self._heart_rate_zone_computer
147
+ time_series, heart_rate_zone_computer
106
148
  )
107
149
  ) is not None:
108
- result["heart_zones_plot"] = heart_rate_zone_plot(heart_zones)
150
+ context["heart_zones_plot"] = heart_rate_zone_plot(heart_zones)
109
151
  if "altitude" in time_series.columns:
110
- result["altitude_time_plot"] = altitude_time_plot(time_series)
152
+ context["altitude_time_plot"] = altitude_time_plot(time_series)
111
153
  if "elevation_gain_cum" in time_series.columns:
112
- result["elevation_gain_cum_plot"] = elevation_gain_cum_plot(time_series)
154
+ context["elevation_gain_cum_plot"] = elevation_gain_cum_plot(time_series)
113
155
  if "heartrate" in time_series.columns:
114
- result["heartrate_time_plot"] = heart_rate_time_plot(time_series)
156
+ context["heartrate_time_plot"] = heart_rate_time_plot(time_series)
115
157
  if "cadence" in time_series.columns:
116
- result["cadence_time_plot"] = cadence_time_plot(time_series)
117
- return result
158
+ context["cadence_time_plot"] = cadence_time_plot(time_series)
159
+
160
+ return render_template("activity/show.html.j2", **context)
118
161
 
119
- def render_sharepic(self, id: int) -> bytes:
120
- activity = self._repository.get_activity_by_id(id)
121
- time_series = self._repository.get_time_series(id)
122
- for coordinates in self._config.privacy_zones.values():
162
+ @blueprint.route("/<int:id>/sharepic.png")
163
+ def sharepic(id: int):
164
+ activity = repository.get_activity_by_id(id)
165
+ time_series = repository.get_time_series(id)
166
+ for coordinates in config.privacy_zones.values():
123
167
  privacy_zone = PrivacyZone(coordinates)
124
168
  time_series = privacy_zone.filter_time_series(time_series)
125
169
  if len(time_series) == 0:
126
- time_series = self._repository.get_time_series(id)
127
- return make_sharepic(
128
- activity, time_series, self._config.sharepic_suppressed_fields, self._config
170
+ time_series = repository.get_time_series(id)
171
+ return Response(
172
+ make_sharepic(
173
+ activity, time_series, config.sharepic_suppressed_fields, config
174
+ ),
175
+ mimetype="image/png",
129
176
  )
130
177
 
131
- def render_day(self, year: int, month: int, day: int) -> dict:
132
- meta = self._repository.meta
178
+ @blueprint.route("/day/<int:year>/<int:month>/<int:day>")
179
+ def day(year: int, month: int, day: int):
180
+ meta = repository.meta
133
181
  selection = meta["start"].dt.date == datetime.date(year, month, day)
134
182
  activities_that_day = meta.loc[selection]
135
183
 
136
184
  time_series = [
137
- self._repository.get_time_series(activity_id)
185
+ repository.get_time_series(activity_id)
138
186
  for activity_id in activities_that_day["id"]
139
187
  ]
140
188
 
@@ -163,7 +211,7 @@ class ActivityController:
163
211
  for i, activity_record in enumerate(activities_list):
164
212
  activity_record["color"] = matplotlib.colors.to_hex(cmap(i % 8))
165
213
 
166
- return {
214
+ context = {
167
215
  "activities": activities_list,
168
216
  "geojson": geojson.dumps(fc),
169
217
  "date": datetime.date(year, month, day).isoformat(),
@@ -173,59 +221,36 @@ class ActivityController:
173
221
  "month": month,
174
222
  "year": year,
175
223
  }
224
+ return render_template(
225
+ "activity/day.html.j2",
226
+ **context,
227
+ )
176
228
 
177
- def render_day_sharepic(self, year: int, month: int, day: int) -> bytes:
178
- meta = self._repository.meta
229
+ @blueprint.route("/day-sharepic/<int:year>/<int:month>/<int:day>/sharepic.png")
230
+ def day_sharepic(year: int, month: int, day: int):
231
+ meta = repository.meta
179
232
  selection = meta["start"].dt.date == datetime.date(year, month, day)
180
233
  activities_that_day = meta.loc[selection]
181
234
 
182
235
  time_series = [
183
- self._repository.get_time_series(activity_id)
236
+ repository.get_time_series(activity_id)
184
237
  for activity_id in activities_that_day["id"]
185
238
  ]
186
239
  assert len(activities_that_day) > 0
187
240
  assert len(time_series) > 0
188
- return (make_day_sharepic(activities_that_day, time_series, self._config),)
189
-
190
- def render_all(self) -> dict:
191
- cmap = matplotlib.colormaps["Dark2"]
192
- fc = geojson.FeatureCollection(
193
- features=[
194
- geojson.Feature(
195
- geometry=geojson.MultiLineString(
196
- coordinates=[
197
- [
198
- [lon, lat]
199
- for lat, lon in zip(
200
- group["latitude"], group["longitude"]
201
- )
202
- ]
203
- for _, group in self._repository.get_time_series(
204
- activity["id"]
205
- ).groupby("segment_id")
206
- ]
207
- ),
208
- properties={
209
- "color": matplotlib.colors.to_hex(cmap(i % 8)),
210
- "activity_name": activity["name"],
211
- "activity_id": str(activity["id"]),
212
- },
213
- )
214
- for i, activity in enumerate(self._repository.iter_activities())
215
- ]
241
+ return Response(
242
+ make_day_sharepic(activities_that_day, time_series, config),
243
+ mimetype="image/png",
216
244
  )
217
245
 
218
- return {
219
- "geojson": geojson.dumps(fc),
220
- }
221
-
222
- def render_name(self, name: str) -> dict:
223
- meta = self._repository.meta
246
+ @blueprint.route("/name/<name>")
247
+ def name(name: str):
248
+ meta = repository.meta
224
249
  selection = meta["name"] == name
225
250
  activities_with_name = meta.loc[selection]
226
251
 
227
252
  time_series = [
228
- self._repository.get_time_series(activity_id)
253
+ repository.get_time_series(activity_id)
229
254
  for activity_id in activities_with_name["id"]
230
255
  ]
231
256
 
@@ -254,7 +279,7 @@ class ActivityController:
254
279
  for i, activity_record in enumerate(activities_list):
255
280
  activity_record["color"] = matplotlib.colors.to_hex(cmap(i % 8))
256
281
 
257
- return {
282
+ context = {
258
283
  "activities": activities_list,
259
284
  "geojson": geojson.dumps(fc),
260
285
  "name": name,
@@ -263,6 +288,115 @@ class ActivityController:
263
288
  "distance_plot": name_distance_plot(activities_with_name),
264
289
  "minutes_plot": name_minutes_plot(activities_with_name),
265
290
  }
291
+ return render_template(
292
+ "activity/name.html.j2",
293
+ **context,
294
+ )
295
+
296
+ @blueprint.route("/edit/<id>", methods=["GET", "POST"])
297
+ @needs_authentication(authenticator)
298
+ def edit(id: str):
299
+ activity = DB.session.get(Activity, int(id))
300
+ if activity is None:
301
+ abort(404)
302
+ equipments = DB.session.scalars(sqlalchemy.select(Equipment)).all()
303
+ kinds = DB.session.scalars(sqlalchemy.select(Kind)).all()
304
+
305
+ if request.method == "POST":
306
+ activity.name = request.form.get("name")
307
+
308
+ form_equipment = request.form.get("equipment")
309
+ if form_equipment == "null":
310
+ activity.equipment = None
311
+ else:
312
+ activity.equipment = DB.session.get(Equipment, int(form_equipment))
313
+
314
+ form_kind = request.form.get("kind")
315
+ if form_kind == "null":
316
+ activity.kind = None
317
+ else:
318
+ activity.kind = DB.session.get(Kind, int(form_kind))
319
+
320
+ DB.session.commit()
321
+ return redirect(url_for(".show", id=activity.id))
322
+
323
+ return render_template(
324
+ "activity/edit.html.j2",
325
+ activity=activity,
326
+ kinds=kinds,
327
+ equipments=equipments,
328
+ )
329
+
330
+ @blueprint.route("/trim/<id>", methods=["GET", "POST"])
331
+ @needs_authentication(authenticator)
332
+ def trim(id: str):
333
+ activity = DB.session.get(Activity, int(id))
334
+ if activity is None:
335
+ abort(404)
336
+
337
+ if request.method == "POST":
338
+ form_begin = request.form.get("begin")
339
+ form_end = request.form.get("end")
340
+
341
+ if form_begin:
342
+ activity.index_begin = int(form_begin)
343
+ if form_end:
344
+ activity.index_end = int(form_end)
345
+
346
+ update_via_time_series(activity, activity.time_series)
347
+
348
+ DB.session.commit()
349
+
350
+ cmap = matplotlib.colormaps["turbo"]
351
+ num_points = len(activity.time_series)
352
+ begin = activity.index_begin or 0
353
+ end = activity.index_end or num_points
354
+
355
+ fc = geojson.FeatureCollection(
356
+ features=[
357
+ geojson.Feature(
358
+ geometry=geojson.LineString(
359
+ [
360
+ (lon, lat)
361
+ for lat, lon in zip(group["latitude"], group["longitude"])
362
+ ]
363
+ )
364
+ )
365
+ for _, group in activity.raw_time_series.groupby("segment_id")
366
+ ]
367
+ + [
368
+ geojson.Feature(
369
+ geometry=geojson.Point(
370
+ (lon, lat),
371
+ ),
372
+ properties={
373
+ "name": f"{index}",
374
+ "markerType": "circle",
375
+ "markerStyle": {
376
+ "fillColor": matplotlib.colors.to_hex(
377
+ cmap(1 - index / num_points)
378
+ ),
379
+ "fillOpacity": 0.5,
380
+ "radius": 8,
381
+ "color": "black" if begin <= index < end else "white",
382
+ "opacity": 0.8,
383
+ "weight": 2,
384
+ },
385
+ },
386
+ )
387
+ for _, group in activity.raw_time_series.groupby("segment_id")
388
+ for index, lat, lon in zip(
389
+ group.index, group["latitude"], group["longitude"]
390
+ )
391
+ ]
392
+ )
393
+ return render_template(
394
+ "activity/trim.html.j2",
395
+ activity=activity,
396
+ color_line_geojson=geojson.dumps(fc),
397
+ )
398
+
399
+ return blueprint
266
400
 
267
401
 
268
402
  def speed_time_plot(time_series: pd.DataFrame) -> str:
@@ -482,7 +616,7 @@ def make_sharepic_base(time_series_list: list[pd.DataFrame], config: Config):
482
616
 
483
617
 
484
618
  def make_sharepic(
485
- activity: ActivityMeta,
619
+ activity: Activity,
486
620
  time_series: pd.DataFrame,
487
621
  sharepic_suppressed_fields: list[str],
488
622
  config: Config,
@@ -497,17 +631,21 @@ def make_sharepic(
497
631
  )
498
632
 
499
633
  facts = {
500
- "kind": f"{activity['kind']}",
501
- "start": f"{activity['start'].date()}",
502
- "equipment": f"{activity['equipment']}",
503
- "distance_km": f"\n{activity['distance_km']:.1f} km",
504
- "elapsed_time": re.sub(r"^0 days ", "", f"{activity['elapsed_time']}"),
634
+ "distance_km": f"\n{activity.distance_km:.1f} km",
505
635
  }
506
-
507
- if activity.get("calories", 0) and not pd.isna(activity["calories"]):
508
- facts["calories"] = f"{activity['calories']:.0f} kcal"
509
- if activity.get("steps", 0) and not pd.isna(activity["steps"]):
510
- facts["steps"] = f"{activity['steps']:.0f} steps"
636
+ if activity.start:
637
+ facts["start"] = f"{activity.start.date()}"
638
+ if activity.elapsed_time:
639
+ facts["elapsed_time"] = re.sub(r"^0 days ", "", f"{activity.elapsed_time}")
640
+ if activity.kind:
641
+ facts["kind"] = f"{activity.kind.name}"
642
+ if activity.equipment:
643
+ facts["equipment"] = f"{activity.equipment.name}"
644
+
645
+ if activity.calories:
646
+ facts["calories"] = f"{activity.calories} kcal"
647
+ if activity.steps:
648
+ facts["steps"] = f"{activity.steps} steps"
511
649
 
512
650
  facts = {
513
651
  key: value
@@ -4,7 +4,7 @@ from flask import render_template
4
4
  from flask import request
5
5
  from flask import url_for
6
6
 
7
- from .authenticator import Authenticator
7
+ from ..authenticator import Authenticator
8
8
 
9
9
 
10
10
  def make_auth_blueprint(authenticator: Authenticator) -> Blueprint:
@@ -0,0 +1,61 @@
1
+ import altair as alt
2
+ import pandas as pd
3
+ from flask import Blueprint
4
+ from flask import render_template
5
+
6
+
7
+ def make_bubble_chart_blueprint(repository) -> Blueprint:
8
+ blueprint = Blueprint("bubble_chart", __name__, template_folder="templates")
9
+
10
+ @blueprint.route("/", endpoint="index")
11
+ def bubble_chart():
12
+ activities = repository.meta
13
+
14
+ # Ensure 'activity_id' exists in the activities DataFrame
15
+ if "activity_id" not in activities.columns:
16
+ activities["activity_id"] = (
17
+ activities.index
18
+ ) # Use index as fallback if missing
19
+
20
+ # Prepare the bubble chart data
21
+ bubble_data = activities[
22
+ ["start", "distance_km", "kind", "activity_id"]
23
+ ].rename(
24
+ columns={
25
+ "start": "date",
26
+ "distance_km": "distance",
27
+ "kind": "activity",
28
+ "activity_id": "id",
29
+ }
30
+ )
31
+ bubble_data["date"] = pd.to_datetime(bubble_data["date"]).dt.date
32
+ bubble_data["activity_url"] = bubble_data["id"].apply(
33
+ lambda x: f"/activity/{x}"
34
+ )
35
+
36
+ # Create the bubble chart
37
+ bubble_chart = (
38
+ alt.Chart(bubble_data, title="Distance per Day (Bubble Chart)")
39
+ .mark_circle()
40
+ .encode(
41
+ x=alt.X("date:T", title="Date"),
42
+ y=alt.Y("distance:Q", title="Distance (km)"),
43
+ size=alt.Size(
44
+ "distance:Q", scale=alt.Scale(range=[10, 300]), title="Distance"
45
+ ),
46
+ color=alt.Color("activity:N", title="Activity"),
47
+ tooltip=[
48
+ alt.Tooltip("date:T", title="Date"),
49
+ alt.Tooltip("distance:Q", title="Distance (km)", format=".1f"),
50
+ alt.Tooltip("activity:N", title="Activity"),
51
+ alt.Tooltip("activity_url:N", title="Activity Link"),
52
+ ],
53
+ )
54
+ .properties(height=800, width=1200)
55
+ .interactive()
56
+ .to_json(format="vega")
57
+ )
58
+
59
+ return render_template("bubble_chart/index.html.j2", bubble_chart=bubble_chart)
60
+
61
+ return blueprint
@@ -1,18 +1,18 @@
1
1
  import collections
2
2
  import datetime
3
3
 
4
+ from flask import Blueprint
5
+ from flask import render_template
6
+
4
7
  from ...core.activities import ActivityRepository
5
8
 
6
9
 
7
- class CalendarController:
8
- def __init__(self, repository: ActivityRepository) -> None:
9
- self._repository = repository
10
+ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
11
+ blueprint = Blueprint("calendar", __name__, template_folder="templates")
10
12
 
11
- def render_overview(self) -> dict:
12
- meta = self._repository.meta.copy()
13
- meta["date"] = meta["start"].dt.date
14
- meta["year"] = meta["start"].dt.year
15
- meta["month"] = meta["start"].dt.month
13
+ @blueprint.route("/")
14
+ def index():
15
+ meta = repository.meta
16
16
 
17
17
  monthly_distance = meta.groupby(
18
18
  ["year", "month"],
@@ -33,20 +33,16 @@ class CalendarController:
33
33
  for index, row in yearly_distance.reset_index().iterrows()
34
34
  }
35
35
 
36
- return {
37
- "num_activities": len(self._repository.meta),
36
+ context = {
37
+ "num_activities": len(repository),
38
38
  "monthly_distances": monthly_pivot,
39
39
  "yearly_distances": yearly_distances,
40
40
  }
41
+ return render_template("calendar/index.html.j2", **context)
41
42
 
42
- def render_month(self, year: int, month: int) -> dict:
43
- meta = self._repository.meta.copy()
44
- meta["date"] = meta["start"].dt.date
45
- meta["year"] = meta["start"].dt.year
46
- meta["month"] = meta["start"].dt.month
47
- meta["day"] = meta["start"].dt.day
48
- meta["day_of_week"] = meta["start"].dt.day_of_week
49
- meta["isoweek"] = meta["start"].dt.isocalendar().week
43
+ @blueprint.route("/<int:year>/<int:month>")
44
+ def month(year: int, month: int):
45
+ meta = repository.meta
50
46
 
51
47
  filtered = meta.loc[
52
48
  (meta["year"] == year) & (meta["month"] == month)
@@ -72,9 +68,13 @@ class CalendarController:
72
68
  }
73
69
  )
74
70
 
75
- return {
71
+ context = {
76
72
  "year": year,
77
73
  "month": month,
78
74
  "weeks": weeks,
79
75
  "day_of_month": day_of_month,
80
76
  }
77
+
78
+ return render_template("calendar/month.html.j2", **context)
79
+
80
+ return blueprint
@@ -7,13 +7,13 @@ from flask import Blueprint
7
7
  from flask import render_template
8
8
  from flask import request
9
9
 
10
- from geo_activity_playground.core.activities import ActivityRepository
11
- from geo_activity_playground.core.meta_search import apply_search_query
12
- from geo_activity_playground.webui.search_util import search_query_from_form
13
- from geo_activity_playground.webui.search_util import SearchQueryHistory
10
+ from ...core.activities import ActivityRepository
11
+ from ...core.meta_search import apply_search_query
12
+ from ..search_util import search_query_from_form
13
+ from ..search_util import SearchQueryHistory
14
14
 
15
15
 
16
- def make_eddington_blueprint(
16
+ def register_eddington_blueprint(
17
17
  repository: ActivityRepository, search_query_history: SearchQueryHistory
18
18
  ) -> Blueprint:
19
19
  blueprint = Blueprint("eddington", __name__, template_folder="templates")