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,446 @@
1
+ import json
2
+ import re
3
+ import shutil
4
+ import urllib.parse
5
+ from typing import Any
6
+ from typing import Optional
7
+
8
+ import sqlalchemy
9
+ from flask import Blueprint
10
+ from flask import flash
11
+ from flask import redirect
12
+ from flask import render_template
13
+ from flask import request
14
+ from flask import Response
15
+ from flask import url_for
16
+
17
+ from ...core.config import ConfigAccessor
18
+ from ...core.datamodel import DB
19
+ from ...core.datamodel import Equipment
20
+ from ...core.datamodel import Kind
21
+ from ...core.heart_rate import HeartRateZoneComputer
22
+ from ...core.paths import _activity_enriched_dir
23
+ from ..authenticator import Authenticator
24
+ from ..authenticator import needs_authentication
25
+ from ..flasher import Flasher
26
+ from ..flasher import FlashTypes
27
+
28
+ VEGA_COLOR_SCHEMES_CONTINUOUS = [
29
+ "lightgreyred",
30
+ "lightgreyteal",
31
+ "lightmulti",
32
+ "lightorange",
33
+ "lighttealblue",
34
+ "blues",
35
+ "tealblues",
36
+ "teals",
37
+ "greens",
38
+ "browns",
39
+ "oranges",
40
+ "reds",
41
+ "purples",
42
+ "warmgreys",
43
+ "greys",
44
+ ]
45
+
46
+ MATPLOTLIB_COLOR_SCHEMES_CONTINUOUS = [
47
+ "afmhot",
48
+ "bone",
49
+ "cividis",
50
+ "copper",
51
+ "gist_gray",
52
+ "gist_heat",
53
+ "gnuplot2",
54
+ "gray",
55
+ "Greys_r",
56
+ "hot",
57
+ "inferno",
58
+ "magma",
59
+ "pink",
60
+ "plasma",
61
+ "viridis",
62
+ ]
63
+
64
+
65
+ SHAREPIC_FIELDS = {
66
+ "calories": "Calories",
67
+ "distance_km": "Distance",
68
+ "elapsed_time": "Elapsed time",
69
+ "equipment": "Equipment",
70
+ "kind": "Kind",
71
+ "name": "Name",
72
+ "start": "Date",
73
+ "Steps": "Steps",
74
+ }
75
+
76
+
77
+ def int_or_none(s: str) -> Optional[int]:
78
+ if s:
79
+ try:
80
+ return int(s)
81
+ except ValueError as e:
82
+ flash(f"Cannot parse integer from {s}: {e}", category="danger")
83
+ return None
84
+
85
+
86
+ def make_settings_blueprint(
87
+ config_accessor: ConfigAccessor, authenticator: Authenticator, flasher: Flasher
88
+ ) -> Blueprint:
89
+ strava_login_helper = StravaLoginHelper(config_accessor)
90
+ blueprint = Blueprint("settings", __name__, template_folder="templates")
91
+
92
+ @blueprint.route("/")
93
+ @needs_authentication(authenticator)
94
+ def index():
95
+ return render_template("settings/index.html.j2")
96
+
97
+ @blueprint.route("/admin-password")
98
+ @needs_authentication(authenticator)
99
+ def admin_password() -> Response:
100
+ if request.method == "POST":
101
+ config_accessor().upload_password = request.form["password"]
102
+ config_accessor.save()
103
+ flasher.flash_message("Updated admin password.", FlashTypes.SUCCESS)
104
+ return render_template(
105
+ "settings/admin-password.html.j2",
106
+ password=config_accessor().upload_password,
107
+ )
108
+
109
+ @blueprint.route("/color-schemes", methods=["GET", "POST"])
110
+ @needs_authentication(authenticator)
111
+ def color_schemes():
112
+ if request.method == "POST":
113
+ config_accessor().color_scheme_for_counts = request.form[
114
+ "color_scheme_for_counts"
115
+ ]
116
+ config_accessor().color_scheme_for_kind = request.form[
117
+ "color_scheme_for_kind"
118
+ ]
119
+ config_accessor().color_scheme_for_heatmap = request.form[
120
+ "color_scheme_for_heatmap"
121
+ ]
122
+ config_accessor.save()
123
+ flash("Updated color schemes.", category="success")
124
+
125
+ return render_template(
126
+ "settings/color-schemes.html.j2",
127
+ color_scheme_for_counts=config_accessor().color_scheme_for_counts,
128
+ color_scheme_for_counts_avail=VEGA_COLOR_SCHEMES_CONTINUOUS,
129
+ color_scheme_for_kind=config_accessor().color_scheme_for_kind,
130
+ color_scheme_for_kind_avail=[
131
+ "accent",
132
+ "category10",
133
+ "category20",
134
+ "category20b",
135
+ "category20c",
136
+ "dark2",
137
+ "paired",
138
+ "pastel1",
139
+ "pastel2",
140
+ "set1",
141
+ "set2",
142
+ "set3",
143
+ "tableau10",
144
+ "tableau20",
145
+ ],
146
+ color_scheme_for_heatmap=config_accessor().color_scheme_for_heatmap,
147
+ color_scheme_for_heatmap_avail=MATPLOTLIB_COLOR_SCHEMES_CONTINUOUS,
148
+ )
149
+
150
+ @blueprint.route("/manage-equipments", methods=["GET", "POST"])
151
+ @needs_authentication(authenticator)
152
+ def manage_equipments():
153
+ if request.method == "POST":
154
+ ids = request.form.getlist("id")
155
+ names = request.form.getlist("name")
156
+ offsets = request.form.getlist("offset_km")
157
+ assert len(ids) == len(names) == len(offsets)
158
+ for id, name, offset in zip(ids, names, offsets):
159
+ if id:
160
+ equipment = DB.session.get(Equipment, int(id))
161
+ equipment.name = name
162
+ equipment.offset_km = int(float(offset))
163
+ if not id and name:
164
+ equipment = Equipment(name)
165
+ if offset:
166
+ equipment.offset_km = int(float(offset))
167
+ DB.session.add(equipment)
168
+ flasher.flash_message(
169
+ f"Equipment '{name}' added.", FlashTypes.SUCCESS
170
+ )
171
+ DB.session.commit()
172
+ equipments = DB.session.scalars(
173
+ sqlalchemy.select(Equipment).order_by(Equipment.name)
174
+ ).all()
175
+ return render_template(
176
+ "settings/manage-equipments.html.j2",
177
+ equipments=equipments,
178
+ )
179
+
180
+ @blueprint.route("/manage-kinds", methods=["GET", "POST"])
181
+ @needs_authentication(authenticator)
182
+ def manage_kinds():
183
+ if request.method == "POST":
184
+ print(request.form)
185
+ ids = request.form.getlist("id")
186
+ names = request.form.getlist("name")
187
+ consider_for_achievements = request.form.getlist(
188
+ "consider_for_achievements"
189
+ )
190
+ assert len(ids) == len(names)
191
+ for id, name in zip(ids, names):
192
+ if id:
193
+ kind = DB.session.get(Kind, int(id))
194
+ kind.name = name
195
+ kind.consider_for_achievements = id in consider_for_achievements
196
+ if not id and name:
197
+ kind = Kind(name)
198
+ if consider_for_achievements:
199
+ kind.consider_for_achievements = (
200
+ "new" in consider_for_achievements
201
+ )
202
+ DB.session.add(kind)
203
+ flasher.flash_message(f"Kind '{name}' added.", FlashTypes.SUCCESS)
204
+ DB.session.commit()
205
+ kinds = DB.session.scalars(sqlalchemy.select(Kind).order_by(Kind.name)).all()
206
+ return render_template(
207
+ "settings/manage-kinds.html.j2",
208
+ kinds=kinds,
209
+ )
210
+
211
+ @blueprint.route("/heart-rate", methods=["GET", "POST"])
212
+ @needs_authentication(authenticator)
213
+ def heart_rate():
214
+ if request.method == "POST":
215
+ birth_year = int_or_none(request.form["birth_year"])
216
+ heart_rate_resting = int_or_none(request.form["heart_rate_resting"])
217
+ if heart_rate_resting is None:
218
+ heart_rate_resting = 0
219
+ heart_rate_maximum = int_or_none(request.form["heart_rate_maximum"])
220
+ config_accessor().birth_year = birth_year
221
+ config_accessor().heart_rate_resting = heart_rate_resting or 0
222
+ config_accessor().heart_rate_maximum = heart_rate_maximum
223
+ config_accessor.save()
224
+ flash("Updated heart rate data.", category="success")
225
+
226
+ context: dict[str, Any] = {
227
+ "birth_year": config_accessor().birth_year,
228
+ "heart_rate_resting": config_accessor().heart_rate_resting,
229
+ "heart_rate_maximum": config_accessor().heart_rate_maximum,
230
+ }
231
+
232
+ heart_rate_computer = HeartRateZoneComputer(config_accessor())
233
+ try:
234
+ context["zone_boundaries"] = heart_rate_computer.zone_boundaries()
235
+ except RuntimeError as e:
236
+ pass
237
+ return render_template("settings/heart-rate.html.j2", **context)
238
+
239
+ @blueprint.route("/metadata-extraction", methods=["GET", "POST"])
240
+ @needs_authentication(authenticator)
241
+ def metadata_extraction():
242
+ if request.method == "POST":
243
+ metadata_extraction_regexes = request.form.getlist("regex")
244
+ new_metadata_extraction_regexes = []
245
+ for regex in metadata_extraction_regexes:
246
+ try:
247
+ re.compile(regex)
248
+ except re.error as e:
249
+ flash(
250
+ f"Cannot parse regex {regex} due to error: {e}",
251
+ category="danger",
252
+ )
253
+ else:
254
+ new_metadata_extraction_regexes.append(regex)
255
+
256
+ config_accessor().metadata_extraction_regexes = (
257
+ new_metadata_extraction_regexes
258
+ )
259
+ config_accessor.save()
260
+ flash("Updated metadata extraction settings.", category="success")
261
+ context = {
262
+ "metadata_extraction_regexes": config_accessor().metadata_extraction_regexes,
263
+ }
264
+ return render_template("settings/metadata-extraction.html.j2", **context)
265
+
266
+ @blueprint.route("/privacy-zones", methods=["GET", "POST"])
267
+ @needs_authentication(authenticator)
268
+ def privacy_zones():
269
+ if request.method == "POST":
270
+ zone_names = request.form.getlist("zone_name")
271
+ zone_geojsons = request.form.getlist("zone_geojson")
272
+ strava_login_helper.save_privacy_zones(zone_names, zone_geojsons)
273
+
274
+ assert len(zone_names) == len(zone_geojsons)
275
+ new_zone_config = {}
276
+
277
+ for zone_name, zone_geojson_str in zip(zone_names, zone_geojsons):
278
+ if not zone_name or not zone_geojson_str:
279
+ continue
280
+
281
+ try:
282
+ zone_geojson = json.loads(zone_geojson_str)
283
+ except json.decoder.JSONDecodeError as e:
284
+ flash(
285
+ f"Could not parse GeoJSON for {zone_name} due to the following error: {e}"
286
+ )
287
+ continue
288
+
289
+ if not zone_geojson["type"] == "FeatureCollection":
290
+ flash(
291
+ f"Pasted GeoJSON for {zone_name} must be of type 'FeatureCollection'.",
292
+ category="danger",
293
+ )
294
+ continue
295
+
296
+ features = zone_geojson["features"]
297
+
298
+ if not len(features) == 1:
299
+ flash(
300
+ f"Pasted GeoJSON for {zone_name} must contain exactly one feature. You cannot have multiple shapes for one privacy zone",
301
+ category="danger",
302
+ )
303
+ continue
304
+
305
+ feature = features[0]
306
+ geometry = feature["geometry"]
307
+
308
+ if not geometry["type"] == "Polygon":
309
+ flash(
310
+ f"Geometry for {zone_name} is not a polygon. You need to create a polygon (or circle or rectangle).",
311
+ category="danger",
312
+ )
313
+ continue
314
+
315
+ coordinates = geometry["coordinates"]
316
+
317
+ if not len(coordinates) == 1:
318
+ flash(
319
+ f"Polygon for {zone_name} consists of multiple polygons. Please supply a simple one.",
320
+ category="danger",
321
+ )
322
+ continue
323
+
324
+ points = coordinates[0]
325
+
326
+ new_zone_config[zone_name] = points
327
+
328
+ config_accessor().privacy_zones = new_zone_config
329
+ config_accessor.save()
330
+ flash("Updated privacy zones.", category="success")
331
+
332
+ context = {
333
+ "privacy_zones": {
334
+ name: _wrap_coordinates(coordinates)
335
+ for name, coordinates in config_accessor().privacy_zones.items()
336
+ }
337
+ }
338
+ return render_template("settings/privacy-zones.html.j2", **context)
339
+
340
+ @blueprint.route("/segmentation", methods=["GET", "POST"])
341
+ @needs_authentication(authenticator)
342
+ def segmentation():
343
+ if request.method == "POST":
344
+ threshold = int(request.form.get("threshold", 0))
345
+ config_accessor().time_diff_threshold_seconds = threshold
346
+ config_accessor.save()
347
+ flash(f"Threshold set to {threshold}.", category="success")
348
+ shutil.rmtree(_activity_enriched_dir)
349
+ return redirect(url_for("upload.reload"))
350
+ return render_template(
351
+ "settings/segmentation.html.j2",
352
+ threshold=config_accessor().time_diff_threshold_seconds,
353
+ )
354
+
355
+ @blueprint.route("/sharepic", methods=["GET", "POST"])
356
+ @needs_authentication(authenticator)
357
+ def sharepic():
358
+ if request.method == "POST":
359
+ names = request.form.getlist("name")
360
+ config_accessor().sharepic_suppressed_fields = list(
361
+ set(SHAREPIC_FIELDS) - set(names)
362
+ )
363
+ config_accessor.save()
364
+ flash("Updated sharepic preferences.", category="success")
365
+ return render_template(
366
+ "settings/sharepic.html.j2",
367
+ names=[
368
+ (
369
+ name,
370
+ label,
371
+ name not in config_accessor().sharepic_suppressed_fields,
372
+ )
373
+ for name, label in SHAREPIC_FIELDS.items()
374
+ ],
375
+ )
376
+
377
+ @blueprint.route("/strava", methods=["GET", "POST"])
378
+ @needs_authentication(authenticator)
379
+ def strava():
380
+ if request.method == "POST":
381
+ strava_client_id = request.form["strava_client_id"]
382
+ strava_client_secret = request.form["strava_client_secret"]
383
+ url = strava_login_helper.save_strava(
384
+ strava_client_id, strava_client_secret
385
+ )
386
+ return redirect(url)
387
+ return render_template(
388
+ "settings/strava.html.j2", **strava_login_helper.render_strava()
389
+ )
390
+
391
+ @blueprint.route("/strava-callback")
392
+ @needs_authentication(authenticator)
393
+ def strava_callback():
394
+ code = request.args.get("code", type=str)
395
+ strava_login_helper.save_strava_code(code)
396
+ return redirect(url_for(".strava"))
397
+
398
+ return blueprint
399
+
400
+
401
+ def _wrap_coordinates(coordinates: list[list[float]]) -> dict:
402
+ return {
403
+ "type": "FeatureCollection",
404
+ "features": [
405
+ {
406
+ "type": "Feature",
407
+ "properties": {},
408
+ "geometry": {"coordinates": [coordinates], "type": "Polygon"},
409
+ }
410
+ ],
411
+ }
412
+
413
+
414
+ class StravaLoginHelper:
415
+ def __init__(self, config_accessor: ConfigAccessor) -> None:
416
+ self._config_accessor = config_accessor
417
+
418
+ def render_strava(self) -> dict:
419
+ return {
420
+ "strava_client_id": self._config_accessor().strava_client_id,
421
+ "strava_client_secret": self._config_accessor().strava_client_secret,
422
+ "strava_client_code": self._config_accessor().strava_client_code,
423
+ }
424
+
425
+ def save_strava(self, client_id: str, client_secret: str) -> str:
426
+ self._strava_client_id = client_id
427
+ self._strava_client_secret = client_secret
428
+
429
+ payload = {
430
+ "client_id": client_id,
431
+ "redirect_uri": url_for(".strava_callback", _external=True),
432
+ "response_type": "code",
433
+ "scope": "activity:read_all",
434
+ }
435
+
436
+ arg_string = "&".join(
437
+ f"{key}={urllib.parse.quote(value)}" for key, value in payload.items()
438
+ )
439
+ return f"https://www.strava.com/oauth/authorize?{arg_string}"
440
+
441
+ def save_strava_code(self, code: str) -> None:
442
+ self._config_accessor().strava_client_id = int(self._strava_client_id)
443
+ self._config_accessor().strava_client_secret = self._strava_client_secret
444
+ self._config_accessor().strava_client_code = code
445
+ self._config_accessor.save()
446
+ flash("Connected to Strava API", category="success")
@@ -1,16 +1,20 @@
1
1
  import geojson
