geo-activity-playground 0.36.2__py3-none-any.whl → 0.38.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/core/activities.py +12 -0
- geo_activity_playground/core/config.py +6 -2
- geo_activity_playground/core/enrichment.py +9 -0
- geo_activity_playground/core/meta_search.py +157 -0
- geo_activity_playground/core/summary_stats.py +30 -0
- geo_activity_playground/core/test_meta_search.py +100 -0
- geo_activity_playground/core/test_summary_stats.py +108 -0
- geo_activity_playground/webui/activity/controller.py +20 -0
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -10
- geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -0
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +17 -0
- geo_activity_playground/webui/app.py +27 -10
- geo_activity_playground/webui/eddington_blueprint.py +167 -58
- geo_activity_playground/webui/{equipment/controller.py → equipment_blueprint.py} +29 -42
- geo_activity_playground/webui/explorer/blueprint.py +4 -0
- geo_activity_playground/webui/heatmap/blueprint.py +10 -29
- geo_activity_playground/webui/heatmap/heatmap_controller.py +45 -103
- geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +5 -37
- geo_activity_playground/webui/search_blueprint.py +40 -71
- geo_activity_playground/webui/search_util.py +64 -0
- geo_activity_playground/webui/summary_blueprint.py +40 -39
- geo_activity_playground/webui/templates/eddington/index.html.j2 +73 -1
- geo_activity_playground/webui/{equipment/templates → templates}/equipment/index.html.j2 +3 -5
- geo_activity_playground/webui/templates/search/index.html.j2 +34 -87
- geo_activity_playground/webui/templates/search_form.html.j2 +116 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +5 -1
- {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/RECORD +31 -27
- geo_activity_playground/webui/equipment/__init__.py +0 -0
- geo_activity_playground/webui/equipment/blueprint.py +0 -16
- {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/entry_points.txt +0 -0
@@ -11,6 +11,8 @@ from PIL import ImageDraw
|
|
11
11
|
|
12
12
|
from geo_activity_playground.core.activities import ActivityRepository
|
13
13
|
from geo_activity_playground.core.config import Config
|
14
|
+
from geo_activity_playground.core.meta_search import apply_search_query
|
15
|
+
from geo_activity_playground.core.meta_search import SearchQuery
|
14
16
|
from geo_activity_playground.core.raster_map import convert_to_grayscale
|
15
17
|
from geo_activity_playground.core.raster_map import GeoBounds
|
16
18
|
from geo_activity_playground.core.raster_map import get_sensible_zoom_level
|
@@ -48,12 +50,7 @@ class HeatmapController:
|
|
48
50
|
"activities_per_tile"
|
49
51
|
]
|
50
52
|
|
51
|
-
def render(
|
52
|
-
self,
|
53
|
-
kinds: list[int],
|
54
|
-
date_start: Optional[datetime.date],
|
55
|
-
date_end: Optional[datetime.date],
|
56
|
-
) -> dict:
|
53
|
+
def render(self, query: SearchQuery) -> dict:
|
57
54
|
zoom = 14
|
58
55
|
tiles = self.tile_histories[zoom]
|
59
56
|
medians = tiles.median(skipna=True)
|
@@ -62,19 +59,6 @@ class HeatmapController:
|
|
62
59
|
)
|
63
60
|
cluster_state = self.tile_evolution_states[zoom]
|
64
61
|
|
65
|
-
available_kinds = sorted(self._repository.meta["kind"].unique())
|
66
|
-
|
67
|
-
if not kinds:
|
68
|
-
kinds = list(range(len(available_kinds)))
|
69
|
-
|
70
|
-
extra_args = []
|
71
|
-
if date_start is not None:
|
72
|
-
extra_args.append(f"date-start={date_start.isoformat()}")
|
73
|
-
if date_end is not None:
|
74
|
-
extra_args.append(f"date-end={date_end.isoformat()}")
|
75
|
-
for kind in kinds:
|
76
|
-
extra_args.append(f"kind={kind}")
|
77
|
-
|
78
62
|
values = {
|
79
63
|
"center": {
|
80
64
|
"latitude": median_lat,
|
@@ -87,14 +71,9 @@ class HeatmapController:
|
|
87
71
|
else {}
|
88
72
|
),
|
89
73
|
},
|
90
|
-
"
|
91
|
-
"
|
92
|
-
"extra_args": "&".join(extra_args),
|
74
|
+
"extra_args": query.to_url_str(),
|
75
|
+
"query": query.to_jinja(),
|
93
76
|
}
|
94
|
-
if date_start is not None:
|
95
|
-
values["date_start"] = date_start.date().isoformat()
|
96
|
-
if date_end is not None:
|
97
|
-
values["date_end"] = date_end.date().isoformat()
|
98
77
|
|
99
78
|
return values
|
100
79
|
|
@@ -103,16 +82,12 @@ class HeatmapController:
|
|
103
82
|
x: int,
|
104
83
|
y: int,
|
105
84
|
z: int,
|
106
|
-
|
107
|
-
date_start: Optional[datetime.date],
|
108
|
-
date_end: Optional[datetime.date],
|
85
|
+
query: SearchQuery,
|
109
86
|
) -> np.ndarray:
|
110
87
|
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
111
88
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
112
|
-
if
|
113
|
-
tile_count_cache_path = pathlib.Path(
|
114
|
-
f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy"
|
115
|
-
)
|
89
|
+
if not query.active:
|
90
|
+
tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
|
116
91
|
if tile_count_cache_path.exists():
|
117
92
|
try:
|
118
93
|
tile_counts = np.load(tile_count_cache_path)
|
@@ -124,52 +99,41 @@ class HeatmapController:
|
|
124
99
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
125
100
|
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
126
101
|
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
102
|
+
|
103
|
+
with work_tracker(
|
104
|
+
tile_count_cache_path.with_suffix(".json")
|
105
|
+
) as parsed_activities:
|
106
|
+
if parsed_activities - activity_ids:
|
107
|
+
logger.warning(
|
108
|
+
f"Resetting heatmap cache for {x=}/{y=}/{z=} because activities have been removed."
|
109
|
+
)
|
110
|
+
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
111
|
+
parsed_activities.clear()
|
112
|
+
for activity_id in activity_ids:
|
113
|
+
if activity_id in parsed_activities:
|
114
|
+
continue
|
115
|
+
parsed_activities.add(activity_id)
|
116
|
+
time_series = self._repository.get_time_series(activity_id)
|
117
|
+
for _, group in time_series.groupby("segment_id"):
|
118
|
+
xy_pixels = (
|
119
|
+
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
120
|
+
* OSM_TILE_SIZE
|
139
121
|
)
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
[group["x"] * 2**z - x, group["y"] * 2**z - y]
|
151
|
-
).T
|
152
|
-
* OSM_TILE_SIZE
|
153
|
-
)
|
154
|
-
im = Image.new("L", tile_pixels)
|
155
|
-
draw = ImageDraw.Draw(im)
|
156
|
-
pixels = list(map(int, xy_pixels.flatten()))
|
157
|
-
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
158
|
-
aim = np.array(im)
|
159
|
-
tile_counts += aim
|
160
|
-
tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
|
161
|
-
np.save(tmp_path, tile_counts)
|
162
|
-
tile_count_cache_path.unlink(missing_ok=True)
|
163
|
-
tmp_path.rename(tile_count_cache_path)
|
122
|
+
im = Image.new("L", tile_pixels)
|
123
|
+
draw = ImageDraw.Draw(im)
|
124
|
+
pixels = list(map(int, xy_pixels.flatten()))
|
125
|
+
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
126
|
+
aim = np.array(im)
|
127
|
+
tile_counts += aim
|
128
|
+
tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
|
129
|
+
np.save(tmp_path, tile_counts)
|
130
|
+
tile_count_cache_path.unlink(missing_ok=True)
|
131
|
+
tmp_path.rename(tile_count_cache_path)
|
164
132
|
else:
|
133
|
+
activities = apply_search_query(self._repository.meta, query)
|
165
134
|
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
166
135
|
for activity_id in activity_ids:
|
167
|
-
|
168
|
-
if not activity["kind"] == kind:
|
169
|
-
continue
|
170
|
-
if date_start is not None and activity["start"] < date_start:
|
171
|
-
continue
|
172
|
-
if date_end is not None and date_end < activity["start"]:
|
136
|
+
if activity_id not in activities["id"]:
|
173
137
|
continue
|
174
138
|
time_series = self._repository.get_time_series(activity_id)
|
175
139
|
for _, group in time_series.groupby("segment_id"):
|
@@ -190,16 +154,11 @@ class HeatmapController:
|
|
190
154
|
x: int,
|
191
155
|
y: int,
|
192
156
|
z: int,
|
193
|
-
|
194
|
-
date_start: Optional[datetime.date],
|
195
|
-
date_end: Optional[datetime.date],
|
157
|
+
query: SearchQuery,
|
196
158
|
) -> np.ndarray:
|
197
159
|
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
198
160
|
tile_counts = np.zeros(tile_pixels)
|
199
|
-
|
200
|
-
for kind_id in kinds_ids:
|
201
|
-
kind = available_kinds[kind_id]
|
202
|
-
tile_counts += self._get_counts(x, y, z, kind, date_start, date_end)
|
161
|
+
tile_counts += self._get_counts(x, y, z, query)
|
203
162
|
|
204
163
|
tile_counts = np.sqrt(tile_counts) / 5
|
205
164
|
tile_counts[tile_counts > 1.0] = 1.0
|
@@ -217,32 +176,17 @@ class HeatmapController:
|
|
217
176
|
] + data_color[:, :, c]
|
218
177
|
return map_tile
|
219
178
|
|
220
|
-
def render_tile(
|
221
|
-
self,
|
222
|
-
x: int,
|
223
|
-
y: int,
|
224
|
-
z: int,
|
225
|
-
kind_ids: list[int],
|
226
|
-
date_start: Optional[datetime.date],
|
227
|
-
date_end: Optional[datetime.date],
|
228
|
-
) -> bytes:
|
179
|
+
def render_tile(self, x: int, y: int, z: int, query: SearchQuery) -> bytes:
|
229
180
|
f = io.BytesIO()
|
230
181
|
pl.imsave(
|
231
182
|
f,
|
232
|
-
self._render_tile_image(x, y, z,
|
183
|
+
self._render_tile_image(x, y, z, query),
|
233
184
|
format="png",
|
234
185
|
)
|
235
186
|
return bytes(f.getbuffer())
|
236
187
|
|
237
188
|
def download_heatmap(
|
238
|
-
self,
|
239
|
-
north: float,
|
240
|
-
east: float,
|
241
|
-
south: float,
|
242
|
-
west: float,
|
243
|
-
kind_ids: list[int],
|
244
|
-
date_start: Optional[datetime.date],
|
245
|
-
date_end: Optional[datetime.date],
|
189
|
+
self, north: float, east: float, south: float, west: float, query: SearchQuery
|
246
190
|
) -> bytes:
|
247
191
|
geo_bounds = GeoBounds(south, west, north, east)
|
248
192
|
tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
|
@@ -265,9 +209,7 @@ class HeatmapController:
|
|
265
209
|
i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
|
266
210
|
j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
|
267
211
|
:,
|
268
|
-
] = self._render_tile_image(
|
269
|
-
x, y, tile_bounds.zoom, kind_ids, date_start, date_end
|
270
|
-
)
|
212
|
+
] = self._render_tile_image(x, y, tile_bounds.zoom, query)
|
271
213
|
|
272
214
|
f = io.BytesIO()
|
273
215
|
pl.imsave(f, background, format="png")
|
@@ -1,44 +1,12 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
+
{% from "search_form.html.j2" import search_form %}
|
2
3
|
|
3
4
|
{% block container %}
|
4
|
-
<
|
5
|
-
<div class="col">
|
6
|
-
<h1>Heatmap</h1>
|
7
|
-
</div>
|
8
|
-
</div>
|
9
|
-
|
10
|
-
<form action="" method="GET">
|
11
|
-
|
12
|
-
<div class="row mb-3">
|
13
|
-
<label class="col-sm-2 col-form-label">Kinds</label>
|
5
|
+
<h1 class="mb-3">Heatmap</h1>
|
14
6
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
<input class="form-check-input" type="checkbox" role="switch" id="kind{{ loop.index0 }}" name="kind"
|
19
|
-
value="{{ loop.index0 }}" {{ 'checked' if loop.index0 in kinds else '' }} />
|
20
|
-
<label class="form-check-label" for="kind{{ loop.index0 }}">{{ kind }}</label>
|
21
|
-
</div>
|
22
|
-
{% endfor %}
|
23
|
-
</div>
|
24
|
-
</div>
|
25
|
-
|
26
|
-
<div class="row mb-3">
|
27
|
-
<label class="col-sm-2 col-form-label">Date range</label>
|
28
|
-
<div class="col-sm-5">
|
29
|
-
<input type="date" id="date-start" name="date-start" value="{{ date_start }}" />
|
30
|
-
<label for="date-start" class="form-label">Start date</label>
|
31
|
-
</div>
|
32
|
-
<div class="col-sm-5">
|
33
|
-
<input type="date" id="date-end" name="date-end" value="{{ date_end }}" />
|
34
|
-
<label for="date-start" class="form-label">End date</label>
|
35
|
-
</div>
|
36
|
-
</div>
|
37
|
-
|
38
|
-
<div class="mb-3">
|
39
|
-
<button type="submit" class="btn btn-primary">Filter heatmap</button>
|
40
|
-
</div>
|
41
|
-
</form>
|
7
|
+
<div class="mb-3">
|
8
|
+
{{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
|
9
|
+
</div>
|
42
10
|
|
43
11
|
<div class="row mb-3">
|
44
12
|
<div class="col">
|
@@ -1,13 +1,22 @@
|
|
1
|
+
import urllib.parse
|
1
2
|
from functools import reduce
|
2
3
|
|
3
4
|
import dateutil.parser
|
4
5
|
from flask import Blueprint
|
5
6
|
from flask import flash
|
7
|
+
from flask import redirect
|
6
8
|
from flask import render_template
|
7
9
|
from flask import request
|
8
10
|
from flask import Response
|
9
11
|
|
10
12
|
from ..core.activities import ActivityRepository
|
13
|
+
from geo_activity_playground.core.config import ConfigAccessor
|
14
|
+
from geo_activity_playground.core.meta_search import apply_search_query
|
15
|
+
from geo_activity_playground.core.meta_search import SearchQuery
|
16
|
+
from geo_activity_playground.webui.authenticator import Authenticator
|
17
|
+
from geo_activity_playground.webui.authenticator import needs_authentication
|
18
|
+
from geo_activity_playground.webui.search_util import search_query_from_form
|
19
|
+
from geo_activity_playground.webui.search_util import SearchQueryHistory
|
11
20
|
|
12
21
|
|
13
22
|
def reduce_or(selections):
|
@@ -18,84 +27,44 @@ def reduce_and(selections):
|
|
18
27
|
return reduce(lambda a, b: a & b, selections)
|
19
28
|
|
20
29
|
|
21
|
-
def make_search_blueprint(
|
30
|
+
def make_search_blueprint(
|
31
|
+
repository: ActivityRepository,
|
32
|
+
search_query_history: SearchQueryHistory,
|
33
|
+
authenticator: Authenticator,
|
34
|
+
config_accessor: ConfigAccessor,
|
35
|
+
) -> Blueprint:
|
22
36
|
blueprint = Blueprint("search", __name__, template_folder="templates")
|
23
37
|
|
24
38
|
@blueprint.route("/")
|
25
39
|
def index():
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
print(request.args)
|
30
|
-
|
31
|
-
activities = repository.meta
|
32
|
-
|
33
|
-
if equipments := request.args.getlist("equipment"):
|
34
|
-
selection = reduce_or(
|
35
|
-
activities["equipment"] == equipment for equipment in equipments
|
36
|
-
)
|
37
|
-
activities = activities.loc[selection]
|
38
|
-
|
39
|
-
if kinds := request.args.getlist("kind"):
|
40
|
-
selection = reduce_or(activities["kind"] == kind for kind in kinds)
|
41
|
-
activities = activities.loc[selection]
|
42
|
-
|
43
|
-
name_exact = bool(request.args.get("name_exact", False))
|
44
|
-
name_casing = bool(request.args.get("name_casing", False))
|
45
|
-
if name := request.args.get("name", ""):
|
46
|
-
if name_casing:
|
47
|
-
haystack = activities["name"]
|
48
|
-
needle = name
|
49
|
-
else:
|
50
|
-
haystack = activities["name"].str.lower()
|
51
|
-
needle = name.lower()
|
52
|
-
if name_exact:
|
53
|
-
selection = haystack == needle
|
54
|
-
else:
|
55
|
-
selection = [needle in an for an in haystack]
|
56
|
-
activities = activities.loc[selection]
|
57
|
-
|
58
|
-
begin = request.args.get("begin", "")
|
59
|
-
end = request.args.get("end", "")
|
60
|
-
|
61
|
-
if begin:
|
62
|
-
try:
|
63
|
-
begin_dt = dateutil.parser.parse(begin)
|
64
|
-
except ValueError:
|
65
|
-
flash(
|
66
|
-
f"Cannot parse date `{begin}`, please use a different format.",
|
67
|
-
category="danger",
|
68
|
-
)
|
69
|
-
else:
|
70
|
-
selection = begin_dt <= activities["start"]
|
71
|
-
activities = activities.loc[selection]
|
72
|
-
|
73
|
-
if end:
|
74
|
-
try:
|
75
|
-
end_dt = dateutil.parser.parse(end)
|
76
|
-
except ValueError:
|
77
|
-
flash(
|
78
|
-
f"Cannot parse date `{end}`, please use a different format.",
|
79
|
-
category="danger",
|
80
|
-
)
|
81
|
-
else:
|
82
|
-
selection = activities["start"] < end_dt
|
83
|
-
activities = activities.loc[selection]
|
84
|
-
|
85
|
-
activities = activities.sort_values("start", ascending=False)
|
40
|
+
query = search_query_from_form(request.args)
|
41
|
+
search_query_history.register_query(query)
|
42
|
+
activities = apply_search_query(repository.meta, query)
|
86
43
|
|
87
44
|
return render_template(
|
88
45
|
"search/index.html.j2",
|
89
|
-
activities=list(activities.iterrows()),
|
90
|
-
|
91
|
-
equipments_avail=sorted(equipments_avail),
|
92
|
-
kinds=request.args.getlist("kind"),
|
93
|
-
kinds_avail=sorted(kinds_avail),
|
94
|
-
name=name,
|
95
|
-
name_exact=name_exact,
|
96
|
-
name_casing=name_casing,
|
97
|
-
begin=begin,
|
98
|
-
end=end,
|
46
|
+
activities=reversed(list(activities.iterrows())),
|
47
|
+
query=query.to_jinja(),
|
99
48
|
)
|
100
49
|
|
50
|
+
@blueprint.route("/save-search-query")
|
51
|
+
@needs_authentication(authenticator)
|
52
|
+
def save_search_query():
|
53
|
+
query = search_query_from_form(request.args)
|
54
|
+
primitives = query.to_primitives()
|
55
|
+
if primitives not in config_accessor().search_queries_favorites:
|
56
|
+
config_accessor().search_queries_favorites.append(primitives)
|
57
|
+
config_accessor.save()
|
58
|
+
return redirect(urllib.parse.unquote_plus(request.args["redirect"]))
|
59
|
+
|
60
|
+
@blueprint.route("/delete-search-query")
|
61
|
+
@needs_authentication(authenticator)
|
62
|
+
def delete_search_query():
|
63
|
+
query = search_query_from_form(request.args)
|
64
|
+
primitives = query.to_primitives()
|
65
|
+
if primitives in config_accessor().search_queries_favorites:
|
66
|
+
config_accessor().search_queries_favorites.remove(primitives)
|
67
|
+
config_accessor.save()
|
68
|
+
return redirect(urllib.parse.unquote_plus(request.args["redirect"]))
|
69
|
+
|
101
70
|
return blueprint
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from werkzeug.datastructures import MultiDict
|
4
|
+
|
5
|
+
from geo_activity_playground.core.config import ConfigAccessor
|
6
|
+
from geo_activity_playground.core.meta_search import _parse_date_or_none
|
7
|
+
from geo_activity_playground.core.meta_search import SearchQuery
|
8
|
+
from geo_activity_playground.webui.authenticator import Authenticator
|
9
|
+
|
10
|
+
|
11
|
+
def search_query_from_form(args: MultiDict) -> SearchQuery:
|
12
|
+
query = SearchQuery(
|
13
|
+
equipment=args.getlist("equipment"),
|
14
|
+
kind=args.getlist("kind"),
|
15
|
+
name=args.get("name", None),
|
16
|
+
name_case_sensitive=_parse_bool(args.get("name_case_sensitive", "false")),
|
17
|
+
start_begin=_parse_date_or_none(args.get("start_begin", None)),
|
18
|
+
start_end=_parse_date_or_none(args.get("start_end", None)),
|
19
|
+
)
|
20
|
+
|
21
|
+
return query
|
22
|
+
|
23
|
+
|
24
|
+
def _parse_bool(s: str) -> bool:
|
25
|
+
return s == "true"
|
26
|
+
|
27
|
+
|
28
|
+
class SearchQueryHistory:
|
29
|
+
def __init__(
|
30
|
+
self, config_accessor: ConfigAccessor, authenticator: Authenticator
|
31
|
+
) -> None:
|
32
|
+
self._config_accessor = config_accessor
|
33
|
+
self._authenticator = authenticator
|
34
|
+
|
35
|
+
def register_query(self, search_query: SearchQuery) -> None:
|
36
|
+
if not self._authenticator.is_authenticated():
|
37
|
+
return
|
38
|
+
|
39
|
+
if not search_query.active:
|
40
|
+
return
|
41
|
+
|
42
|
+
primitives = search_query.to_primitives()
|
43
|
+
while primitives in self._config_accessor().search_queries_last:
|
44
|
+
self._config_accessor().search_queries_last.remove(primitives)
|
45
|
+
self._config_accessor().search_queries_last.append(primitives)
|
46
|
+
while (
|
47
|
+
len(self._config_accessor().search_queries_last)
|
48
|
+
> self._config_accessor().search_queries_num_keep
|
49
|
+
):
|
50
|
+
self._config_accessor().search_queries_last.pop(0)
|
51
|
+
self._config_accessor.save()
|
52
|
+
|
53
|
+
def prepare_favorites(self) -> list[dict]:
|
54
|
+
return self._prepare_list(self._config_accessor().search_queries_favorites)
|
55
|
+
|
56
|
+
def prepare_last(self) -> list[dict]:
|
57
|
+
return self._prepare_list(self._config_accessor().search_queries_last)
|
58
|
+
|
59
|
+
def _prepare_list(self, l: list[dict]) -> list[tuple[str, dict]]:
|
60
|
+
result = []
|
61
|
+
for elem in l:
|
62
|
+
search_query = SearchQuery.from_primitives(elem)
|
63
|
+
result.append((str(search_query), search_query.to_url_str()))
|
64
|
+
return result
|
@@ -5,20 +5,32 @@ import altair as alt
|
|
5
5
|
import pandas as pd
|
6
6
|
from flask import Blueprint
|
7
7
|
from flask import render_template
|
8
|
+
from flask import request
|
8
9
|
|
9
10
|
from geo_activity_playground.core.activities import ActivityRepository
|
10
11
|
from geo_activity_playground.core.activities import make_geojson_from_time_series
|
11
12
|
from geo_activity_playground.core.config import Config
|
13
|
+
from geo_activity_playground.core.meta_search import apply_search_query
|
12
14
|
from geo_activity_playground.webui.plot_util import make_kind_scale
|
15
|
+
from geo_activity_playground.webui.search_util import search_query_from_form
|
16
|
+
from geo_activity_playground.webui.search_util import SearchQueryHistory
|
13
17
|
|
14
18
|
|
15
|
-
def make_summary_blueprint(
|
19
|
+
def make_summary_blueprint(
|
20
|
+
repository: ActivityRepository,
|
21
|
+
config: Config,
|
22
|
+
search_query_history: SearchQueryHistory,
|
23
|
+
) -> Blueprint:
|
16
24
|
blueprint = Blueprint("summary", __name__, template_folder="templates")
|
17
25
|
|
18
26
|
@blueprint.route("/")
|
19
27
|
def index():
|
28
|
+
query = search_query_from_form(request.args)
|
29
|
+
search_query_history.register_query(query)
|
30
|
+
activities = apply_search_query(repository.meta, query)
|
31
|
+
|
20
32
|
kind_scale = make_kind_scale(repository.meta, config)
|
21
|
-
df = embellished_activities(
|
33
|
+
df = embellished_activities(activities)
|
22
34
|
# df = df.loc[df["consider_for_achievements"]]
|
23
35
|
|
24
36
|
year_kind_total = (
|
@@ -46,8 +58,9 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
|
|
46
58
|
repository.get_time_series(activity_id)
|
47
59
|
),
|
48
60
|
)
|
49
|
-
for activity_id, reasons in nominate_activities(
|
61
|
+
for activity_id, reasons in nominate_activities(df).items()
|
50
62
|
],
|
63
|
+
query=query.to_jinja(),
|
51
64
|
)
|
52
65
|
|
53
66
|
return blueprint
|
@@ -56,46 +69,35 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
|
|
56
69
|
def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
|
57
70
|
nominations: dict[int, list[str]] = collections.defaultdict(list)
|
58
71
|
|
59
|
-
|
60
|
-
|
61
|
-
i = subset["distance_km"].idxmax()
|
62
|
-
nominations[i].append(f"Greatest distance: {meta.loc[i].distance_km:.1f} km")
|
72
|
+
_nominate_activities_inner(meta, "", nominations)
|
63
73
|
|
64
|
-
|
65
|
-
|
74
|
+
for kind, group in meta.groupby("kind"):
|
75
|
+
_nominate_activities_inner(group, f" for {kind}", nominations)
|
76
|
+
for equipment, group in meta.groupby("equipment"):
|
77
|
+
_nominate_activities_inner(group, f" with {equipment}", nominations)
|
66
78
|
|
67
|
-
|
68
|
-
i = subset["calories"].idxmax()
|
69
|
-
nominations[i].append(f"Most calories burnt: {meta.loc[i].calories:.0f} kcal")
|
79
|
+
return nominations
|
70
80
|
|
71
|
-
if "steps" in subset.columns and not pd.isna(subset["steps"]).all():
|
72
|
-
i = subset["steps"].idxmax()
|
73
|
-
nominations[i].append(f"Most steps: {meta.loc[i].steps:.0f}")
|
74
81
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
lambda row: f"Most calories burnt for {row.kind}: {row.calories:.0f} kcal",
|
88
|
-
),
|
89
|
-
("steps", lambda row: f"Most steps for {row.kind}: {row.steps:.0f}"),
|
90
|
-
]:
|
91
|
-
if key in group.columns:
|
92
|
-
series = group[key]
|
93
|
-
if not pd.isna(series).all():
|
94
|
-
i = series.idxmax()
|
95
|
-
if not pd.isna(i):
|
96
|
-
nominations[i].append(text(meta.loc[i]))
|
82
|
+
def _nominate_activities_inner(
|
83
|
+
meta: pd.DataFrame, title_suffix: str, nominations: dict[int, list[str]]
|
84
|
+
) -> None:
|
85
|
+
ratings = [
|
86
|
+
("distance_km", "Greatest distance", "{:.1f} km"),
|
87
|
+
("elapsed_time", "Longest elapsed time", "{}"),
|
88
|
+
("average_speed_moving_kmh", "Highest average moving speed", "{:.1f} km/h"),
|
89
|
+
("average_speed_elapsed_kmh", "Highest average elapsed speed", "{:.1f} km/h"),
|
90
|
+
("calories", "Most calories burnt", "{:.0f}"),
|
91
|
+
("steps", "Most steps", "{:.0f}"),
|
92
|
+
("elevation_gain", "Largest elevation gain", "{:.0f} m"),
|
93
|
+
]
|
97
94
|
|
98
|
-
|
95
|
+
for variable, title, format_str in ratings:
|
96
|
+
if variable in meta.columns and not pd.isna(meta[variable]).all():
|
97
|
+
i = meta[variable].idxmax()
|
98
|
+
value = meta.loc[i, variable]
|
99
|
+
format_applied = format_str.format(value)
|
100
|
+
nominations[i].append(f"{title}{title_suffix}: {format_applied}")
|
99
101
|
|
100
102
|
|
101
103
|
def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
|
@@ -108,7 +110,6 @@ def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
|
|
108
110
|
df["hours"] = [
|
109
111
|
elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
|
110
112
|
]
|
111
|
-
del df["elapsed_time"]
|
112
113
|
return df
|
113
114
|
|
114
115
|
|