geo-activity-playground 0.22.0__py3-none-any.whl → 0.24.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 +1 -1
- geo_activity_playground/core/activities.py +16 -9
- geo_activity_playground/core/activity_parsers.py +17 -32
- geo_activity_playground/core/cache_migrations.py +24 -0
- geo_activity_playground/core/heatmap.py +21 -21
- geo_activity_playground/core/privacy_zones.py +16 -0
- geo_activity_playground/core/similarity.py +1 -1
- geo_activity_playground/core/test_time_conversion.py +37 -0
- geo_activity_playground/core/time_conversion.py +14 -0
- geo_activity_playground/explorer/tile_visits.py +44 -27
- geo_activity_playground/importers/__init__.py +0 -0
- geo_activity_playground/importers/directory.py +7 -2
- geo_activity_playground/importers/strava_api.py +6 -0
- geo_activity_playground/importers/strava_checkout.py +12 -3
- geo_activity_playground/webui/__init__.py +0 -0
- geo_activity_playground/webui/activity/__init__.py +0 -0
- geo_activity_playground/webui/activity/blueprint.py +58 -0
- geo_activity_playground/webui/{activity_controller.py → activity/controller.py} +128 -18
- geo_activity_playground/webui/{templates/activity-day.html.j2 → activity/templates/activity/day.html.j2} +14 -2
- geo_activity_playground/webui/{templates/activity-name.html.j2 → activity/templates/activity/name.html.j2} +1 -1
- geo_activity_playground/webui/{templates/activity.html.j2 → activity/templates/activity/show.html.j2} +9 -4
- geo_activity_playground/webui/app.py +68 -281
- geo_activity_playground/webui/calendar/__init__.py +0 -0
- geo_activity_playground/webui/calendar/blueprint.py +26 -0
- geo_activity_playground/webui/{calendar_controller.py → calendar/controller.py} +5 -5
- geo_activity_playground/webui/{templates/calendar.html.j2 → calendar/templates/calendar/index.html.j2} +3 -2
- geo_activity_playground/webui/{templates/calendar-month.html.j2 → calendar/templates/calendar/month.html.j2} +2 -2
- geo_activity_playground/webui/eddington/__init__.py +0 -0
- geo_activity_playground/webui/eddington/blueprint.py +19 -0
- geo_activity_playground/webui/{eddington_controller.py → eddington/controller.py} +14 -6
- geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +56 -0
- geo_activity_playground/webui/entry_controller.py +4 -2
- geo_activity_playground/webui/equipment/__init__.py +0 -0
- geo_activity_playground/webui/equipment/blueprint.py +19 -0
- geo_activity_playground/webui/{equipment_controller.py → equipment/controller.py} +5 -3
- geo_activity_playground/webui/explorer/__init__.py +0 -0
- geo_activity_playground/webui/explorer/blueprint.py +54 -0
- geo_activity_playground/webui/{explorer_controller.py → explorer/controller.py} +6 -2
- geo_activity_playground/webui/{templates/explorer.html.j2 → explorer/templates/explorer/index.html.j2} +2 -2
- geo_activity_playground/webui/heatmap/__init__.py +0 -0
- geo_activity_playground/webui/heatmap/blueprint.py +41 -0
- geo_activity_playground/webui/{heatmap_controller.py → heatmap/heatmap_controller.py} +36 -13
- geo_activity_playground/webui/{templates/heatmap.html.j2 → heatmap/templates/heatmap/index.html.j2} +17 -2
- geo_activity_playground/webui/search_controller.py +1 -9
- geo_activity_playground/webui/square_planner/__init__.py +0 -0
- geo_activity_playground/webui/square_planner/blueprint.py +38 -0
- geo_activity_playground/webui/summary/__init__.py +0 -0
- geo_activity_playground/webui/summary/blueprint.py +16 -0
- geo_activity_playground/webui/summary/controller.py +268 -0
- geo_activity_playground/webui/summary/templates/summary/index.html.j2 +135 -0
- geo_activity_playground/webui/templates/{index.html.j2 → home.html.j2} +1 -1
- geo_activity_playground/webui/templates/page.html.j2 +32 -19
- geo_activity_playground/webui/templates/search.html.j2 +1 -1
- geo_activity_playground/webui/tile/__init__.py +0 -0
- geo_activity_playground/webui/tile/blueprint.py +31 -0
- geo_activity_playground/webui/upload/__init__.py +0 -0
- geo_activity_playground/webui/upload/blueprint.py +28 -0
- geo_activity_playground/webui/{upload_controller.py → upload/controller.py} +15 -6
- geo_activity_playground/webui/{templates/upload.html.j2 → upload/templates/upload/index.html.j2} +12 -11
- {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/METADATA +2 -1
- geo_activity_playground-0.24.0.dist-info/RECORD +95 -0
- geo_activity_playground/webui/config_controller.py +0 -12
- geo_activity_playground/webui/locations_controller.py +0 -28
- geo_activity_playground/webui/summary_controller.py +0 -60
- geo_activity_playground/webui/templates/config.html.j2 +0 -24
- geo_activity_playground/webui/templates/eddington.html.j2 +0 -18
- geo_activity_playground/webui/templates/locations.html.j2 +0 -38
- geo_activity_playground/webui/templates/summary.html.j2 +0 -21
- geo_activity_playground-0.22.0.dist-info/RECORD +0 -74
- /geo_activity_playground/webui/{templates/activity-lines.html.j2 → activity/templates/activity/lines.html.j2} +0 -0
- /geo_activity_playground/webui/{templates/equipment.html.j2 → equipment/templates/equipment/index.html.j2} +0 -0
- /geo_activity_playground/webui/{square_planner_controller.py → square_planner/controller.py} +0 -0
- /geo_activity_playground/webui/{templates/square-planner.html.j2 → square_planner/templates/square_planner/index.html.j2} +0 -0
- /geo_activity_playground/webui/{tile_controller.py → tile/controller.py} +0 -0
- {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,8 @@
|
|
1
1
|
import altair as alt
|
2
2
|
import pandas as pd
|
3
3
|
|
4
|
-
from ..core.config import get_config
|
5
4
|
from geo_activity_playground.core.activities import ActivityRepository
|
5
|
+
from geo_activity_playground.core.config import get_config
|
6
6
|
|
7
7
|
|
8
8
|
class EquipmentController:
|
@@ -18,7 +18,8 @@ class EquipmentController:
|
|
18
18
|
"time": group["start"],
|
19
19
|
"total_distance_km": group["distance_km"].cumsum(),
|
20
20
|
}
|
21
|
-
)
|
21
|
+
),
|
22
|
+
include_groups=False,
|
22
23
|
)
|
23
24
|
.reset_index()
|
24
25
|
)
|
@@ -52,7 +53,8 @@ class EquipmentController:
|
|
52
53
|
"last_use": group["start"].iloc[-1],
|
53
54
|
},
|
54
55
|
index=[0],
|
55
|
-
)
|
56
|
+
),
|
57
|
+
include_groups=False,
|
56
58
|
)
|
57
59
|
.reset_index()
|
58
60
|
.sort_values("last_use", ascending=False)
|
File without changes
|
@@ -0,0 +1,54 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
from flask import Response
|
4
|
+
|
5
|
+
from ...core.activities import ActivityRepository
|
6
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
7
|
+
from .controller import ExplorerController
|
8
|
+
|
9
|
+
|
10
|
+
def make_explorer_blueprint(
|
11
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
12
|
+
) -> Blueprint:
|
13
|
+
explorer_controller = ExplorerController(repository, tile_visit_accessor)
|
14
|
+
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
15
|
+
|
16
|
+
@blueprint.route("/<zoom>")
|
17
|
+
def map(zoom: str):
|
18
|
+
return render_template(
|
19
|
+
"explorer/index.html.j2", **explorer_controller.render(int(zoom))
|
20
|
+
)
|
21
|
+
|
22
|
+
@blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/explored.<suffix>")
|
23
|
+
def download(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
|
24
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
25
|
+
return Response(
|
26
|
+
explorer_controller.export_explored_tiles(
|
27
|
+
int(zoom),
|
28
|
+
float(north),
|
29
|
+
float(east),
|
30
|
+
float(south),
|
31
|
+
float(west),
|
32
|
+
suffix,
|
33
|
+
),
|
34
|
+
mimetype=mimetypes[suffix],
|
35
|
+
headers={"Content-disposition": "attachment"},
|
36
|
+
)
|
37
|
+
|
38
|
+
@blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/missing.<suffix>")
|
39
|
+
def missing(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
|
40
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
41
|
+
return Response(
|
42
|
+
explorer_controller.export_missing_tiles(
|
43
|
+
int(zoom),
|
44
|
+
float(north),
|
45
|
+
float(east),
|
46
|
+
float(south),
|
47
|
+
float(west),
|
48
|
+
suffix,
|
49
|
+
),
|
50
|
+
mimetype=mimetypes[suffix],
|
51
|
+
headers={"Content-disposition": "attachment"},
|
52
|
+
)
|
53
|
+
|
54
|
+
return blueprint
|
@@ -112,8 +112,12 @@ def get_three_color_tiles(
|
|
112
112
|
cmap_last = matplotlib.colormaps["plasma"]
|
113
113
|
tile_dict = {}
|
114
114
|
for tile, tile_data in tile_visits.items():
|
115
|
-
|
116
|
-
|
115
|
+
if not pd.isna(tile_data["first_time"]):
|
116
|
+
first_age_days = (today - tile_data["first_time"].date()).days
|
117
|
+
last_age_days = (today - tile_data["last_time"].date()).days
|
118
|
+
else:
|
119
|
+
first_age_days = 10000
|
120
|
+
last_age_days = 10000
|
117
121
|
tile_dict[tile] = {
|
118
122
|
"first_activity_id": str(tile_data["first_id"]),
|
119
123
|
"first_activity_name": repository.get_activity_by_id(tile_data["first_id"])[
|
@@ -10,8 +10,8 @@
|
|
10
10
|
explored.square_size }}².
|
11
11
|
</p>
|
12
12
|
<p>Open the <a
|
13
|
-
href="
|
14
|
-
Planner</a> to plan the next
|
13
|
+
href="{{ url_for('square_planner.index', zoom=zoom, x=explored.square_x, y=explored.square_y, size=explored.square_size) }}">Square
|
14
|
+
Planner</a> to plan the next extension.</p>
|
15
15
|
</div>
|
16
16
|
</div>
|
17
17
|
|
File without changes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
from flask import request
|
4
|
+
from flask import Response
|
5
|
+
|
6
|
+
from ...core.activities import ActivityRepository
|
7
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
8
|
+
from .heatmap_controller import HeatmapController
|
9
|
+
|
10
|
+
|
11
|
+
def make_heatmap_blueprint(
|
12
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
13
|
+
) -> Blueprint:
|
14
|
+
heatmap_controller = HeatmapController(repository, tile_visit_accessor)
|
15
|
+
blueprint = Blueprint("heatmap", __name__, template_folder="templates")
|
16
|
+
|
17
|
+
@blueprint.route("/")
|
18
|
+
def index():
|
19
|
+
return render_template(
|
20
|
+
"heatmap/index.html.j2",
|
21
|
+
**heatmap_controller.render(request.args.getlist("kind"))
|
22
|
+
)
|
23
|
+
|
24
|
+
@blueprint.route("/tile/<z>/<x>/<y>/<kinds>.png")
|
25
|
+
def tile(x: str, y: str, z: str, kinds: str):
|
26
|
+
return Response(
|
27
|
+
heatmap_controller.render_tile(int(x), int(y), int(z), kinds.split(";")),
|
28
|
+
mimetype="image/png",
|
29
|
+
)
|
30
|
+
|
31
|
+
@blueprint.route("/download/<north>/<east>/<south>/<west>/<kinds>")
|
32
|
+
def download(north: str, east: str, south: str, west: str, kinds: str):
|
33
|
+
return Response(
|
34
|
+
heatmap_controller.download_heatmap(
|
35
|
+
float(north), float(east), float(south), float(west), kinds.split(";")
|
36
|
+
),
|
37
|
+
mimetype="image/png",
|
38
|
+
headers={"Content-disposition": 'attachment; filename="heatmap.png"'},
|
39
|
+
)
|
40
|
+
|
41
|
+
return blueprint
|
@@ -1,11 +1,9 @@
|
|
1
1
|
import io
|
2
2
|
import logging
|
3
3
|
import pathlib
|
4
|
-
import pickle
|
5
4
|
|
6
5
|
import matplotlib.pylab as pl
|
7
6
|
import numpy as np
|
8
|
-
import pandas as pd
|
9
7
|
from PIL import Image
|
10
8
|
from PIL import ImageDraw
|
11
9
|
|
@@ -17,7 +15,7 @@ from geo_activity_playground.core.tasks import work_tracker
|
|
17
15
|
from geo_activity_playground.core.tiles import get_tile
|
18
16
|
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
19
17
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
20
|
-
from geo_activity_playground.webui.
|
18
|
+
from geo_activity_playground.webui.explorer.controller import (
|
21
19
|
bounding_box_for_biggest_cluster,
|
22
20
|
)
|
23
21
|
|
@@ -38,15 +36,22 @@ class HeatmapController:
|
|
38
36
|
self.tile_histories = self._tile_visit_accessor.histories
|
39
37
|
self.tile_evolution_states = self._tile_visit_accessor.states
|
40
38
|
self.tile_visits = self._tile_visit_accessor.visits
|
39
|
+
self.activities_per_tile = self._tile_visit_accessor.activities_per_tile
|
41
40
|
|
42
|
-
def render(self) -> dict:
|
41
|
+
def render(self, kinds: list[str] = []) -> dict:
|
43
42
|
zoom = 14
|
44
43
|
tiles = self.tile_histories[zoom]
|
45
|
-
medians = tiles.median()
|
44
|
+
medians = tiles.median(skipna=True)
|
46
45
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
47
46
|
medians["tile_x"], medians["tile_y"], zoom
|
48
47
|
)
|
49
48
|
cluster_state = self.tile_evolution_states[zoom]
|
49
|
+
|
50
|
+
available_kinds = sorted(self._repository.meta["kind"].unique())
|
51
|
+
|
52
|
+
if not kinds:
|
53
|
+
kinds = available_kinds
|
54
|
+
|
50
55
|
return {
|
51
56
|
"center": {
|
52
57
|
"latitude": median_lat,
|
@@ -58,18 +63,21 @@ class HeatmapController:
|
|
58
63
|
if len(cluster_state.memberships) > 0
|
59
64
|
else {}
|
60
65
|
),
|
61
|
-
}
|
66
|
+
},
|
67
|
+
"kinds": kinds,
|
68
|
+
"available_kinds": available_kinds,
|
69
|
+
"kinds_str": ";".join(kinds),
|
62
70
|
}
|
63
71
|
|
64
|
-
def
|
72
|
+
def _get_counts(self, x: int, y: int, z: int, kind: str) -> np.ndarray:
|
65
73
|
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
66
|
-
tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
|
74
|
+
tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy")
|
67
75
|
if tile_count_cache_path.exists():
|
68
76
|
tile_counts = np.load(tile_count_cache_path)
|
69
77
|
else:
|
70
78
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
71
79
|
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
72
|
-
activity_ids = self.
|
80
|
+
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
73
81
|
if activity_ids:
|
74
82
|
with work_tracker(
|
75
83
|
tile_count_cache_path.with_suffix(".json")
|
@@ -78,6 +86,9 @@ class HeatmapController:
|
|
78
86
|
if activity_id in parsed_activities:
|
79
87
|
continue
|
80
88
|
parsed_activities.add(activity_id)
|
89
|
+
activity = self._repository.get_activity_by_id(activity_id)
|
90
|
+
if activity["kind"] != kind:
|
91
|
+
continue
|
81
92
|
time_series = self._repository.get_time_series(activity_id)
|
82
93
|
for _, group in time_series.groupby("segment_id"):
|
83
94
|
xy_pixels = (
|
@@ -93,6 +104,16 @@ class HeatmapController:
|
|
93
104
|
aim = np.array(im)
|
94
105
|
tile_counts += aim
|
95
106
|
np.save(tile_count_cache_path, tile_counts)
|
107
|
+
return tile_counts
|
108
|
+
|
109
|
+
def _render_tile_image(
|
110
|
+
self, x: int, y: int, z: int, kinds: list[str]
|
111
|
+
) -> np.ndarray:
|
112
|
+
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
113
|
+
tile_counts = np.zeros(tile_pixels)
|
114
|
+
for kind in kinds:
|
115
|
+
tile_counts += self._get_counts(x, y, z, kind)
|
116
|
+
|
96
117
|
tile_counts = np.sqrt(tile_counts) / 5
|
97
118
|
tile_counts[tile_counts > 1.0] = 1.0
|
98
119
|
|
@@ -109,12 +130,14 @@ class HeatmapController:
|
|
109
130
|
] + data_color[:, :, c]
|
110
131
|
return map_tile
|
111
132
|
|
112
|
-
def render_tile(self, x: int, y: int, z: int) -> bytes:
|
133
|
+
def render_tile(self, x: int, y: int, z: int, kinds: list[str]) -> bytes:
|
113
134
|
f = io.BytesIO()
|
114
|
-
pl.imsave(f, self._render_tile_image(x, y, z), format="png")
|
135
|
+
pl.imsave(f, self._render_tile_image(x, y, z, kinds), format="png")
|
115
136
|
return bytes(f.getbuffer())
|
116
137
|
|
117
|
-
def download_heatmap(
|
138
|
+
def download_heatmap(
|
139
|
+
self, north: float, east: float, south: float, west: float, kinds: list[str]
|
140
|
+
) -> bytes:
|
118
141
|
geo_bounds = GeoBounds(south, west, north, east)
|
119
142
|
tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
|
120
143
|
|
@@ -130,7 +153,7 @@ class HeatmapController:
|
|
130
153
|
i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
|
131
154
|
j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
|
132
155
|
:,
|
133
|
-
] = self._render_tile_image(x, y, tile_bounds.zoom)
|
156
|
+
] = self._render_tile_image(x, y, tile_bounds.zoom, kinds)
|
134
157
|
|
135
158
|
f = io.BytesIO()
|
136
159
|
pl.imsave(f, background, format="png")
|
geo_activity_playground/webui/{templates/heatmap.html.j2 → heatmap/templates/heatmap/index.html.j2}
RENAMED
@@ -7,6 +7,21 @@
|
|
7
7
|
</div>
|
8
8
|
</div>
|
9
9
|
|
10
|
+
<div class="row mb-3">
|
11
|
+
<div class="col">
|
12
|
+
<form action="" method="GET">
|
13
|
+
{% for kind in available_kinds %}
|
14
|
+
<div class="form-check form-check-inline form-switch">
|
15
|
+
<input class="form-check-input" type="checkbox" role="switch" id="{{ kind }}" name="kind"
|
16
|
+
value="{{ kind }}" {{ 'checked' if kind in kinds else '' }} />
|
17
|
+
<label class="form-check-label" for="{{ kind }}">{{ kind }}</label>
|
18
|
+
</div>
|
19
|
+
{% endfor %}
|
20
|
+
<button type="submit" class="btn btn-primary">Show selected kinds</button>
|
21
|
+
</form>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
10
25
|
<div class="row mb-3">
|
11
26
|
<div class="col">
|
12
27
|
<div id="heatmap" style="height: 800px;"></div>
|
@@ -18,7 +33,7 @@
|
|
18
33
|
center: [{{ center.latitude }}, {{ center.longitude }}],
|
19
34
|
zoom: 12
|
20
35
|
});
|
21
|
-
L.tileLayer('/heatmap/tile/{z}/{x}/{y}.png', {
|
36
|
+
L.tileLayer('/heatmap/tile/{z}/{x}/{y}/{{ kinds_str }}.png', {
|
22
37
|
maxZoom: 19,
|
23
38
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
24
39
|
}).addTo(map)
|
@@ -32,7 +47,7 @@
|
|
32
47
|
function downloadAs() {
|
33
48
|
bounds = map.getBounds()
|
34
49
|
window.location.href =
|
35
|
-
`/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}`
|
50
|
+
`/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/{{ kinds_str }}`
|
36
51
|
}
|
37
52
|
</script>
|
38
53
|
</div>
|
@@ -14,14 +14,6 @@ class SearchController:
|
|
14
14
|
activities = []
|
15
15
|
for _, row in self._repository.meta.iterrows():
|
16
16
|
if name in row["name"]:
|
17
|
-
activities.append(
|
18
|
-
{
|
19
|
-
"name": row["name"],
|
20
|
-
"start": row["start"].isoformat(),
|
21
|
-
"kind": row["kind"],
|
22
|
-
"distance_km": row["distance_km"],
|
23
|
-
"elapsed_time": row["elapsed_time"],
|
24
|
-
}
|
25
|
-
)
|
17
|
+
activities.append(row)
|
26
18
|
|
27
19
|
return {"activities": activities}
|
File without changes
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
from flask import Response
|
4
|
+
|
5
|
+
from ...core.activities import ActivityRepository
|
6
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
7
|
+
from .controller import SquarePlannerController
|
8
|
+
|
9
|
+
|
10
|
+
def make_square_planner_blueprint(
|
11
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
12
|
+
) -> Blueprint:
|
13
|
+
blueprint = Blueprint("square_planner", __name__, template_folder="templates")
|
14
|
+
controller = SquarePlannerController(repository, tile_visit_accessor)
|
15
|
+
|
16
|
+
@blueprint.route("/<zoom>/<x>/<y>/<size>")
|
17
|
+
def index(zoom, x, y, size):
|
18
|
+
return render_template(
|
19
|
+
"square_planner/index.html.j2",
|
20
|
+
**controller.action_planner(int(zoom), int(x), int(y), int(size))
|
21
|
+
)
|
22
|
+
|
23
|
+
@blueprint.route("/<zoom>/<x>/<y>/<size>/missing.<suffix>")
|
24
|
+
def square_planner_missing(zoom, x, y, size, suffix: str):
|
25
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
26
|
+
return Response(
|
27
|
+
controller.export_missing_tiles(
|
28
|
+
int(zoom),
|
29
|
+
int(x),
|
30
|
+
int(y),
|
31
|
+
int(size),
|
32
|
+
suffix,
|
33
|
+
),
|
34
|
+
mimetype=mimetypes[suffix],
|
35
|
+
headers={"Content-disposition": "attachment"},
|
36
|
+
)
|
37
|
+
|
38
|
+
return blueprint
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
|
4
|
+
from ...core.activities import ActivityRepository
|
5
|
+
from .controller import SummaryController
|
6
|
+
|
7
|
+
|
8
|
+
def make_summary_blueprint(repository: ActivityRepository) -> Blueprint:
|
9
|
+
summary_controller = SummaryController(repository)
|
10
|
+
blueprint = Blueprint("summary", __name__, template_folder="templates")
|
11
|
+
|
12
|
+
@blueprint.route("/")
|
13
|
+
def index():
|
14
|
+
return render_template("summary/index.html.j2", **summary_controller.render())
|
15
|
+
|
16
|
+
return blueprint
|
@@ -0,0 +1,268 @@
|
|
1
|
+
import collections
|
2
|
+
import datetime
|
3
|
+
import functools
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
import altair as alt
|
7
|
+
import pandas as pd
|
8
|
+
|
9
|
+
from geo_activity_playground.core.activities import ActivityRepository
|
10
|
+
from geo_activity_playground.core.activities import make_geojson_from_time_series
|
11
|
+
|
12
|
+
|
13
|
+
class SummaryController:
|
14
|
+
def __init__(self, repository: ActivityRepository) -> None:
|
15
|
+
self._repository = repository
|
16
|
+
|
17
|
+
@functools.cache
|
18
|
+
def render(self) -> dict:
|
19
|
+
df = embellished_activities(self._repository.meta)
|
20
|
+
df = df.loc[df["consider_for_achievements"]]
|
21
|
+
|
22
|
+
year_kind_total = (
|
23
|
+
df[["year", "kind", "distance_km", "hours"]]
|
24
|
+
.groupby(["year", "kind"])
|
25
|
+
.sum()
|
26
|
+
.reset_index()
|
27
|
+
)
|
28
|
+
|
29
|
+
return {
|
30
|
+
"plot_distance_heatmap": plot_distance_heatmap(df),
|
31
|
+
"plot_monthly_distance": plot_monthly_distance(df),
|
32
|
+
"plot_yearly_distance": plot_yearly_distance(year_kind_total),
|
33
|
+
"plot_year_cumulative": plot_year_cumulative(df),
|
34
|
+
"tabulate_year_kind_mean": tabulate_year_kind_mean(df)
|
35
|
+
.reset_index()
|
36
|
+
.to_dict(orient="split"),
|
37
|
+
"plot_weekly_distance": plot_weekly_distance(df),
|
38
|
+
"nominations": [
|
39
|
+
(
|
40
|
+
self._repository.get_activity_by_id(activity_id),
|
41
|
+
reasons,
|
42
|
+
make_geojson_from_time_series(
|
43
|
+
self._repository.get_time_series(activity_id)
|
44
|
+
),
|
45
|
+
)
|
46
|
+
for activity_id, reasons in nominate_activities(
|
47
|
+
self._repository.meta
|
48
|
+
).items()
|
49
|
+
],
|
50
|
+
}
|
51
|
+
|
52
|
+
|
53
|
+
def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
|
54
|
+
nominations: dict[int, list[str]] = collections.defaultdict(list)
|
55
|
+
|
56
|
+
subset = meta.loc[meta["consider_for_achievements"]]
|
57
|
+
|
58
|
+
i = subset["distance_km"].idxmax()
|
59
|
+
nominations[i].append(f"Greatest distance: {meta.loc[i].distance_km:.1f} km")
|
60
|
+
|
61
|
+
i = subset["elapsed_time"].idxmax()
|
62
|
+
nominations[i].append(f"Longest elapsed time: {meta.loc[i].elapsed_time}")
|
63
|
+
|
64
|
+
i = subset["calories"].idxmax()
|
65
|
+
nominations[i].append(f"Most calories burnt: {meta.loc[i].calories:.0f} kcal")
|
66
|
+
|
67
|
+
i = subset["steps"].idxmax()
|
68
|
+
nominations[i].append(f"Most steps: {meta.loc[i].steps:.0f}")
|
69
|
+
|
70
|
+
for kind, group in meta.groupby("kind"):
|
71
|
+
for key, text in [
|
72
|
+
(
|
73
|
+
"distance_km",
|
74
|
+
lambda row: f"Greatest distance for {row.kind}: {row.distance_km:.1f} km",
|
75
|
+
),
|
76
|
+
(
|
77
|
+
"elapsed_time",
|
78
|
+
lambda row: f"Longest elapsed time for {row.kind}: {row.elapsed_time}",
|
79
|
+
),
|
80
|
+
(
|
81
|
+
"calories",
|
82
|
+
lambda row: f"Most calories burnt for {row.kind}: {row.calories:.0f} kcal",
|
83
|
+
),
|
84
|
+
("steps", lambda row: f"Most steps for {row.kind}: {row.steps:.0f}"),
|
85
|
+
]:
|
86
|
+
series = group[key]
|
87
|
+
i = series.idxmax()
|
88
|
+
if not pd.isna(i):
|
89
|
+
nominations[i].append(text(meta.loc[i]))
|
90
|
+
|
91
|
+
return nominations
|
92
|
+
|
93
|
+
|
94
|
+
def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
|
95
|
+
df = meta.copy()
|
96
|
+
df["year"] = [start.year for start in df["start"]]
|
97
|
+
df["month"] = [start.month for start in df["start"]]
|
98
|
+
df["day"] = [start.day for start in df["start"]]
|
99
|
+
df["week"] = [start.isocalendar().week for start in df["start"]]
|
100
|
+
df["hours"] = [
|
101
|
+
elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
|
102
|
+
]
|
103
|
+
del df["elapsed_time"]
|
104
|
+
return df
|
105
|
+
|
106
|
+
|
107
|
+
def plot_distance_heatmap(meta: pd.DataFrame) -> str:
|
108
|
+
return (
|
109
|
+
alt.Chart(
|
110
|
+
meta.loc[
|
111
|
+
(
|
112
|
+
meta["start"]
|
113
|
+
>= pd.to_datetime(
|
114
|
+
datetime.datetime.now() - datetime.timedelta(days=2 * 365)
|
115
|
+
)
|
116
|
+
)
|
117
|
+
],
|
118
|
+
title="Daily Distance Heatmap",
|
119
|
+
)
|
120
|
+
.mark_rect()
|
121
|
+
.encode(
|
122
|
+
alt.X("date(start):O", title="Day of month"),
|
123
|
+
alt.Y(
|
124
|
+
"yearmonth(start):O",
|
125
|
+
scale=alt.Scale(reverse=True),
|
126
|
+
title="Year and month",
|
127
|
+
),
|
128
|
+
alt.Color("sum(distance_km)", scale=alt.Scale(scheme="viridis")),
|
129
|
+
[
|
130
|
+
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
131
|
+
alt.Tooltip(
|
132
|
+
"sum(distance_km)", format=".1f", title="Total distance / km"
|
133
|
+
),
|
134
|
+
alt.Tooltip("count(distance_km)", title="Number of activities"),
|
135
|
+
],
|
136
|
+
)
|
137
|
+
.to_json(format="vega")
|
138
|
+
)
|
139
|
+
|
140
|
+
|
141
|
+
def plot_monthly_distance(meta: pd.DataFrame) -> str:
|
142
|
+
return (
|
143
|
+
alt.Chart(
|
144
|
+
meta.loc[
|
145
|
+
(
|
146
|
+
meta["start"]
|
147
|
+
>= pd.to_datetime(
|
148
|
+
datetime.datetime.now() - datetime.timedelta(days=2 * 365)
|
149
|
+
)
|
150
|
+
)
|
151
|
+
],
|
152
|
+
title="Monthly Distance",
|
153
|
+
)
|
154
|
+
.mark_bar()
|
155
|
+
.encode(
|
156
|
+
alt.X("month(start)", title="Month"),
|
157
|
+
alt.Y("sum(distance_km)", title="Distance / km"),
|
158
|
+
alt.Color("kind", scale=alt.Scale(scheme="category10"), title="Kind"),
|
159
|
+
alt.Column("year(start):O", title="Year"),
|
160
|
+
)
|
161
|
+
.resolve_axis(x="independent")
|
162
|
+
.to_json(format="vega")
|
163
|
+
)
|
164
|
+
|
165
|
+
|
166
|
+
def plot_yearly_distance(year_kind_total: pd.DataFrame) -> str:
|
167
|
+
return (
|
168
|
+
alt.Chart(year_kind_total, title="Total Distance per Year")
|
169
|
+
.mark_bar()
|
170
|
+
.encode(
|
171
|
+
alt.X("year:O", title="Year"),
|
172
|
+
alt.Y("distance_km", title="Distance / km"),
|
173
|
+
alt.Color("kind", title="Kind"),
|
174
|
+
[
|
175
|
+
alt.Tooltip("year:O", title="Year"),
|
176
|
+
alt.Tooltip("kind", title="Kind"),
|
177
|
+
alt.Tooltip("distance_km", title="Distance / km"),
|
178
|
+
],
|
179
|
+
)
|
180
|
+
.to_json(format="vega")
|
181
|
+
)
|
182
|
+
|
183
|
+
|
184
|
+
def plot_year_cumulative(df: pd.DataFrame) -> str:
|
185
|
+
year_cumulative = (
|
186
|
+
df[["year", "week", "distance_km"]]
|
187
|
+
.groupby("year")
|
188
|
+
.apply(
|
189
|
+
lambda group: pd.DataFrame(
|
190
|
+
{"week": group["week"], "distance_km": group["distance_km"].cumsum()}
|
191
|
+
),
|
192
|
+
include_groups=False,
|
193
|
+
)
|
194
|
+
.reset_index()
|
195
|
+
)
|
196
|
+
|
197
|
+
return (
|
198
|
+
alt.Chart(year_cumulative, width=500, title="Cumultative Distance per Year")
|
199
|
+
.mark_line()
|
200
|
+
.encode(
|
201
|
+
alt.X("week", title="Week"),
|
202
|
+
alt.Y("distance_km", title="Distance / km"),
|
203
|
+
alt.Color("year:N", title="Year"),
|
204
|
+
[
|
205
|
+
alt.Tooltip("week", title="Week"),
|
206
|
+
alt.Tooltip("year:N", title="Year"),
|
207
|
+
alt.Tooltip("distance_km", title="Distance / km"),
|
208
|
+
],
|
209
|
+
)
|
210
|
+
.interactive()
|
211
|
+
.to_json(format="vega")
|
212
|
+
)
|
213
|
+
|
214
|
+
|
215
|
+
def tabulate_year_kind_mean(df: pd.DataFrame) -> pd.DataFrame:
|
216
|
+
year_kind_mean = (
|
217
|
+
df[["year", "kind", "distance_km", "hours"]]
|
218
|
+
.groupby(["year", "kind"])
|
219
|
+
.mean()
|
220
|
+
.reset_index()
|
221
|
+
)
|
222
|
+
|
223
|
+
year_kind_mean_distance = year_kind_mean.pivot(
|
224
|
+
index="year", columns="kind", values="distance_km"
|
225
|
+
)
|
226
|
+
|
227
|
+
return year_kind_mean_distance
|
228
|
+
|
229
|
+
|
230
|
+
def plot_weekly_distance(df: pd.DataFrame) -> str:
|
231
|
+
week_kind_total_distance = (
|
232
|
+
df[["year", "week", "kind", "distance_km"]]
|
233
|
+
.groupby(["year", "week", "kind"])
|
234
|
+
.sum()
|
235
|
+
.reset_index()
|
236
|
+
)
|
237
|
+
week_kind_total_distance["year_week"] = [
|
238
|
+
f"{year}-{week:02d}"
|
239
|
+
for year, week in zip(
|
240
|
+
week_kind_total_distance["year"], week_kind_total_distance["week"]
|
241
|
+
)
|
242
|
+
]
|
243
|
+
|
244
|
+
last_year = week_kind_total_distance["year"].iloc[-1]
|
245
|
+
last_week = week_kind_total_distance["week"].iloc[-1]
|
246
|
+
|
247
|
+
return (
|
248
|
+
alt.Chart(
|
249
|
+
week_kind_total_distance.loc[
|
250
|
+
(week_kind_total_distance["year"] == last_year)
|
251
|
+
| (week_kind_total_distance["year"] == last_year - 1)
|
252
|
+
& (week_kind_total_distance["week"] >= last_week)
|
253
|
+
],
|
254
|
+
title="Weekly Distance",
|
255
|
+
)
|
256
|
+
.mark_bar()
|
257
|
+
.encode(
|
258
|
+
alt.X("year_week", title="Year and Week"),
|
259
|
+
alt.Y("distance_km", title="Distance / km"),
|
260
|
+
alt.Color("kind", title="Kind"),
|
261
|
+
[
|
262
|
+
alt.Tooltip("year_week", title="Year and Week"),
|
263
|
+
alt.Tooltip("kind", title="Kind"),
|
264
|
+
alt.Tooltip("distance_km", title="Distance / km"),
|
265
|
+
],
|
266
|
+
)
|
267
|
+
.to_json(format="vega")
|
268
|
+
)
|