geo-activity-playground 0.35.0__py3-none-any.whl → 0.36.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 +12 -0
- geo_activity_playground/core/activities.py +21 -13
- geo_activity_playground/core/raster_map.py +246 -0
- geo_activity_playground/core/tiles.py +6 -50
- geo_activity_playground/explorer/video.py +1 -1
- geo_activity_playground/heatmap_video.py +93 -0
- geo_activity_playground/importers/activity_parsers.py +1 -1
- geo_activity_playground/importers/directory.py +6 -15
- geo_activity_playground/webui/activity/blueprint.py +3 -10
- geo_activity_playground/webui/activity/controller.py +10 -71
- geo_activity_playground/webui/app.py +32 -22
- geo_activity_playground/webui/{auth/blueprint.py → auth_blueprint.py} +1 -1
- geo_activity_playground/webui/calendar/blueprint.py +2 -5
- geo_activity_playground/webui/{eddington/controller.py → eddington_blueprint.py} +17 -13
- geo_activity_playground/webui/equipment/blueprint.py +2 -8
- geo_activity_playground/webui/explorer/blueprint.py +2 -10
- geo_activity_playground/webui/heatmap/blueprint.py +36 -10
- geo_activity_playground/webui/heatmap/heatmap_controller.py +151 -71
- geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +30 -12
- geo_activity_playground/webui/{search/blueprint.py → search_blueprint.py} +1 -1
- geo_activity_playground/webui/settings/blueprint.py +1 -2
- geo_activity_playground/webui/square_planner_blueprint.py +118 -0
- geo_activity_playground/webui/{summary/controller.py → summary_blueprint.py} +23 -24
- geo_activity_playground/webui/templates/page.html.j2 +11 -0
- geo_activity_playground/webui/tile_blueprint.py +42 -0
- geo_activity_playground/webui/upload_blueprint.py +1 -3
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/RECORD +36 -43
- geo_activity_playground/core/heatmap.py +0 -194
- geo_activity_playground/webui/eddington/__init__.py +0 -0
- geo_activity_playground/webui/eddington/blueprint.py +0 -19
- geo_activity_playground/webui/square_planner/__init__.py +0 -0
- geo_activity_playground/webui/square_planner/blueprint.py +0 -38
- geo_activity_playground/webui/square_planner/controller.py +0 -101
- geo_activity_playground/webui/summary/__init__.py +0 -0
- geo_activity_playground/webui/summary/blueprint.py +0 -17
- geo_activity_playground/webui/tile/__init__.py +0 -0
- geo_activity_playground/webui/tile/blueprint.py +0 -32
- geo_activity_playground/webui/tile/controller.py +0 -36
- /geo_activity_playground/webui/{auth/templates → templates}/auth/index.html.j2 +0 -0
- /geo_activity_playground/webui/{eddington/templates → templates}/eddington/index.html.j2 +0 -0
- /geo_activity_playground/webui/{search/templates → templates}/search/index.html.j2 +0 -0
- /geo_activity_playground/webui/{square_planner/templates → templates}/square_planner/index.html.j2 +0 -0
- /geo_activity_playground/webui/{summary/templates → templates}/summary/index.html.j2 +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
|
|
1
|
+
import datetime
|
1
2
|
import io
|
2
3
|
import logging
|
3
4
|
import pathlib
|
5
|
+
from typing import Optional
|
4
6
|
|
5
7
|
import matplotlib.pylab as pl
|
6
8
|
import numpy as np
|
@@ -9,12 +11,13 @@ from PIL import ImageDraw
|
|
9
11
|
|
10
12
|
from geo_activity_playground.core.activities import ActivityRepository
|
11
13
|
from geo_activity_playground.core.config import Config
|
12
|
-
from geo_activity_playground.core.
|
13
|
-
from geo_activity_playground.core.
|
14
|
-
from geo_activity_playground.core.
|
15
|
-
from geo_activity_playground.core.
|
14
|
+
from geo_activity_playground.core.raster_map import convert_to_grayscale
|
15
|
+
from geo_activity_playground.core.raster_map import GeoBounds
|
16
|
+
from geo_activity_playground.core.raster_map import get_sensible_zoom_level
|
17
|
+
from geo_activity_playground.core.raster_map import get_tile
|
18
|
+
from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
|
19
|
+
from geo_activity_playground.core.raster_map import PixelBounds
|
16
20
|
from geo_activity_playground.core.tasks import work_tracker
|
17
|
-
from geo_activity_playground.core.tiles import get_tile
|
18
21
|
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
19
22
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
20
23
|
from geo_activity_playground.webui.explorer.controller import (
|
@@ -25,9 +28,6 @@ from geo_activity_playground.webui.explorer.controller import (
|
|
25
28
|
logger = logging.getLogger(__name__)
|
26
29
|
|
27
30
|
|
28
|
-
OSM_TILE_SIZE = 256 # OSM tile size in pixel
|
29
|
-
|
30
|
-
|
31
31
|
class HeatmapController:
|
32
32
|
def __init__(
|
33
33
|
self,
|
@@ -48,7 +48,12 @@ class HeatmapController:
|
|
48
48
|
"activities_per_tile"
|
49
49
|
]
|
50
50
|
|
51
|
-
def render(
|
51
|
+
def render(
|
52
|
+
self,
|
53
|
+
kinds: list[int],
|
54
|
+
date_start: Optional[datetime.date],
|
55
|
+
date_end: Optional[datetime.date],
|
56
|
+
) -> dict:
|
52
57
|
zoom = 14
|
53
58
|
tiles = self.tile_histories[zoom]
|
54
59
|
medians = tiles.median(skipna=True)
|
@@ -60,9 +65,17 @@ class HeatmapController:
|
|
60
65
|
available_kinds = sorted(self._repository.meta["kind"].unique())
|
61
66
|
|
62
67
|
if not kinds:
|
63
|
-
kinds = available_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}")
|
64
77
|
|
65
|
-
|
78
|
+
values = {
|
66
79
|
"center": {
|
67
80
|
"latitude": median_lat,
|
68
81
|
"longitude": median_lon,
|
@@ -76,71 +89,117 @@ class HeatmapController:
|
|
76
89
|
},
|
77
90
|
"kinds": kinds,
|
78
91
|
"available_kinds": available_kinds,
|
79
|
-
"
|
92
|
+
"extra_args": "&".join(extra_args),
|
80
93
|
}
|
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()
|
81
98
|
|
82
|
-
|
99
|
+
return values
|
100
|
+
|
101
|
+
def _get_counts(
|
102
|
+
self,
|
103
|
+
x: int,
|
104
|
+
y: int,
|
105
|
+
z: int,
|
106
|
+
kind: str,
|
107
|
+
date_start: Optional[datetime.date],
|
108
|
+
date_end: Optional[datetime.date],
|
109
|
+
) -> np.ndarray:
|
83
110
|
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
84
|
-
|
85
|
-
if
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
94
|
-
else:
|
95
|
-
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
96
|
-
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
97
|
-
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
98
|
-
activity_ids_kind = set()
|
99
|
-
for activity_id in activity_ids:
|
100
|
-
activity = self._repository.get_activity_by_id(activity_id)
|
101
|
-
if activity["kind"] == kind:
|
102
|
-
activity_ids_kind.add(activity_id)
|
103
|
-
if activity_ids_kind:
|
104
|
-
with work_tracker(
|
105
|
-
tile_count_cache_path.with_suffix(".json")
|
106
|
-
) as parsed_activities:
|
107
|
-
if parsed_activities - activity_ids_kind:
|
111
|
+
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
|
+
)
|
116
|
+
if tile_count_cache_path.exists():
|
117
|
+
try:
|
118
|
+
tile_counts = np.load(tile_count_cache_path)
|
119
|
+
except ValueError:
|
108
120
|
logger.warning(
|
109
|
-
f"
|
121
|
+
f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
|
110
122
|
)
|
123
|
+
tile_count_cache_path.unlink()
|
111
124
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
125
|
+
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
126
|
+
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."
|
124
139
|
)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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)
|
164
|
+
else:
|
165
|
+
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
166
|
+
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"]:
|
173
|
+
continue
|
174
|
+
time_series = self._repository.get_time_series(activity_id)
|
175
|
+
for _, group in time_series.groupby("segment_id"):
|
176
|
+
xy_pixels = (
|
177
|
+
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
178
|
+
* OSM_TILE_SIZE
|
179
|
+
)
|
180
|
+
im = Image.new("L", tile_pixels)
|
181
|
+
draw = ImageDraw.Draw(im)
|
182
|
+
pixels = list(map(int, xy_pixels.flatten()))
|
183
|
+
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
184
|
+
aim = np.array(im)
|
185
|
+
tile_counts += aim
|
135
186
|
return tile_counts
|
136
187
|
|
137
188
|
def _render_tile_image(
|
138
|
-
self,
|
189
|
+
self,
|
190
|
+
x: int,
|
191
|
+
y: int,
|
192
|
+
z: int,
|
193
|
+
kinds_ids: list[int],
|
194
|
+
date_start: Optional[datetime.date],
|
195
|
+
date_end: Optional[datetime.date],
|
139
196
|
) -> np.ndarray:
|
140
197
|
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
141
198
|
tile_counts = np.zeros(tile_pixels)
|
142
|
-
|
143
|
-
|
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)
|
144
203
|
|
145
204
|
tile_counts = np.sqrt(tile_counts) / 5
|
146
205
|
tile_counts[tile_counts > 1.0] = 1.0
|
@@ -158,21 +217,40 @@ class HeatmapController:
|
|
158
217
|
] + data_color[:, :, c]
|
159
218
|
return map_tile
|
160
219
|
|
161
|
-
def render_tile(
|
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:
|
162
229
|
f = io.BytesIO()
|
163
|
-
pl.imsave(
|
230
|
+
pl.imsave(
|
231
|
+
f,
|
232
|
+
self._render_tile_image(x, y, z, kind_ids, date_start, date_end),
|
233
|
+
format="png",
|
234
|
+
)
|
164
235
|
return bytes(f.getbuffer())
|
165
236
|
|
166
237
|
def download_heatmap(
|
167
|
-
self,
|
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],
|
168
246
|
) -> bytes:
|
169
247
|
geo_bounds = GeoBounds(south, west, north, east)
|
170
248
|
tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
|
171
249
|
pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
|
172
250
|
|
173
251
|
background = np.zeros((*pixel_bounds.shape, 3))
|
174
|
-
for x in range(tile_bounds.
|
175
|
-
for y in range(tile_bounds.
|
252
|
+
for x in range(tile_bounds.x1, tile_bounds.x2):
|
253
|
+
for y in range(tile_bounds.y1, tile_bounds.y2):
|
176
254
|
tile = (
|
177
255
|
np.array(
|
178
256
|
get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
|
@@ -180,14 +258,16 @@ class HeatmapController:
|
|
180
258
|
/ 255
|
181
259
|
)
|
182
260
|
|
183
|
-
i = y - tile_bounds.
|
184
|
-
j = x - tile_bounds.
|
261
|
+
i = y - tile_bounds.y1
|
262
|
+
j = x - tile_bounds.x1
|
185
263
|
|
186
264
|
background[
|
187
265
|
i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
|
188
266
|
j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
|
189
267
|
:,
|
190
|
-
] = self._render_tile_image(
|
268
|
+
] = self._render_tile_image(
|
269
|
+
x, y, tile_bounds.zoom, kind_ids, date_start, date_end
|
270
|
+
)
|
191
271
|
|
192
272
|
f = io.BytesIO()
|
193
273
|
pl.imsave(f, background, format="png")
|
@@ -7,20 +7,38 @@
|
|
7
7
|
</div>
|
8
8
|
</div>
|
9
9
|
|
10
|
-
<
|
11
|
-
|
12
|
-
|
10
|
+
<form action="" method="GET">
|
11
|
+
|
12
|
+
<div class="row mb-3">
|
13
|
+
<label class="col-sm-2 col-form-label">Kinds</label>
|
14
|
+
|
15
|
+
<div class="col-sm-10">
|
13
16
|
{% 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="{{
|
16
|
-
value="{{
|
17
|
-
<label class="form-check-label" for="{{
|
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>
|
18
21
|
</div>
|
19
22
|
{% endfor %}
|
20
|
-
|
21
|
-
</form>
|
23
|
+
</div>
|
22
24
|
</div>
|
23
|
-
|
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>
|
24
42
|
|
25
43
|
<div class="row mb-3">
|
26
44
|
<div class="col">
|
@@ -33,7 +51,7 @@
|
|
33
51
|
center: [{{ center.latitude }}, {{ center.longitude }}],
|
34
52
|
zoom: 12
|
35
53
|
});
|
36
|
-
L.tileLayer('/heatmap/tile/{z}/{x}/{y}
|
54
|
+
L.tileLayer('/heatmap/tile/{z}/{x}/{y}.png?{{ extra_args|safe }}', {
|
37
55
|
maxZoom: 19,
|
38
56
|
attribution: '{{ map_tile_attribution|safe }}'
|
39
57
|
}).addTo(map)
|
@@ -47,7 +65,7 @@
|
|
47
65
|
function downloadAs() {
|
48
66
|
bounds = map.getBounds()
|
49
67
|
window.location.href =
|
50
|
-
`/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/{{
|
68
|
+
`/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/heatmap.png?{{ extra_args|safe }}`
|
51
69
|
}
|
52
70
|
</script>
|
53
71
|
</div>
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import geojson
|
2
|
+
from flask import Blueprint
|
3
|
+
from flask import redirect
|
4
|
+
from flask import render_template
|
5
|
+
from flask import Response
|
6
|
+
from flask import url_for
|
7
|
+
|
8
|
+
from geo_activity_playground.explorer.grid_file import make_explorer_rectangle
|
9
|
+
from geo_activity_playground.explorer.grid_file import make_explorer_tile
|
10
|
+
from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
|
11
|
+
from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
|
12
|
+
from geo_activity_playground.explorer.grid_file import make_grid_points
|
13
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
14
|
+
|
15
|
+
|
16
|
+
def make_square_planner_blueprint(tile_visit_accessor: TileVisitAccessor) -> Blueprint:
|
17
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
18
|
+
|
19
|
+
blueprint = Blueprint("square_planner", __name__, template_folder="templates")
|
20
|
+
|
21
|
+
@blueprint.route("/<int:zoom>")
|
22
|
+
def landing(zoom: int):
|
23
|
+
explored = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
24
|
+
return redirect(
|
25
|
+
url_for(
|
26
|
+
"square_planner.index",
|
27
|
+
zoom=zoom,
|
28
|
+
x=explored.square_x,
|
29
|
+
y=explored.square_y,
|
30
|
+
size=explored.max_square_size,
|
31
|
+
)
|
32
|
+
)
|
33
|
+
|
34
|
+
@blueprint.route("/<int:zoom>/<int:x>/<int:y>/<int:size>")
|
35
|
+
def index(zoom: int, x: int, y: int, size: int):
|
36
|
+
square_geojson = geojson.dumps(
|
37
|
+
geojson.FeatureCollection(
|
38
|
+
features=[
|
39
|
+
make_explorer_rectangle(
|
40
|
+
x,
|
41
|
+
y,
|
42
|
+
x + size,
|
43
|
+
y + size,
|
44
|
+
zoom,
|
45
|
+
)
|
46
|
+
]
|
47
|
+
)
|
48
|
+
)
|
49
|
+
|
50
|
+
missing_geojson = geojson.dumps(
|
51
|
+
geojson.FeatureCollection(
|
52
|
+
features=[
|
53
|
+
make_explorer_tile(
|
54
|
+
tile_x,
|
55
|
+
tile_y,
|
56
|
+
{},
|
57
|
+
zoom,
|
58
|
+
)
|
59
|
+
for tile_x in range(x, x + size)
|
60
|
+
for tile_y in range(y, y + size)
|
61
|
+
if (tile_x, tile_y) not in set(tile_visits[zoom].keys())
|
62
|
+
]
|
63
|
+
)
|
64
|
+
)
|
65
|
+
|
66
|
+
return render_template(
|
67
|
+
"square_planner/index.html.j2",
|
68
|
+
explored_geojson=_get_explored_geojson(tile_visits[zoom].keys(), zoom),
|
69
|
+
missing_geojson=missing_geojson,
|
70
|
+
square_geojson=square_geojson,
|
71
|
+
zoom=zoom,
|
72
|
+
square_x=x,
|
73
|
+
square_y=y,
|
74
|
+
square_size=size,
|
75
|
+
)
|
76
|
+
|
77
|
+
@blueprint.route("/<int:zoom>/<int:x>/<int:y>/<int:size>/missing.<suffix>")
|
78
|
+
def square_planner_missing(zoom: int, x: int, y: int, size: int, suffix: str):
|
79
|
+
points = make_grid_points(
|
80
|
+
(
|
81
|
+
(tile_x, tile_y)
|
82
|
+
for tile_x in range(x, x + size)
|
83
|
+
for tile_y in range(y, y + size)
|
84
|
+
if (tile_x, tile_y) not in set(tile_visits[zoom].keys())
|
85
|
+
),
|
86
|
+
zoom,
|
87
|
+
)
|
88
|
+
if suffix == "geojson":
|
89
|
+
response = make_grid_file_geojson(points)
|
90
|
+
elif suffix == "gpx":
|
91
|
+
response = make_grid_file_gpx(points)
|
92
|
+
else:
|
93
|
+
raise RuntimeError(f"Unsupported suffix {suffix}.")
|
94
|
+
|
95
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
96
|
+
return Response(
|
97
|
+
response,
|
98
|
+
mimetype=mimetypes[suffix],
|
99
|
+
headers={"Content-disposition": "attachment"},
|
100
|
+
)
|
101
|
+
|
102
|
+
return blueprint
|
103
|
+
|
104
|
+
|
105
|
+
def _get_explored_geojson(tile_visits: list[tuple[int, int]], zoom: int) -> str:
|
106
|
+
return geojson.dumps(
|
107
|
+
geojson.FeatureCollection(
|
108
|
+
features=[
|
109
|
+
make_explorer_tile(
|
110
|
+
tile_x,
|
111
|
+
tile_y,
|
112
|
+
{},
|
113
|
+
zoom,
|
114
|
+
)
|
115
|
+
for tile_x, tile_y in tile_visits
|
116
|
+
]
|
117
|
+
)
|
118
|
+
)
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import collections
|
2
2
|
import datetime
|
3
|
-
import functools
|
4
|
-
from typing import Optional
|
5
3
|
|
6
4
|
import altair as alt
|
7
5
|
import pandas as pd
|
6
|
+
from flask import Blueprint
|
7
|
+
from flask import render_template
|
8
8
|
|
9
9
|
from geo_activity_playground.core.activities import ActivityRepository
|
10
10
|
from geo_activity_playground.core.activities import make_geojson_from_time_series
|
@@ -12,15 +12,13 @@ from geo_activity_playground.core.config import Config
|
|
12
12
|
from geo_activity_playground.webui.plot_util import make_kind_scale
|
13
13
|
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
self._repository = repository
|
18
|
-
self._config = config
|
15
|
+
def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
|
16
|
+
blueprint = Blueprint("summary", __name__, template_folder="templates")
|
19
17
|
|
20
|
-
@
|
21
|
-
def
|
22
|
-
kind_scale = make_kind_scale(
|
23
|
-
df = embellished_activities(
|
18
|
+
@blueprint.route("/")
|
19
|
+
def index():
|
20
|
+
kind_scale = make_kind_scale(repository.meta, config)
|
21
|
+
df = embellished_activities(repository.meta)
|
24
22
|
# df = df.loc[df["consider_for_achievements"]]
|
25
23
|
|
26
24
|
year_kind_total = (
|
@@ -30,28 +28,29 @@ class SummaryController:
|
|
30
28
|
.reset_index()
|
31
29
|
)
|
32
30
|
|
33
|
-
return
|
34
|
-
"
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
return render_template(
|
32
|
+
"summary/index.html.j2",
|
33
|
+
plot_distance_heatmaps=plot_distance_heatmaps(df, config),
|
34
|
+
plot_monthly_distance=plot_monthly_distance(df, kind_scale),
|
35
|
+
plot_yearly_distance=plot_yearly_distance(year_kind_total, kind_scale),
|
36
|
+
plot_year_cumulative=plot_year_cumulative(df),
|
37
|
+
tabulate_year_kind_mean=tabulate_year_kind_mean(df)
|
39
38
|
.reset_index()
|
40
39
|
.to_dict(orient="split"),
|
41
|
-
|
42
|
-
|
40
|
+
plot_weekly_distance=plot_weekly_distance(df, kind_scale),
|
41
|
+
nominations=[
|
43
42
|
(
|
44
|
-
|
43
|
+
repository.get_activity_by_id(activity_id),
|
45
44
|
reasons,
|
46
45
|
make_geojson_from_time_series(
|
47
|
-
|
46
|
+
repository.get_time_series(activity_id)
|
48
47
|
),
|
49
48
|
)
|
50
|
-
for activity_id, reasons in nominate_activities(
|
51
|
-
self._repository.meta
|
52
|
-
).items()
|
49
|
+
for activity_id, reasons in nominate_activities(repository.meta).items()
|
53
50
|
],
|
54
|
-
|
51
|
+
)
|
52
|
+
|
53
|
+
return blueprint
|
55
54
|
|
56
55
|
|
57
56
|
def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
|
@@ -97,6 +97,17 @@
|
|
97
97
|
Tiles (Zoom 14)</a></li>
|
98
98
|
<li><a class="dropdown-item" href="{{ url_for('explorer.map', zoom=17) }}">Squadratinhos
|
99
99
|
(Zoom 17)</a></li>
|
100
|
+
|
101
|
+
<li>
|
102
|
+
<hr class="dropdown-divider">
|
103
|
+
</li>
|
104
|
+
|
105
|
+
<li><a class="dropdown-item"
|
106
|
+
href="{{ url_for('square_planner.landing', zoom=14) }}">Square Planner
|
107
|
+
(Zoom 14)</a></li>
|
108
|
+
<li><a class="dropdown-item"
|
109
|
+
href="{{ url_for('square_planner.landing', zoom=17) }}">Square Planner
|
110
|
+
(Zoom 17)</a></li>
|
100
111
|
</ul>
|
101
112
|
</li>
|
102
113
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import io
|
2
|
+
|
3
|
+
import matplotlib.pyplot as pl
|
4
|
+
import numpy as np
|
5
|
+
from flask import Blueprint
|
6
|
+
from flask import Response
|
7
|
+
|
8
|
+
from geo_activity_playground.core.config import Config
|
9
|
+
from geo_activity_playground.core.raster_map import get_tile
|
10
|
+
|
11
|
+
|
12
|
+
def make_tile_blueprint(config: Config) -> Blueprint:
|
13
|
+
blueprint = Blueprint("tiles", __name__, template_folder="templates")
|
14
|
+
|
15
|
+
@blueprint.route("/color/<int:z>/<int:x>/<int:y>.png")
|
16
|
+
def tile_color(x: int, y: int, z: int):
|
17
|
+
map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
|
18
|
+
f = io.BytesIO()
|
19
|
+
pl.imsave(f, map_tile, format="png")
|
20
|
+
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
21
|
+
|
22
|
+
@blueprint.route("/grayscale/<int:z>/<int:x>/<int:y>.png")
|
23
|
+
def tile_grayscale(x: int, y: int, z: int):
|
24
|
+
map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
|
25
|
+
map_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2) # to grayscale
|
26
|
+
map_tile = np.dstack((map_tile, map_tile, map_tile)) # to rgb
|
27
|
+
f = io.BytesIO()
|
28
|
+
pl.imsave(f, map_tile, format="png")
|
29
|
+
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
30
|
+
|
31
|
+
@blueprint.route("/pastel/<int:z>/<int:x>/<int:y>.png")
|
32
|
+
def tile_pastel(x: int, y: int, z: int):
|
33
|
+
map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
|
34
|
+
averaged_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2)
|
35
|
+
grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
|
36
|
+
factor = 0.7
|
37
|
+
pastel_tile = factor * grayscale_tile + (1 - factor) * map_tile
|
38
|
+
f = io.BytesIO()
|
39
|
+
pl.imsave(f, pastel_tile, format="png")
|
40
|
+
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
41
|
+
|
42
|
+
return blueprint
|
@@ -108,9 +108,7 @@ def scan_for_activities(
|
|
108
108
|
skip_strava: bool = False,
|
109
109
|
) -> None:
|
110
110
|
if pathlib.Path("Activities").exists():
|
111
|
-
import_from_directory(
|
112
|
-
config.metadata_extraction_regexes, config.num_processes, config
|
113
|
-
)
|
111
|
+
import_from_directory(config.metadata_extraction_regexes, config)
|
114
112
|
if pathlib.Path("Strava Export").exists():
|
115
113
|
import_from_strava_checkout()
|
116
114
|
if config.strava_client_code and not skip_strava:
|