2
+ import sqlalchemy
2
3
  from flask import Blueprint
3
4
  from flask import redirect
4
5
  from flask import render_template
6
+ from flask import request
5
7
  from flask import Response
6
8
  from flask import url_for
7
9
 
8
- from geo_activity_playground.explorer.grid_file import make_explorer_rectangle
9
- from geo_activity_playground.explorer.grid_file import make_explorer_tile
10
- from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
11
- from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
12
- from geo_activity_playground.explorer.grid_file import make_grid_points
13
- from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
10
+ from ...core.datamodel import DB
11
+ from ...core.datamodel import SquarePlannerBookmark
12
+ from ...explorer.grid_file import make_explorer_rectangle
13
+ from ...explorer.grid_file import make_explorer_tile
14
+ from ...explorer.grid_file import make_grid_file_geojson
15
+ from ...explorer.grid_file import make_grid_file_gpx
16
+ from ...explorer.grid_file import make_grid_points
17
+ from ...explorer.tile_visits import TileVisitAccessor
14
18
 
15
19
 
16
20
  def make_square_planner_blueprint(tile_visit_accessor: TileVisitAccessor) -> Blueprint:
@@ -72,6 +76,9 @@ def make_square_planner_blueprint(tile_visit_accessor: TileVisitAccessor) -> Blu
72
76
  square_x=x,
