geo-activity-playground 0.36.1__py3-none-any.whl → 0.37.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/meta_search.py +117 -0
- geo_activity_playground/core/summary_stats.py +30 -0
- geo_activity_playground/core/test_meta_search.py +91 -0
- geo_activity_playground/core/test_summary_stats.py +108 -0
- geo_activity_playground/webui/app.py +9 -5
- geo_activity_playground/webui/eddington_blueprint.py +8 -3
- geo_activity_playground/webui/{equipment/controller.py → equipment_blueprint.py} +19 -41
- geo_activity_playground/webui/heatmap/blueprint.py +7 -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 +6 -69
- geo_activity_playground/webui/search_util.py +31 -0
- geo_activity_playground/webui/summary_blueprint.py +15 -11
- geo_activity_playground/webui/templates/eddington/index.html.j2 +5 -0
- geo_activity_playground/webui/{equipment/templates → templates}/equipment/index.html.j2 +3 -5
- geo_activity_playground/webui/templates/search/index.html.j2 +30 -83
- geo_activity_playground/webui/templates/search_form.html.j2 +82 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +5 -1
- {geo_activity_playground-0.36.1.dist-info → geo_activity_playground-0.37.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.36.1.dist-info → geo_activity_playground-0.37.0.dist-info}/RECORD +23 -19
- geo_activity_playground/webui/equipment/__init__.py +0 -0
- geo_activity_playground/webui/equipment/blueprint.py +0 -16
- {geo_activity_playground-0.36.1.dist-info → geo_activity_playground-0.37.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.36.1.dist-info → geo_activity_playground-0.37.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.36.1.dist-info → geo_activity_playground-0.37.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) }}
|
9
|
+
</div>
|
42
10
|
|
43
11
|
<div class="row mb-3">
|
44
12
|
<div class="col">
|
@@ -8,6 +8,9 @@ from flask import request
|
|
8
8
|
from flask import Response
|
9
9
|
|
10
10
|
from ..core.activities import ActivityRepository
|
11
|
+
from geo_activity_playground.core.meta_search import apply_search_query
|
12
|
+
from geo_activity_playground.core.meta_search import SearchQuery
|
13
|
+
from geo_activity_playground.webui.search_util import search_query_from_form
|
11
14
|
|
12
15
|
|
13
16
|
def reduce_or(selections):
|
@@ -23,79 +26,13 @@ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
|
|
23
26
|
|
24
27
|
@blueprint.route("/")
|
25
28
|
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)
|
29
|
+
query = search_query_from_form(request.args)
|
30
|
+
activities = apply_search_query(repository.meta, query)
|
86
31
|
|
87
32
|
return render_template(
|
88
33
|
"search/index.html.j2",
|
89
34
|
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,
|
35
|
+
query=query.to_jinja(),
|
99
36
|
)
|
100
37
|
|
101
38
|
return blueprint
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import datetime
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import dateutil.parser
|
5
|
+
from werkzeug.datastructures import MultiDict
|
6
|
+
|
7
|
+
from geo_activity_playground.core.meta_search import SearchQuery
|
8
|
+
|
9
|
+
|
10
|
+
def search_query_from_form(args: MultiDict) -> SearchQuery:
|
11
|
+
query = SearchQuery(
|
12
|
+
equipment=args.getlist("equipment"),
|
13
|
+
kind=args.getlist("kind"),
|
14
|
+
name=args.get("name", None),
|
15
|
+
name_case_sensitive=_parse_bool(args.get("name_case_sensitive", "false")),
|
16
|
+
start_begin=_parse_date_or_none(args.get("start_begin", None)),
|
17
|
+
start_end=_parse_date_or_none(args.get("start_end", None)),
|
18
|
+
)
|
19
|
+
|
20
|
+
return query
|
21
|
+
|
22
|
+
|
23
|
+
def _parse_date_or_none(s: Optional[str]) -> Optional[datetime.date]:
|
24
|
+
if not s:
|
25
|
+
return None
|
26
|
+
else:
|
27
|
+
return dateutil.parser.parse(s).date()
|
28
|
+
|
29
|
+
|
30
|
+
def _parse_bool(s: str) -> bool:
|
31
|
+
return s == "true"
|
@@ -5,11 +5,14 @@ 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
|
13
16
|
|
14
17
|
|
15
18
|
def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
|
@@ -17,8 +20,11 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
|
|
17
20
|
|
18
21
|
@blueprint.route("/")
|
19
22
|
def index():
|
23
|
+
query = search_query_from_form(request.args)
|
24
|
+
activities = apply_search_query(repository.meta, query)
|
25
|
+
|
20
26
|
kind_scale = make_kind_scale(repository.meta, config)
|
21
|
-
df = embellished_activities(
|
27
|
+
df = embellished_activities(activities)
|
22
28
|
# df = df.loc[df["consider_for_achievements"]]
|
23
29
|
|
24
30
|
year_kind_total = (
|
@@ -46,8 +52,9 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
|
|
46
52
|
repository.get_time_series(activity_id)
|
47
53
|
),
|
48
54
|
)
|
49
|
-
for activity_id, reasons in nominate_activities(
|
55
|
+
for activity_id, reasons in nominate_activities(df).items()
|
50
56
|
],
|
57
|
+
query=query.to_jinja(),
|
51
58
|
)
|
52
59
|
|
53
60
|
return blueprint
|
@@ -56,20 +63,18 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
|
|
56
63
|
def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
|
57
64
|
nominations: dict[int, list[str]] = collections.defaultdict(list)
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
i = subset["distance_km"].idxmax()
|
66
|
+
i = meta["distance_km"].idxmax()
|
62
67
|
nominations[i].append(f"Greatest distance: {meta.loc[i].distance_km:.1f} km")
|
63
68
|
|
64
|
-
i =
|
69
|
+
i = meta["elapsed_time"].idxmax()
|
65
70
|
nominations[i].append(f"Longest elapsed time: {meta.loc[i].elapsed_time}")
|
66
71
|
|
67
|
-
if "calories" in
|
68
|
-
i =
|
72
|
+
if "calories" in meta.columns and not pd.isna(meta["calories"]).all():
|
73
|
+
i = meta["calories"].idxmax()
|
69
74
|
nominations[i].append(f"Most calories burnt: {meta.loc[i].calories:.0f} kcal")
|
70
75
|
|
71
|
-
if "steps" in
|
72
|
-
i =
|
76
|
+
if "steps" in meta.columns and not pd.isna(meta["steps"]).all():
|
77
|
+
i = meta["steps"].idxmax()
|
73
78
|
nominations[i].append(f"Most steps: {meta.loc[i].steps:.0f}")
|
74
79
|
|
75
80
|
for kind, group in meta.groupby("kind"):
|
@@ -108,7 +113,6 @@ def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
|
|
108
113
|
df["hours"] = [
|
109
114
|
elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
|
110
115
|
]
|
111
|
-
del df["elapsed_time"]
|
112
116
|
return df
|
113
117
|
|
114
118
|
|
@@ -1,8 +1,13 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
+
{% from "search_form.html.j2" import search_form %}
|
2
3
|
|
3
4
|
{% block container %}
|
4
5
|
<h1 class="mb-3">Eddington Number</h1>
|
5
6
|
|
7
|
+
<div class="mb-3">
|
8
|
+
{{ search_form(query, equipments_avail, kinds_avail) }}
|
9
|
+
</div>
|
10
|
+
|
6
11
|
<div class="row mb-3">
|
7
12
|
<div class="col-md-4">
|
8
13
|
<h2>Definition</h2>
|
@@ -13,7 +13,6 @@
|
|
13
13
|
<thead>
|
14
14
|
<tr>
|
15
15
|
<th>Equipment</th>
|
16
|
-
<th>Primarily used for</th>
|
17
16
|
<th style="text-align: right;">Distance / km</th>
|
18
17
|
<th style="text-align: right;">First use</th>
|
19
18
|
<th style="text-align: right;">Last use</th>
|
@@ -23,10 +22,9 @@
|
|
23
22
|
{% for equipment in equipment_summary %}
|
24
23
|
<tr>
|
25
24
|
<td>{{ equipment.equipment }}</td>
|
26
|
-
<td>{{ equipment.
|
27
|
-
<td style="text-align: right;">{{ equipment.
|
28
|
-
<td style="text-align: right;">{{ equipment.
|
29
|
-
<td style="text-align: right;">{{ equipment.last_use.date() }}</td>
|
25
|
+
<td style="text-align: right;">{{ equipment.total_distance_km }}</td>
|
26
|
+
<td style="text-align: right;">{{ equipment.first_use }}</td>
|
27
|
+
<td style="text-align: right;">{{ equipment.last_use }}</td>
|
30
28
|
</tr>
|
31
29
|
{% endfor %}
|
32
30
|
</tbody>
|
@@ -1,91 +1,38 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
+
{% from "search_form.html.j2" import search_form %}
|
2
3
|
|
3
4
|
{% block container %}
|
4
5
|
|
5
6
|
<h1 class="row mb-3">Activities Overview & Search</h1>
|
6
7
|
|
7
|
-
<div class="
|
8
|
-
|
9
|
-
<form>
|
10
|
-
<div class="mb-3">
|
11
|
-
<label for="name" class="form-label">Name</label>
|
12
|
-
<input type="text" class="form-control" id="name" name="name" value="{{ name }}">
|
13
|
-
<div class="form-check">
|
14
|
-
<input class="form-check-input" type="checkbox" name="name_exact" value="true" id="name_exact" {% if
|
15
|
-
name_exact %} checked {% endif %}>
|
16
|
-
<label class="form-check-label" for="name_exact">
|
17
|
-
Exact match
|
18
|
-
</label>
|
19
|
-
</div>
|
20
|
-
<div class="form-check">
|
21
|
-
<input class="form-check-input" type="checkbox" name="name_casing" value="true" id="name_casing" {%
|
22
|
-
if name_casing %} checked {% endif %}>
|
23
|
-
<label class="form-check-label" for="name_casing">
|
24
|
-
Case sensitive
|
25
|
-
</label>
|
26
|
-
</div>
|
27
|
-
</div>
|
28
|
-
|
29
|
-
<div class="mb-3">
|
30
|
-
<label for="begin" class="form-label">After</label>
|
31
|
-
<input type="text" class="form-control" id="begin" name="begin" value="{{ begin }}">
|
32
|
-
<label for="end" class="form-label">Until</label>
|
33
|
-
<input type="text" class="form-control" id="end" name="end" value="{{ end }}">
|
34
|
-
</div>
|
35
|
-
|
36
|
-
<div class="mb-3">
|
37
|
-
<label for="" class="form-label">Kind</label>
|
38
|
-
{% for kind in kinds_avail %}
|
39
|
-
<div class="form-check">
|
40
|
-
<input class="form-check-input" type="checkbox" name="kind" value="{{ kind }}" id="kind_{{ kind }}"
|
41
|
-
{% if kind in kinds %} checked {% endif %}>
|
42
|
-
<label class="form-check-label" for="kind_{{ kind }}">
|
43
|
-
{{ kind }}
|
44
|
-
</label>
|
45
|
-
</div>
|
46
|
-
{% endfor %}
|
47
|
-
</div>
|
48
|
-
|
49
|
-
<div class="mb-3">
|
50
|
-
<label for="" class="form-label">Equipment</label>
|
51
|
-
{% for equipment in equipments_avail %}
|
52
|
-
<div class="form-check">
|
53
|
-
<input class="form-check-input" type="checkbox" name="equipment" value="{{ equipment }}"
|
54
|
-
id="equipment_{{ equipment }}" {% if equipment in equipments %} checked {% endif %}>
|
55
|
-
<label class="form-check-label" for="equipment_{{ equipment }}">
|
56
|
-
{{ equipment }}
|
57
|
-
</label>
|
58
|
-
</div>
|
59
|
-
{% endfor %}
|
60
|
-
</div>
|
61
|
-
|
62
|
-
<button type="submit" class="btn btn-primary">Search</button>
|
63
|
-
</form>
|
64
|
-
</div>
|
65
|
-
|
66
|
-
<div class="col-md-10">
|
67
|
-
<table class="table table-sort table-arrows">
|
68
|
-
<thead>
|
69
|
-
<tr>
|
70
|
-
<th>Name</th>
|
71
|
-
<th>Start</th>
|
72
|
-
<th>Kind</th>
|
73
|
-
<th class="numeric-sort">Distance</th>
|
74
|
-
<th>Elapsed time</th>
|
75
|
-
</tr>
|
76
|
-
</thead>
|
77
|
-
<tbody>
|
78
|
-
{% for index, activity in activities %}
|
79
|
-
<tr>
|
80
|
-
<td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
|
81
|
-
<td>{{ activity['start']|dt }}</td>
|
82
|
-
<td>{{ activity['kind'] }}</td>
|
83
|
-
<td>{{ '%.1f' % activity["distance_km"] }} km</td>
|
84
|
-
<td>{{ activity.elapsed_time|td }}</td>
|
85
|
-
</tr>
|
86
|
-
{% endfor %}
|
87
|
-
</tbody>
|
88
|
-
</table>
|
89
|
-
</div>
|
8
|
+
<div class="mb-3">
|
9
|
+
{{ search_form(query, equipments_avail, kinds_avail) }}
|
90
10
|
</div>
|
11
|
+
|
12
|
+
<table class="table table-sort table-arrows">
|
13
|
+
<thead>
|
14
|
+
<tr>
|
15
|
+
<th>Name</th>
|
16
|
+
<th>Start</th>
|
17
|
+
<th>Kind</th>
|
18
|
+
<th class="numeric-sort">Distance</th>
|
19
|
+
<th>Elapsed time</th>
|
20
|
+
</tr>
|
21
|
+
</thead>
|
22
|
+
<tbody>
|
23
|
+
{% for index, activity in activities %}
|
24
|
+
<tr>
|
25
|
+
<td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
|
26
|
+
<td>
|
27
|
+
{% if activity['start'] is defined %}
|
28
|
+
{{ activity['start']|dt }}
|
29
|
+
{% endif %}
|
30
|
+
</td>
|
31
|
+
<td>{{ activity['kind'] }}</td>
|
32
|
+
<td>{{ '%.1f' % activity["distance_km"] }} km</td>
|
33
|
+
<td>{{ activity.elapsed_time|td }}</td>
|
34
|
+
</tr>
|
35
|
+
{% endfor %}
|
36
|
+
</tbody>
|
37
|
+
</table>
|
91
38
|
{% endblock %}
|