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.
- geo_activity_playground/__main__.py +5 -47
- geo_activity_playground/alembic/README +1 -0
- geo_activity_playground/alembic/env.py +76 -0
- geo_activity_playground/alembic/script.py.mako +26 -0
- geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +33 -0
- geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +73 -0
- geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +28 -0
- geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +30 -0
- geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +28 -0
- geo_activity_playground/alembic/versions/script.py.mako +28 -0
- geo_activity_playground/core/activities.py +50 -136
- geo_activity_playground/core/config.py +3 -3
- geo_activity_playground/core/datamodel.py +257 -0
- geo_activity_playground/core/enrichment.py +90 -92
- geo_activity_playground/core/heart_rate.py +1 -2
- geo_activity_playground/core/paths.py +6 -7
- geo_activity_playground/core/raster_map.py +43 -4
- geo_activity_playground/core/similarity.py +1 -2
- geo_activity_playground/core/tasks.py +2 -2
- geo_activity_playground/core/test_meta_search.py +3 -3
- geo_activity_playground/core/test_summary_stats.py +1 -1
- geo_activity_playground/explorer/grid_file.py +2 -2
- geo_activity_playground/explorer/tile_visits.py +8 -10
- geo_activity_playground/heatmap_video.py +7 -8
- geo_activity_playground/importers/activity_parsers.py +2 -2
- geo_activity_playground/importers/directory.py +9 -10
- geo_activity_playground/importers/strava_api.py +9 -9
- geo_activity_playground/importers/strava_checkout.py +12 -13
- geo_activity_playground/importers/test_csv_parser.py +3 -3
- geo_activity_playground/importers/test_directory.py +1 -1
- geo_activity_playground/importers/test_strava_api.py +1 -1
- geo_activity_playground/webui/app.py +94 -86
- geo_activity_playground/webui/authenticator.py +1 -1
- geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
- geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
- geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
- geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +9 -5
- geo_activity_playground/webui/blueprints/entry_views.py +68 -0
- geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +58 -4
- geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
- geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
- geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
- geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
- geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
- geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +21 -26
- geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
- geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
- geo_activity_playground/webui/flasher.py +26 -0
- geo_activity_playground/webui/plot_util.py +1 -1
- geo_activity_playground/webui/search_util.py +4 -6
- geo_activity_playground/webui/static/images/layers-2x.png +0 -0
- geo_activity_playground/webui/static/images/layers.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon.png +0 -0
- geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
- geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
- geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
- geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
- geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +60 -36
- geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
- geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
- geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
- geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
- geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
- geo_activity_playground/webui/templates/home.html.j2 +6 -6
- geo_activity_playground/webui/templates/page.html.j2 +2 -1
- geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
- geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
- geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +1 -1
- geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
- geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
- {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/METADATA +5 -1
- geo_activity_playground-0.39.0.dist-info/RECORD +133 -0
- geo_activity_playground/__init__.py +0 -0
- geo_activity_playground/core/__init__.py +0 -0
- geo_activity_playground/explorer/__init__.py +0 -0
- geo_activity_playground/importers/__init__.py +0 -0
- geo_activity_playground/webui/__init__.py +0 -0
- geo_activity_playground/webui/activity/__init__.py +0 -0
- geo_activity_playground/webui/activity/blueprint.py +0 -109
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
- geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
- geo_activity_playground/webui/calendar/__init__.py +0 -0
- geo_activity_playground/webui/calendar/blueprint.py +0 -23
- geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
- geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
- geo_activity_playground/webui/entry_controller.py +0 -63
- geo_activity_playground/webui/explorer/__init__.py +0 -0
- geo_activity_playground/webui/explorer/blueprint.py +0 -62
- geo_activity_playground/webui/heatmap/__init__.py +0 -0
- geo_activity_playground/webui/heatmap/blueprint.py +0 -51
- geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
- geo_activity_playground/webui/settings/blueprint.py +0 -262
- geo_activity_playground/webui/settings/controller.py +0 -272
- geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
- geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
- geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
- geo_activity_playground/webui/tile_blueprint.py +0 -42
- geo_activity_playground-0.38.1.dist-info/RECORD +0 -129
- /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
- /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
- /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.38.1.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
- {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 => Ride
|
14
|
-
Radfahrt => 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 %}
|
geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2
DELETED
@@ -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
|