73
77
  square_y=y,
74
78
  square_size=size,
79
+ bookmarks=DB.session.scalars(
80
+ sqlalchemy.select(SquarePlannerBookmark)
81
+ ).all(),
75
82
  )
76
83
 
77
84
  @blueprint.route("/<int:zoom>/<int:x>/<int:y>/<int:size>/missing.<suffix>")
@@ -99,6 +106,24 @@ def make_square_planner_blueprint(tile_visit_accessor: TileVisitAccessor) -> Blu
99
106
  headers={"Content-disposition": "attachment"},
100
107
  )
101
108
 
109
+ @blueprint.route(
110
+ "/save-bookmark/<int:zoom>/<int:x>/<int:y>/<int:size>", methods=["POST"]
111
+ )
112
+ def save_bookmark(zoom: int, x: int, y: int, size: int):
113
+ bookmark = SquarePlannerBookmark(
114
+ zoom=zoom, x=x, y=y, size=size, name=request.form["name"]
115
+ )
116
+ DB.session.add(bookmark)
117
+ DB.session.commit()
118
+ return redirect(request.referrer)
119
+
120
+ @blueprint.route("/delete-bookmark/<int:id>")
121
+ def delete_bookmark(id: int):
122
+ bookmark = DB.session.get(SquarePlannerBookmark, id)
123
+ DB.session.delete(bookmark)
124
+ DB.session.commit()
125
+ return redirect(request.referrer)
126
+
102
127
  return blueprint
