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
@@ -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")
|
geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py}
RENAMED
@@ -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
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
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
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
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 =
|
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
|
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(
|
@@ -163,6 +151,13 @@ def plot_monthly_distance(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
|
|
163
151
|
alt.Y("sum(distance_km)", title="Distance / km"),
|
164
152
|
alt.Color("kind", scale=kind_scale, title="Kind"),
|
165
153
|
alt.Column("year(start):O", title="Year"),
|
154
|
+
[
|
155
|
+
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
156
|
+
alt.Tooltip(
|
157
|
+
"sum(distance_km)", format=".1f", title="Total distance / km"
|
158
|
+
),
|
159
|
+
alt.Tooltip("count(distance_km)", title="Number of activities"),
|
160
|
+
],
|
166
161
|
)
|
167
162
|
.resolve_axis(x="independent")
|
168
163
|
.to_json(format="vega")
|
@@ -180,7 +175,7 @@ def plot_yearly_distance(year_kind_total: pd.DataFrame, kind_scale: alt.Scale) -
|
|
180
175
|
[
|
181
176
|
alt.Tooltip("year:O", title="Year"),
|
182
177
|
alt.Tooltip("kind", title="Kind"),
|
183
|
-
alt.Tooltip("distance_km", title="Distance / km"),
|
178
|
+
alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
|
184
179
|
],
|
185
180
|
)
|
186
181
|
.to_json(format="vega")
|
@@ -210,7 +205,7 @@ def plot_year_cumulative(df: pd.DataFrame) -> str:
|
|
210
205
|
[
|
211
206
|
alt.Tooltip("week", title="Week"),
|
212
207
|
alt.Tooltip("iso_year:N", title="Year"),
|
213
|
-
alt.Tooltip("distance_km", title="Distance / km"),
|
208
|
+
alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
|
214
209
|
],
|
215
210
|
)
|
216
211
|
.interactive()
|
@@ -267,7 +262,7 @@ def plot_weekly_distance(df: pd.DataFrame, kind_scale: alt.Scale) -> str:
|
|
267
262
|
[
|
268
263
|
alt.Tooltip("year_week", title="Year and Week"),
|
269
264
|
alt.Tooltip("kind", title="Kind"),
|
270
|
-
alt.Tooltip("distance_km", title="Distance / km"),
|
265
|
+
alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
|
271
266
|
],
|
272
267
|
)
|
273
268
|
.to_json(format="vega")
|
@@ -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
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from
|
21
|
-
from
|
22
|
-
|
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
|
-
|
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)
|