geo-activity-playground 0.38.1__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} +9 -5
  39. geo_activity_playground/webui/blueprints/entry_views.py +68 -0
  40. geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +58 -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} +21 -26
  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 +60 -36
  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/metadata-extraction.html.j2 +1 -1
  72. geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
  73. geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
  74. geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
  75. {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/METADATA +5 -1
  76. geo_activity_playground-0.39.0.dist-info/RECORD +133 -0
  77. geo_activity_playground/__init__.py +0 -0
  78. geo_activity_playground/core/__init__.py +0 -0
  79. geo_activity_playground/explorer/__init__.py +0 -0
  80. geo_activity_playground/importers/__init__.py +0 -0
  81. geo_activity_playground/webui/__init__.py +0 -0
  82. geo_activity_playground/webui/activity/__init__.py +0 -0
  83. geo_activity_playground/webui/activity/blueprint.py +0 -109
  84. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
  85. geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
  86. geo_activity_playground/webui/calendar/__init__.py +0 -0
  87. geo_activity_playground/webui/calendar/blueprint.py +0 -23
  88. geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
  89. geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
  90. geo_activity_playground/webui/entry_controller.py +0 -63
  91. geo_activity_playground/webui/explorer/__init__.py +0 -0
  92. geo_activity_playground/webui/explorer/blueprint.py +0 -62
  93. geo_activity_playground/webui/heatmap/__init__.py +0 -0
  94. geo_activity_playground/webui/heatmap/blueprint.py +0 -51
  95. geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
  96. geo_activity_playground/webui/settings/blueprint.py +0 -262
  97. geo_activity_playground/webui/settings/controller.py +0 -272
  98. geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
  99. geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
  100. geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
  101. geo_activity_playground/webui/tile_blueprint.py +0 -42
  102. geo_activity_playground-0.38.1.dist-info/RECORD +0 -129
  103. /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
  104. /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
  105. /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
  106. /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
  107. /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
  108. /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.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.1.dist-info → geo_activity_playground-0.39.0.dist-info}/LICENSE +0 -0
  112. {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
  113. {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/entry_points.txt +0 -0
@@ -1,262 +0,0 @@
1
- import shutil
2
- from typing import Optional
3
-
4
- from flask import Blueprint
5
- from flask import flash
6
- from flask import redirect
7
- from flask import render_template
8
- from flask import request
9
- from flask import url_for
10
-
11
- from geo_activity_playground.core.config import ConfigAccessor
12
- from geo_activity_playground.core.paths import _activity_enriched_dir
13
- from geo_activity_playground.webui.authenticator import Authenticator
14
- from geo_activity_playground.webui.authenticator import needs_authentication
15
- from geo_activity_playground.webui.settings.controller import SettingsController
16
-
17
-
18
- VEGA_COLOR_SCHEMES_CONTINUOUS = [
19
- "lightgreyred",
20
- "lightgreyteal",
21
- "lightmulti",
22
- "lightorange",
23
- "lighttealblue",
24
- "blues",
25
- "tealblues",
26
- "teals",
27
- "greens",
28
- "browns",
29
- "oranges",
30
- "reds",
31
- "purples",
32
- "warmgreys",
33
- "greys",
34
- ]
35
-
36
- MATPLOTLIB_COLOR_SCHEMES_CONTINUOUS = [
37
- "afmhot",
38
- "bone",
39
- "cividis",
40
- "copper",
41
- "gist_gray",
42
- "gist_heat",
43
- "gnuplot2",
44
- "gray",
45
- "Greys_r",
46
- "hot",
47
- "inferno",
48
- "magma",
49
- "pink",
50
- "plasma",
51
- "viridis",
52
- ]
53
-
54
-
55
- def int_or_none(s: str) -> Optional[int]:
56
- if s:
57
- try:
58
- return int(s)
59
- except ValueError as e:
60
- flash(f"Cannot parse integer from {s}: {e}", category="danger")
61
- return None
62
-
63
-
64
- def make_settings_blueprint(
65
- config_accessor: ConfigAccessor, authenticator: Authenticator
66
- ) -> Blueprint:
67
- settings_controller = SettingsController(config_accessor)
68
- blueprint = Blueprint("settings", __name__, template_folder="templates")
69
-
70
- @blueprint.route("/")
71
- @needs_authentication(authenticator)
72
- def index():
73
- return render_template("settings/index.html.j2")
74
-
75
- @blueprint.route("/admin-password", methods=["GET", "POST"])
76
- @needs_authentication(authenticator)
77
- def admin_password():
78
- if request.method == "POST":
79
- settings_controller.save_admin_password(request.form["password"])
80
- return render_template(
81
- "settings/admin-password.html.j2",
82
- **settings_controller.render_admin_password(),
83
- )
84
-
85
- @blueprint.route("/color-schemes", methods=["GET", "POST"])
86
- @needs_authentication(authenticator)
87
- def color_schemes():
88
- if request.method == "POST":
89
- config_accessor().color_scheme_for_counts = request.form[
90
- "color_scheme_for_counts"
91
- ]
92
- config_accessor().color_scheme_for_kind = request.form[
93
- "color_scheme_for_kind"
94
- ]
95
- config_accessor().color_scheme_for_heatmap = request.form[
96
- "color_scheme_for_heatmap"
97
- ]
98
- config_accessor.save()
99
- flash("Updated color schemes.", category="success")
100
-
101
- return render_template(
102
- "settings/color-schemes.html.j2",
103
- color_scheme_for_counts=config_accessor().color_scheme_for_counts,
104
- color_scheme_for_counts_avail=VEGA_COLOR_SCHEMES_CONTINUOUS,
105
- color_scheme_for_kind=config_accessor().color_scheme_for_kind,
106
- color_scheme_for_kind_avail=[
107
- "accent",
108
- "category10",
109
- "category20",
110
- "category20b",
111
- "category20c",
112
- "dark2",
113
- "paired",
114
- "pastel1",
115
- "pastel2",
116
- "set1",
117
- "set2",
118
- "set3",
119
- "tableau10",
120
- "tableau20",
121
- ],
122
- color_scheme_for_heatmap=config_accessor().color_scheme_for_heatmap,
123
- color_scheme_for_heatmap_avail=MATPLOTLIB_COLOR_SCHEMES_CONTINUOUS,
124
- )
125
-
126
- @blueprint.route("/equipment-offsets", methods=["GET", "POST"])
127
- @needs_authentication(authenticator)
128
- def equipment_offsets():
129
- if request.method == "POST":
130
- equipments = request.form.getlist("equipment")
131
- offsets = request.form.getlist("offset")
132
- settings_controller.save_equipment_offsets(equipments, offsets)
133
- return render_template(
134
- "settings/equipment-offsets.html.j2",
135
- **settings_controller.render_equipment_offsets(),
136
- )
137
-
138
- @blueprint.route("/heart-rate", methods=["GET", "POST"])
139
- @needs_authentication(authenticator)
140
- def heart_rate():
141
- if request.method == "POST":
142
- birth_year = int_or_none(request.form["birth_year"])
143
- heart_rate_resting = int_or_none(request.form["heart_rate_resting"])
144
- if heart_rate_resting is None:
145
- heart_rate_resting = 0
146
- heart_rate_maximum = int_or_none(request.form["heart_rate_maximum"])
147
- settings_controller.save_heart_rate(
148
- birth_year, heart_rate_resting, heart_rate_maximum
149
- )
150
- return render_template(
151
- "settings/heart-rate.html.j2", **settings_controller.render_heart_rate()
152
- )
153
-
154
- @blueprint.route("/kind-renames", methods=["GET", "POST"])
155
- @needs_authentication(authenticator)
156
- def kind_renames():
157
- if request.method == "POST":
158
- rules_str = request.form["rules_str"]
159
- rules = {}
160
- try:
161
- for line in rules_str.strip().split("\n"):
162
- first, second = line.split(" => ")
163
- rules[first.strip()] = second.strip()
164
- config_accessor().kind_renames = rules
165
- config_accessor.save()
166
- flash(f"Kind renames updated.", category="success")
167
- shutil.rmtree(_activity_enriched_dir)
168
- return redirect(url_for("upload.reload"))
169
- except ValueError as e:
170
- flash(f"Cannot parse this. Please try again.", category="danger")
171
- else:
172
- rules_str = "\n".join(
173
- f"{key} => {value}"
174
- for key, value in config_accessor().kind_renames.items()
175
- )
176
- return render_template(
177
- "settings/kind-renames.html.j2",
178
- rules_str=rules_str,
179
- )
180
-
181
- @blueprint.route("/kinds-without-achievements", methods=["GET", "POST"])
182
- @needs_authentication(authenticator)
183
- def kinds_without_achievements():
184
- if request.method == "POST":
185
- kinds = request.form.getlist("kind")
186
- settings_controller.save_kinds_without_achievements(kinds)
187
- return render_template(
188
- "settings/kinds-without-achievements.html.j2",
189
- **settings_controller.render_kinds_without_achievements(),
190
- )
191
-
192
- @blueprint.route("/metadata-extraction", methods=["GET", "POST"])
193
- @needs_authentication(authenticator)
194
- def metadata_extraction():
195
- if request.method == "POST":
196
- regexes = request.form.getlist("regex")
197
- settings_controller.save_metadata_extraction(regexes)
198
- return render_template(
199
- "settings/metadata-extraction.html.j2",
200
- **settings_controller.render_metadata_extraction(),
201
- )
202
-
203
- @blueprint.route("/privacy-zones", methods=["GET", "POST"])
204
- @needs_authentication(authenticator)
205
- def privacy_zones():
206
- if request.method == "POST":
207
- zone_names = request.form.getlist("zone_name")
208
- zone_geojsons = request.form.getlist("zone_geojson")
209
- settings_controller.save_privacy_zones(zone_names, zone_geojsons)
210
- return render_template(
211
- "settings/privacy-zones.html.j2",
212
- **settings_controller.render_privacy_zones(),
213
- )
214
-
215
- @blueprint.route("/segmentation", methods=["GET", "POST"])
216
- @needs_authentication(authenticator)
217
- def segmentation():
218
- if request.method == "POST":
219
- threshold = int(request.form.get("threshold", 0))
220
- config_accessor().time_diff_threshold_seconds = threshold
221
- config_accessor.save()
222
- flash(f"Threshold set to {threshold}.", category="success")
223
- shutil.rmtree(_activity_enriched_dir)
224
- return redirect(url_for("upload.reload"))
225
- return render_template(
226
- "settings/segmentation.html.j2",
227
- threshold=config_accessor().time_diff_threshold_seconds,
228
- )
229
-
230
- @blueprint.route("/sharepic", methods=["GET", "POST"])
231
- @needs_authentication(authenticator)
232
- def sharepic():
233
- if request.method == "POST":
234
- names = request.form.getlist("name")
235
- settings_controller.save_sharepic(names)
236
- return render_template(
237
- "settings/sharepic.html.j2",
238
- **settings_controller.render_sharepic(),
239
- )
240
-
241
- @blueprint.route("/strava", methods=["GET", "POST"])
242
- @needs_authentication(authenticator)
243
- def strava():
244
- if request.method == "POST":
245
- strava_client_id = request.form["strava_client_id"]
246
- strava_client_secret = request.form["strava_client_secret"]
247
- url = settings_controller.save_strava(
248
- strava_client_id, strava_client_secret
249
- )
250
- return redirect(url)
251
- return render_template(
252
- "settings/strava.html.j2", **settings_controller.render_strava()
253
- )
254
-
255
- @blueprint.route("/strava-callback")
256
- @needs_authentication(authenticator)
257
- def strava_callback():
258
- code = request.args.get("code", type=str)
259
- settings_controller.save_strava_code(code)
260
- return redirect(url_for(".strava"))
261
-
262
- return blueprint
@@ -1,272 +0,0 @@
1
- import json
2
- import re
3
- import urllib.parse
4
- from typing import Any
5
- from typing import Optional
6
-
7
- from flask import flash
8
- from flask import url_for
9
-
10
- from geo_activity_playground.core.config import ConfigAccessor
11
- from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
12
-
13
-
14
- SHAREPIC_FIELDS = {
15
- "calories": "Calories",
16
- "distance_km": "Distance",
17
- "elapsed_time": "Elapsed time",
18
- "equipment": "Equipment",
19
- "kind": "Kind",
20
- "name": "Name",
21
- "start": "Date",
22
- "Steps": "Steps",
23
- }
24
-
25
-
26
- class SettingsController:
27
- def __init__(self, config_accessor: ConfigAccessor) -> None:
28
- self._config_accessor = config_accessor
29
-
30
- def render_admin_password(self) -> dict:
31
- return {
32
- "password": self._config_accessor().upload_password,
33
- }
34
-
35
- def save_admin_password(self, password: str) -> None:
36
- self._config_accessor().upload_password = password
37
- self._config_accessor.save()
38
- flash("Updated admin password.", category="success")
39
-
40
- def render_equipment_offsets(self) -> dict:
41
- return {
42
- "equipment_offsets": self._config_accessor().equipment_offsets,
43
- }
44
-
45
- def save_equipment_offsets(
46
- self,
47
- equipments: list[str],
48
- offsets: list[str],
49
- ) -> None:
50
- assert len(equipments) == len(offsets)
51
- new_equipment_offsets = {}
52
- for equipment, offset_str in zip(equipments, offsets):
53
- if not equipment or not offset_str:
54
- continue
55
-
56
- try:
57
- offset = float(offset_str)
58
- except ValueError as e:
59
- flash(
60
- f"Cannot parse number {offset_str} for {equipment}: {e}",
61
- category="danger",
62
- )
63
- continue
64
-
65
- if not offset:
66
- continue
67
-
68
- new_equipment_offsets[equipment] = offset
69
- self._config_accessor().equipment_offsets = new_equipment_offsets
70
- self._config_accessor.save()
71
- flash("Updated equipment offsets.", category="success")
72
-
73
- def render_heart_rate(self) -> dict:
74
- result: dict[str, Any] = {
75
- "birth_year": self._config_accessor().birth_year,
76
- "heart_rate_resting": self._config_accessor().heart_rate_resting,
77
- "heart_rate_maximum": self._config_accessor().heart_rate_maximum,
78
- }
79
-
80
- self._heart_rate_computer = HeartRateZoneComputer(self._config_accessor())
81
- try:
82
- result["zone_boundaries"] = self._heart_rate_computer.zone_boundaries()
83
- except RuntimeError as e:
84
- pass
85
- return result
86
-
87
- def save_heart_rate(
88
- self,
89
- birth_year: Optional[int],
90
- heart_rate_resting: Optional[int],
91
- heart_rate_maximum: Optional[int],
92
- ) -> None:
93
- self._config_accessor().birth_year = birth_year
94
- self._config_accessor().heart_rate_resting = heart_rate_resting or 0
95
- self._config_accessor().heart_rate_maximum = heart_rate_maximum
96
- self._config_accessor.save()
97
- flash("Updated heart rate data.", category="success")
98
-
99
- def render_kinds_without_achievements(self) -> dict:
100
- return {
101
- "kinds_without_achievements": self._config_accessor().kinds_without_achievements,
102
- }
103
-
104
- def save_kinds_without_achievements(
105
- self,
106
- kinds: list[str],
107
- ) -> None:
108
- new_kinds = [kind.strip() for kind in kinds if kind.strip()]
109
- new_kinds.sort()
110
-
111
- self._config_accessor().kinds_without_achievements = new_kinds
112
- self._config_accessor.save()
113
- flash("Updated kinds without achievements.", category="success")
114
-
115
- def render_metadata_extraction(self) -> dict:
116
- return {
117
- "metadata_extraction_regexes": self._config_accessor().metadata_extraction_regexes,
118
- }
119
-
120
- def save_metadata_extraction(
121
- self,
122
- metadata_extraction_regexes: list[str],
123
- ) -> None:
124
- new_metadata_extraction_regexes = []
125
- for regex in metadata_extraction_regexes:
126
- try:
127
- re.compile(regex)
128
- except re.error as e:
129
- flash(
130
- f"Cannot parse regex {regex} due to error: {e}", category="danger"
131
- )
132
- else:
133
- new_metadata_extraction_regexes.append(regex)
134
-
135
- self._config_accessor().metadata_extraction_regexes = (
136
- new_metadata_extraction_regexes
137
- )
138
- self._config_accessor.save()
139
- flash("Updated metadata extraction settings.", category="success")
140
-
141
- def render_privacy_zones(self) -> dict:
142
- return {
143
- "privacy_zones": {
144
- name: _wrap_coordinates(coordinates)
145
- for name, coordinates in self._config_accessor().privacy_zones.items()
146
- }
147
- }
148
-
149
- def save_privacy_zones(
150
- self, zone_names: list[str], zone_geojsons: list[str]
151
- ) -> None:
152
- assert len(zone_names) == len(zone_geojsons)
153
- new_zone_config = {}
154
-
155
- for zone_name, zone_geojson_str in zip(zone_names, zone_geojsons):
156
- if not zone_name or not zone_geojson_str:
157
- continue
158
-
159
- try:
160
- zone_geojson = json.loads(zone_geojson_str)
161
- except json.decoder.JSONDecodeError as e:
162
- flash(
163
- f"Could not parse GeoJSON for {zone_name} due to the following error: {e}"
164
- )
165
- continue
166
-
167
- if not zone_geojson["type"] == "FeatureCollection":
168
- flash(
169
- f"Pasted GeoJSON for {zone_name} must be of type 'FeatureCollection'.",
170
- category="danger",
171
- )
172
- continue
173
-
174
- features = zone_geojson["features"]
175
-
176
- if not len(features) == 1:
177
- flash(
178
- f"Pasted GeoJSON for {zone_name} must contain exactly one feature. You cannot have multiple shapes for one privacy zone",
179
- category="danger",
180
- )
181
- continue
182
-
183
- feature = features[0]
184
- geometry = feature["geometry"]
185
-
186
- if not geometry["type"] == "Polygon":
187
- flash(
188
- f"Geometry for {zone_name} is not a polygon. You need to create a polygon (or circle or rectangle).",
189
- category="danger",
190
- )
191
- continue
192
-
193
- coordinates = geometry["coordinates"]
194
-
195
- if not len(coordinates) == 1:
196
- flash(
197
- f"Polygon for {zone_name} consists of multiple polygons. Please supply a simple one.",
198
- category="danger",
199
- )
200
- continue
201
-
202
- points = coordinates[0]
203
-
204
- new_zone_config[zone_name] = points
205
-
206
- self._config_accessor().privacy_zones = new_zone_config
207
- self._config_accessor.save()
208
- flash("Updated privacy zones.", category="success")
209
-
210
- def render_sharepic(self) -> dict:
211
-
212
- return {
213
- "names": [
214
- (
215
- name,
216
- label,
217
- name not in self._config_accessor().sharepic_suppressed_fields,
218
- )
219
- for name, label in SHAREPIC_FIELDS.items()
220
- ]
221
- }
222
-
223
- def save_sharepic(self, names: list[str]) -> None:
224
- self._config_accessor().sharepic_suppressed_fields = list(
225
- set(SHAREPIC_FIELDS) - set(names)
226
- )
227
- self._config_accessor.save()
228
- flash("Updated sharepic preferences.", category="success")
229
- pass
230
-
231
- def render_strava(self) -> dict:
232
- return {
233
- "strava_client_id": self._config_accessor().strava_client_id,
234
- "strava_client_secret": self._config_accessor().strava_client_secret,
235
- "strava_client_code": self._config_accessor().strava_client_code,
236
- }
237
-
238
- def save_strava(self, client_id: str, client_secret: str) -> str:
239
- self._strava_client_id = client_id
240
- self._strava_client_secret = client_secret
241
-
242
- payload = {
243
- "client_id": client_id,
244
- "redirect_uri": url_for(".strava_callback", _external=True),
245
- "response_type": "code",
246
- "scope": "activity:read_all",
247
- }
248
-
249
- arg_string = "&".join(
250
- f"{key}={urllib.parse.quote(value)}" for key, value in payload.items()
251
- )
252
- return f"https://www.strava.com/oauth/authorize?{arg_string}"
253
-
254
- def save_strava_code(self, code: str) -> None:
255
- self._config_accessor().strava_client_id = int(self._strava_client_id)
256
- self._config_accessor().strava_client_secret = self._strava_client_secret
257
- self._config_accessor().strava_client_code = code
258
- self._config_accessor.save()
259
- flash("Connected to Strava API", category="success")
260
-
261
-
262
- def _wrap_coordinates(coordinates: list[list[float]]) -> dict:
263
- return {
264
- "type": "FeatureCollection",
265
- "features": [
266
- {
267
- "type": "Feature",
268
- "properties": {},
269
- "geometry": {"coordinates": [coordinates], "type": "Polygon"},
270
- }
271
- ],
272
- }
@@ -1,44 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
-
5
- <h1 class="mb-3">Equipment Offsets</h1>
6
-
7
- <p>If you record every activity with an equipment but haven't started from the beginning, you might have a certain
8
- distance that is not accounted for. In order to have this reflected in the equipment overview, you can enter offsets
9
- here.</p>
10
-
11
- <form method="POST">
12
- <div class="row row-cols-1 row-cols-md-3 g-4 mb-3">
13
- {% for equipment, offset in equipment_offsets.items() %}
14
- <div class="col-md-4">
15
- <div class="mb-3">
16
- <label for="equipment_{{ loop.index }}" class="form-label">Equipment</label>
17
- <input type="text" class="form-control" id="equipment_{{ loop.index }}" name="equipment"
18
- value="{{ equipment }}" />
19
- </div>
20
- <div class="mb-3">
21
- <label for="offset_{{ loop.index }}" class="form-label">Offset / km</label>
22
- <input type="number" class="form-control" id="offset_{{ loop.index }}" name="offset"
23
- value="{{ offset }}" />
24
- </div>
25
- </div>
26
- {% endfor %}
27
-
28
- <div class="col-md-4">
29
- <div class="mb-3">
30
- <label for="equipment_new" class="form-label">Equipment</label>
31
- <input type="text" class="form-control" id="equipment_new" name="equipment" />
32
- </div>
33
- <div class="mb-3">
34
- <label for="offset_new" class="form-label">Offset / km</label>
35
- <input type="number" class="form-control" id="offset_new" name="offset" />
36
- </div>
37
- </div>
38
- </div>
39
-
40
- <button type="submit" class="btn btn-primary">Save</button>
41
- </form>
42
-
43
-
44
- {% endblock %}
@@ -1,25 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
-
5
- <h1 class="mb-3">Kind renaming</h1>
6
-
7
- <p>If you have used different apps for tracking, you might have that your bike rides have <i>kind</i> "ride", "Ride",
8
- "Radfahrt" and so on. In order to unify these, you can specify mappings from old to new names.</p>
9
-
10
- <p>If you want to unify these to "Ride", enter the following:</p>
11
-
12
- <pre><code>
13
- ride =&gt; Ride
14
- Radfahrt =&gt; Ride
15
- </code></pre>
16
-
17
- <form method="POST">
18
- <div class="mb-3">
19
- <label for="rules" class="form-label">Rules</label>
20
- <textarea class="form-control" id=rules" cols="80" rows="10" name="rules_str">{{ rules_str }}</textarea>
21
- </div>
22
- <button type="submit" class="btn btn-primary">Save</button>
23
- </form>
24
-
25
- {% endblock %}
@@ -1,30 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
-
5
- <h1 class="mb-3">Kinds without Achievements</h1>
6
-
7
- <p>Apart from bicycle rides and runs, you might also record your car and train rides. If you don't want to include these
8
- for achievements like explorer tiles, you can enter the activity kinds that should not be considered here.</p>
9
-
10
- <form method="POST">
11
- <div class="row">
12
- <div class="col-md-6">
13
- {% for kind in kinds_without_achievements %}
14
- <div class="mb-3">
15
- <label for="kind_{{ loop.index }}" class="form-label">Kind</label>
16
- <input type="text" class="form-control" id="kind_{{ loop.index }}" name="kind" value="{{ kind }}" />
17
- </div>
18
- {% endfor %}
19
- <div class="mb-3">
20
- <label for="regex_new" class="form-label">Kind</label>
21
- <input type="text" class="form-control" id="kind_new" name="kind" />
22
- </div>
23
- </div>
24
- </div>
25
-
26
- <button type="submit" class="btn btn-primary">Save</button>
27
- </form>
28
-
29
-
30
- {% endblock %}
@@ -1,42 +0,0 @@
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 geo_activity_playground.core.config import Config
9
- from geo_activity_playground.core.raster_map import get_tile
10
-
11
-
12
- def make_tile_blueprint(config: Config) -> Blueprint:
13
- blueprint = Blueprint("tiles", __name__, template_folder="templates")
14
-
15
- @blueprint.route("/color/<int:z>/<int:x>/<int:y>.png")
16
- def tile_color(x: int, y: int, z: int):
17
- map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
18
- f = io.BytesIO()
19
- pl.imsave(f, map_tile, format="png")
20
- return Response(bytes(f.getbuffer()), mimetype="image/png")
21
-
22
- @blueprint.route("/grayscale/<int:z>/<int:x>/<int:y>.png")
23
- def tile_grayscale(x: int, y: int, z: int):
24
- map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
25
- map_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2) # to grayscale
26
- map_tile = np.dstack((map_tile, map_tile, map_tile)) # to rgb
27
- f = io.BytesIO()
28
- pl.imsave(f, map_tile, format="png")
29
- return Response(bytes(f.getbuffer()), mimetype="image/png")
30
-
31
- @blueprint.route("/pastel/<int:z>/<int:x>/<int:y>.png")
32
- def tile_pastel(x: int, y: int, z: int):
33
- map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
34
- averaged_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2)
35
- grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
36
- factor = 0.7
37
- pastel_tile = factor * grayscale_tile + (1 - factor) * map_tile
38
- f = io.BytesIO()
39
- pl.imsave(f, pastel_tile, format="png")
40
- return Response(bytes(f.getbuffer()), mimetype="image/png")
41
-
42
- return blueprint