103
128
 
104
129
 
@@ -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.activities import make_geojson_from_time_series
12
- from geo_activity_playground.core.config import Config
13
- from geo_activity_playground.core.meta_search import apply_search_query
14
- from geo_activity_playground.webui.plot_util import make_kind_scale
15
- from geo_activity_playground.webui.search_util import search_query_from_form
16
- from geo_activity_playground.webui.search_util import SearchQueryHistory
10
+ from ...core.activities import ActivityRepository
11
+ from ...core.activities import make_geojson_from_time_series
12
+ from ...core.config import Config
13
+ from ...core.meta_search import apply_search_query
14
+ from ..plot_util import make_kind_scale
15
+ from ..search_util import search_query_from_form
16
+ from ..search_util import SearchQueryHistory
17
17
 
18
18
 
19
19
  def make_summary_blueprint(
@@ -30,8 +30,7 @@ def make_summary_blueprint(
30
30
  activities = apply_search_query(repository.meta, query)
31
31
 
32
32
  kind_scale = make_kind_scale(repository.meta, config)
33
- df = embellished_activities(activities)
34
- # df = df.loc[df["consider_for_achievements"]]
33
+ df = activities
35
34
 
36
35
  year_kind_total = (
37
36
  df[["year", "kind", "distance_km", "hours"]]
@@ -40,6 +39,8 @@ def make_summary_blueprint(
40
39
  .reset_index()
41
40
  )
42
41
 
42
+ nominations = nominate_activities(df)
43
+
43
44
  return render_template(
44
45
  "summary/index.html.j2",
45
46
  plot_distance_heatmaps=plot_distance_heatmaps(df, config),
@@ -58,7 +59,7 @@ def make_summary_blueprint(
58
59
  repository.get_time_series(activity_id)
59
60
  ),
60
61
  )
61
- for activity_id, reasons in nominate_activities(df).items()
62
+ for activity_id, reasons in nominations.items()
62
63
  ],
63
64
  query=query.to_jinja(),
64
65
  )
@@ -100,19 +101,6 @@ def _nominate_activities_inner(
100
101
  nominations[i].append(f"{title}{title_suffix}: {format_applied}")
101
102
 
102
103
 
103
- def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
104
- df = meta.loc[~pd.isna(meta["start"])].copy()
105
- df["year"] = [start.year for start in df["start"]]
106
- df["month"] = [start.month for start in df["start"]]
107
- df["day"] = [start.day for start in df["start"]]
108
- df["week"] = [start.isocalendar().week for start in df["start"]]
109
- df["iso_year"] = [start.isocalendar().year for start in df["start"]]
110
- df["hours"] = [
111
- elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
112
- ]
113
- return df
114
-
115
-
116
104
  def plot_distance_heatmaps(meta: pd.DataFrame, config: Config) -> dict[int, str]:
117
105
  return {
118
106
  year: alt.Chart(
@@ -0,0 +1,27 @@
1
+ import io
2
+
3
+ import matplotlib.pyplot as pl
4
+ import numpy as np
5
+ from flask import Blueprint
6
+ from flask import Response
7
+
8
+ from ...core.raster_map import ImageTransform
9
+ from ...core.raster_map import TileGetter
10
+
11
+
12
+ def make_tile_blueprint(
13
+ image_transforms: dict[str, ImageTransform],
14
+ tile_getter: TileGetter,
15
+ ) -> Blueprint:
16
+
17
+ blueprint = Blueprint("tile", __name__, template_folder="templates")
18
+
19
+ @blueprint.route("/<scheme>/<int:z>/<int:x>/<int:y>.png")
20
+ def tile(scheme: str, z: int, x: int, y: int) -> Response:
21
+ map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
22
+ transformed_tile = image_transforms[scheme].transform_image(map_tile)
23
+ f = io.BytesIO()
24
+ pl.imsave(f, transformed_tile, format="png")
25
+ return Response(bytes(f.getbuffer()), mimetype="image/png")
26
+
27
+ return blueprint
@@ -8,21 +8,18 @@ from flask import render_template
8
8
  from flask import request
9
9
  from flask import url_for
10
10
 
11
- from ..core.activities import ActivityRepository
12
- from ..core.activities import build_activity_meta
13
- from ..explorer.tile_visits import compute_tile_evolution
14
- from ..explorer.tile_visits import compute_tile_visits_new
15
- from ..explorer.tile_visits import TileVisitAccessor
16
- from geo_activity_playground.core.config import Config
17
- from geo_activity_playground.core.enrichment import enrich_activities
18
- from geo_activity_playground.importers.directory import get_file_hash
19
- from geo_activity_playground.importers.directory import import_from_directory
20
- from geo_activity_playground.importers.strava_api import import_from_strava_api
21
- from geo_activity_playground.importers.strava_checkout import (
22
- import_from_strava_checkout,
23
- )
24
- from geo_activity_playground.webui.authenticator import Authenticator
25
- from geo_activity_playground.webui.authenticator import needs_authentication
11
+ from ...core.activities import ActivityRepository
12
+ from ...core.config import Config
13
+ from ...core.enrichment import populate_database_from_extracted
14
+ from ...explorer.tile_visits import compute_tile_evolution
15
+ from ...explorer.tile_visits import compute_tile_visits_new
16
+ from ...explorer.tile_visits import TileVisitAccessor
17
+ from ...importers.directory import get_file_hash
18
+ from ...importers.directory import import_from_directory
19
+ from ...importers.strava_api import import_from_strava_api
20
+ from ...importers.strava_checkout import import_from_strava_checkout
21
+ from ..authenticator import Authenticator
22
+ from ..authenticator import needs_authentication
26
23
 
27
24
 
28
25
  def make_upload_blueprint(
@@ -114,9 +111,7 @@ def scan_for_activities(
114
111
  if config.strava_client_code and not skip_strava:
115
112
  import_from_strava_api(config)
116
113
 
117
- enrich_activities(config)
118
- build_activity_meta()
119
- repository.reload()
114
+ populate_database_from_extracted(config)
120
115
 
121
116
  if len(repository) > 0:
122
117
  compute_tile_visits_new(repository, tile_visit_accessor)
@@ -0,0 +1,26 @@
1
+ import abc
2
+ from enum import Enum
3
+
4
+ import flask
5
+
6
+
7
+ class FlashTypes(Enum):
8
+ PRIMARY = "primary"
9
+ SECONDARY = "secondary"
10
+ SUCCESS = "success"
11
+ DANGER = "danger"
12
+ WARNING = "warning"
13
+ INFO = "info"
14
+ LIGHT = "light"
15
+ DARK = "dark"
16
+
17
+
18
+ class Flasher(abc.ABC):
19
+ @abc.abstractmethod
20
+ def flash_message(self, message: str, type: FlashTypes):
21
+ pass
22
+
23
+
24
+ class FlaskFlasher(Flasher):
25
+ def flash_message(self, message, type):
26
+ flask.flash(message, category=type.value)
@@ -1,7 +1,7 @@
1
1
  import altair as alt
2
2
  import pandas as pd
3
3
 
4
- from geo_activity_playground.core.config import Config
4
+ from ..core.config import Config
5
5
 
6
6
 
7
7
  def make_kind_scale(meta: pd.DataFrame, config: Config) -> alt.Scale: