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.
Files changed (33) hide show
  1. geo_activity_playground/core/activities.py +12 -0
  2. geo_activity_playground/core/config.py +6 -2
  3. geo_activity_playground/core/enrichment.py +9 -0
  4. geo_activity_playground/core/meta_search.py +157 -0
  5. geo_activity_playground/core/summary_stats.py +30 -0
  6. geo_activity_playground/core/test_meta_search.py +100 -0
  7. geo_activity_playground/core/test_summary_stats.py +108 -0
  8. geo_activity_playground/webui/activity/controller.py +20 -0
  9. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -10
  10. geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -0
  11. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +17 -0
  12. geo_activity_playground/webui/app.py +27 -10
  13. geo_activity_playground/webui/eddington_blueprint.py +167 -58
  14. geo_activity_playground/webui/{equipment/controller.py → equipment_blueprint.py} +29 -42
  15. geo_activity_playground/webui/explorer/blueprint.py +4 -0
  16. geo_activity_playground/webui/heatmap/blueprint.py +10 -29
  17. geo_activity_playground/webui/heatmap/heatmap_controller.py +45 -103
  18. geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +5 -37
  19. geo_activity_playground/webui/search_blueprint.py +40 -71
  20. geo_activity_playground/webui/search_util.py +64 -0
  21. geo_activity_playground/webui/summary_blueprint.py +40 -39
  22. geo_activity_playground/webui/templates/eddington/index.html.j2 +73 -1
  23. geo_activity_playground/webui/{equipment/templates → templates}/equipment/index.html.j2 +3 -5
  24. geo_activity_playground/webui/templates/search/index.html.j2 +34 -87
  25. geo_activity_playground/webui/templates/search_form.html.j2 +116 -0
  26. geo_activity_playground/webui/templates/summary/index.html.j2 +5 -1
  27. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/METADATA +1 -1
  28. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/RECORD +31 -27
  29. geo_activity_playground/webui/equipment/__init__.py +0 -0
  30. geo_activity_playground/webui/equipment/blueprint.py +0 -16
  31. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/LICENSE +0 -0
  32. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.38.0.dist-info}/WHEEL +0 -0
  33. {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
- "kinds": kinds,
91
- "available_kinds": available_kinds,
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
- kind: str,
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 date_start is None and date_end is None:
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
- activity_ids_kind = set()
128
- for activity_id in activity_ids:
129
- activity = self._repository.get_activity_by_id(activity_id)
130
- if activity["kind"] == kind:
131
- activity_ids_kind.add(activity_id)
132
- if activity_ids_kind:
133
- with work_tracker(
134
- tile_count_cache_path.with_suffix(".json")
135
- ) as parsed_activities:
136
- if parsed_activities - activity_ids_kind:
137
- logger.warning(
138
- f"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
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
- tile_counts = np.zeros(tile_pixels, dtype=np.int32)
141
- parsed_activities.clear()
142
- for activity_id in activity_ids_kind:
143
- if activity_id in parsed_activities:
144
- continue
145
- parsed_activities.add(activity_id)
146
- time_series = self._repository.get_time_series(activity_id)
147
- for _, group in time_series.groupby("segment_id"):
148
- xy_pixels = (
149
- np.array(
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
- activity = self._repository.get_activity_by_id(activity_id)
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
- kinds_ids: list[int],
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
- available_kinds = sorted(self._repository.meta["kind"].unique())
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, kind_ids, date_start, date_end),
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
- <div class="row mb-3">
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
- <div class="col-sm-10">
16
- {% for kind in available_kinds %}
17
- <div class="form-check form-check-inline form-switch ">
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(repository: ActivityRepository) -> 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
- kinds_avail = repository.meta["kind"].unique()
27
- equipments_avail = repository.meta["equipment"].unique()
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
- equipments=request.args.getlist("equipment"),
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(repository: ActivityRepository, config: Config) -> 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(repository.meta)
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(repository.meta).items()
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
- subset = meta.loc[meta["consider_for_achievements"]]
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
- i = subset["elapsed_time"].idxmax()
65
- nominations[i].append(f"Longest elapsed time: {meta.loc[i].elapsed_time}")
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
- if "calories" in subset.columns and not pd.isna(subset["calories"]).all():
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
- for kind, group in meta.groupby("kind"):
76
- for key, text in [
77
- (
78
- "distance_km",
79
- lambda row: f"Greatest distance for {row.kind}: {row.distance_km:.1f} km",
80
- ),
81
- (
82
- "elapsed_time",
83
- lambda row: f"Longest elapsed time for {row.kind}: {row.elapsed_time}",
84
- ),
85
- (
86
- "calories",
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
- return nominations
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