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