geo-activity-playground 0.45.0__py3-none-any.whl → 1.0.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/datamodel.py +3 -0
- geo_activity_playground/core/export.py +129 -0
- geo_activity_playground/core/meta_search.py +1 -1
- geo_activity_playground/webui/app.py +3 -1
- geo_activity_playground/webui/authenticator.py +4 -2
- geo_activity_playground/webui/blueprints/auth_blueprint.py +3 -0
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +300 -188
- geo_activity_playground/webui/blueprints/export_blueprint.py +30 -0
- geo_activity_playground/webui/blueprints/upload_blueprint.py +9 -0
- geo_activity_playground/webui/static/server-side-explorer.js +55 -0
- geo_activity_playground/webui/templates/auth/index.html.j2 +1 -0
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +41 -36
- geo_activity_playground/webui/templates/export/index.html.j2 +39 -0
- geo_activity_playground/webui/templates/page.html.j2 +3 -6
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/RECORD +19 -16
- geo_activity_playground/webui/templates/explorer/index.html.j2 +0 -148
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
|
|
1
|
+
import abc
|
1
2
|
import datetime
|
2
3
|
import io
|
3
4
|
import itertools
|
4
5
|
import logging
|
6
|
+
from collections.abc import Iterable
|
7
|
+
from typing import Union
|
5
8
|
|
6
9
|
import altair as alt
|
7
10
|
import geojson
|
@@ -12,8 +15,10 @@ import pandas as pd
|
|
12
15
|
import sqlalchemy
|
13
16
|
from flask import Blueprint
|
14
17
|
from flask import flash
|
18
|
+
from flask import json
|
15
19
|
from flask import redirect
|
16
20
|
from flask import render_template
|
21
|
+
from flask import request
|
17
22
|
from flask import Response
|
18
23
|
from flask import url_for
|
19
24
|
|
@@ -43,6 +48,80 @@ alt.data_transformers.enable("vegafusion")
|
|
43
48
|
logger = logging.getLogger(__name__)
|
44
49
|
|
45
50
|
|
51
|
+
def blend_color(
|
52
|
+
base: np.ndarray, addition: Union[np.ndarray, float], opacity: float
|
53
|
+
) -> np.ndarray:
|
54
|
+
return (1 - opacity) * base + opacity * addition
|
55
|
+
|
56
|
+
|
57
|
+
class ColorStrategy(abc.ABC):
|
58
|
+
@abc.abstractmethod
|
59
|
+
def color_image(
|
60
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
61
|
+
) -> np.ndarray:
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
class ClusterColorStrategy(ColorStrategy):
|
66
|
+
def __init__(self, evolution_state, tile_visits):
|
67
|
+
self.evolution_state = evolution_state
|
68
|
+
self.tile_visits = tile_visits
|
69
|
+
self.max_cluster_members = max(
|
70
|
+
evolution_state.clusters.values(),
|
71
|
+
key=len,
|
72
|
+
)
|
73
|
+
|
74
|
+
def color_image(
|
75
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
76
|
+
) -> np.ndarray:
|
77
|
+
if tile_xy in self.max_cluster_members:
|
78
|
+
return blend_color(grayscale, np.array([[[55, 126, 184]]]) / 256, 0.3)
|
79
|
+
elif tile_xy in self.evolution_state.memberships:
|
80
|
+
return blend_color(grayscale, np.array([[[77, 175, 74]]]) / 256, 0.3)
|
81
|
+
elif tile_xy in self.tile_visits:
|
82
|
+
return blend_color(grayscale, 0.0, 0.3)
|
83
|
+
else:
|
84
|
+
return grayscale
|
85
|
+
|
86
|
+
|
87
|
+
class VisitTimeColorStrategy(ColorStrategy):
|
88
|
+
def __init__(self, tile_visits, use_first=True):
|
89
|
+
self.tile_visits = tile_visits
|
90
|
+
self.use_first = use_first
|
91
|
+
|
92
|
+
def color_image(
|
93
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
94
|
+
) -> np.ndarray:
|
95
|
+
if tile_xy in self.tile_visits:
|
96
|
+
today = datetime.date.today()
|
97
|
+
cmap = matplotlib.colormaps["plasma"]
|
98
|
+
tile_info = self.tile_visits[tile_xy]
|
99
|
+
relevant_time = (
|
100
|
+
tile_info["first_time"] if self.use_first else tile_info["last_time"]
|
101
|
+
)
|
102
|
+
last_age_days = (today - relevant_time.date()).days
|
103
|
+
color = cmap(max(1 - last_age_days / (2 * 365), 0.0))
|
104
|
+
return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
|
105
|
+
else:
|
106
|
+
return grayscale
|
107
|
+
|
108
|
+
|
109
|
+
class NumVisitsColorStrategy(ColorStrategy):
|
110
|
+
def __init__(self, tile_visits):
|
111
|
+
self.tile_visits = tile_visits
|
112
|
+
|
113
|
+
def color_image(
|
114
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
115
|
+
) -> np.ndarray:
|
116
|
+
if tile_xy in self.tile_visits:
|
117
|
+
cmap = matplotlib.colormaps["viridis"]
|
118
|
+
tile_info = self.tile_visits[tile_xy]
|
119
|
+
color = cmap(min(len(tile_info["activity_ids"]) / 50, 1.0))
|
120
|
+
return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
|
121
|
+
else:
|
122
|
+
return grayscale
|
123
|
+
|
124
|
+
|
46
125
|
def make_explorer_blueprint(
|
47
126
|
authenticator: Authenticator,
|
48
127
|
tile_visit_accessor: TileVisitAccessor,
|
@@ -52,48 +131,6 @@ def make_explorer_blueprint(
|
|
52
131
|
) -> Blueprint:
|
53
132
|
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
54
133
|
|
55
|
-
@blueprint.route("/<int:zoom>")
|
56
|
-
def map(zoom: int):
|
57
|
-
if zoom not in config_accessor().explorer_zoom_levels:
|
58
|
-
return {"zoom_level_not_generated": zoom}
|
59
|
-
|
60
|
-
tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
|
61
|
-
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
62
|
-
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
63
|
-
|
64
|
-
medians = tile_histories[zoom].median()
|
65
|
-
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
66
|
-
medians["tile_x"], medians["tile_y"], zoom
|
67
|
-
)
|
68
|
-
|
69
|
-
explored = get_three_color_tiles(
|
70
|
-
tile_visits[zoom], tile_evolution_states[zoom], zoom
|
71
|
-
)
|
72
|
-
|
73
|
-
context = {
|
74
|
-
"center": {
|
75
|
-
"latitude": median_lat,
|
76
|
-
"longitude": median_lon,
|
77
|
-
"bbox": (
|
78
|
-
bounding_box_for_biggest_cluster(
|
79
|
-
tile_evolution_states[zoom].clusters.values(), zoom
|
80
|
-
)
|
81
|
-
if len(tile_evolution_states[zoom].memberships) > 0
|
82
|
-
else {}
|
83
|
-
),
|
84
|
-
},
|
85
|
-
"explored": explored,
|
86
|
-
"plot_tile_evolution": plot_tile_evolution(tile_histories[zoom]),
|
87
|
-
"plot_cluster_evolution": plot_cluster_evolution(
|
88
|
-
tile_evolution_states[zoom].cluster_evolution
|
89
|
-
),
|
90
|
-
"plot_square_evolution": plot_square_evolution(
|
91
|
-
tile_evolution_states[zoom].square_evolution
|
92
|
-
),
|
93
|
-
"zoom": zoom,
|
94
|
-
}
|
95
|
-
return render_template("explorer/index.html.j2", **context)
|
96
|
-
|
97
134
|
@blueprint.route("/enable-zoom-level/<int:zoom>")
|
98
135
|
@needs_authentication(authenticator)
|
99
136
|
def enable_zoom_level(zoom: int):
|
@@ -164,10 +201,10 @@ def make_explorer_blueprint(
|
|
164
201
|
if zoom not in config_accessor().explorer_zoom_levels:
|
165
202
|
return {"zoom_level_not_generated": zoom}
|
166
203
|
|
167
|
-
|
168
|
-
|
204
|
+
tile_evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
205
|
+
tile_history = tile_visit_accessor.tile_state["tile_history"][zoom]
|
169
206
|
|
170
|
-
medians =
|
207
|
+
medians = tile_history.median()
|
171
208
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
172
209
|
medians["tile_x"], medians["tile_y"], zoom
|
173
210
|
)
|
@@ -178,182 +215,257 @@ def make_explorer_blueprint(
|
|
178
215
|
"longitude": median_lon,
|
179
216
|
"bbox": (
|
180
217
|
bounding_box_for_biggest_cluster(
|
181
|
-
|
218
|
+
tile_evolution_state.clusters.values(), zoom
|
182
219
|
)
|
183
|
-
if len(
|
220
|
+
if len(tile_evolution_state.memberships) > 0
|
184
221
|
else {}
|
185
222
|
),
|
186
223
|
},
|
187
|
-
"plot_tile_evolution": plot_tile_evolution(
|
224
|
+
"plot_tile_evolution": plot_tile_evolution(tile_history),
|
188
225
|
"plot_cluster_evolution": plot_cluster_evolution(
|
189
|
-
|
226
|
+
tile_evolution_state.cluster_evolution
|
190
227
|
),
|
191
228
|
"plot_square_evolution": plot_square_evolution(
|
192
|
-
|
229
|
+
tile_evolution_state.square_evolution
|
193
230
|
),
|
194
231
|
"zoom": zoom,
|
232
|
+
"num_tiles": len(tile_history),
|
233
|
+
"num_cluster_tiles": len(tile_evolution_state.memberships),
|
234
|
+
"square_x": tile_evolution_state.square_x,
|
235
|
+
"square_y": tile_evolution_state.square_y,
|
236
|
+
"square_size": tile_evolution_state.max_square_size,
|
237
|
+
"max_cluster_size": max(map(len, tile_evolution_state.clusters.values())),
|
195
238
|
}
|
196
239
|
return render_template("explorer/server-side.html.j2", **context)
|
197
240
|
|
198
241
|
@blueprint.route("/<int:zoom>/tile/<int:z>/<int:x>/<int:y>.png")
|
199
242
|
def tile(zoom: int, z: int, x: int, y: int) -> Response:
|
200
243
|
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
244
|
+
evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
201
245
|
|
202
246
|
map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
|
247
|
+
grayscale = image_transforms["grayscale"].transform_image(map_tile)
|
248
|
+
square_line_width = 3
|
249
|
+
square_color = np.array([[[228, 26, 28]]]) / 256
|
250
|
+
|
251
|
+
match request.args.get("color_strategy", "cluster"):
|
252
|
+
case "cluster":
|
253
|
+
color_strategy = ClusterColorStrategy(evolution_state, tile_visits)
|
254
|
+
case "first":
|
255
|
+
color_strategy = VisitTimeColorStrategy(tile_visits, use_first=True)
|
256
|
+
case "last":
|
257
|
+
color_strategy = VisitTimeColorStrategy(tile_visits, use_first=False)
|
258
|
+
case "visits":
|
259
|
+
color_strategy = NumVisitsColorStrategy(tile_visits)
|
260
|
+
case _:
|
261
|
+
raise ValueError("Unsupported color strategy.")
|
262
|
+
|
203
263
|
if z >= zoom:
|
204
264
|
factor = 2 ** (z - zoom)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
265
|
+
tile_x = x // factor
|
266
|
+
tile_y = y // factor
|
267
|
+
tile_xy = (tile_x, tile_y)
|
268
|
+
result = color_strategy.color_image(tile_xy, grayscale)
|
269
|
+
|
270
|
+
if x % factor == 0:
|
271
|
+
result[:, 0, :] = 0.5
|
272
|
+
if y % factor == 0:
|
273
|
+
result[0, :, :] = 0.5
|
274
|
+
|
275
|
+
if (
|
276
|
+
evolution_state.square_x is not None
|
277
|
+
and evolution_state.square_y is not None
|
278
|
+
):
|
279
|
+
if (
|
280
|
+
x % factor == 0
|
281
|
+
and tile_x == evolution_state.square_x
|
282
|
+
and evolution_state.square_y
|
283
|
+
<= tile_y
|
284
|
+
< evolution_state.square_y + evolution_state.max_square_size
|
285
|
+
):
|
286
|
+
result[:, 0:square_line_width] = blend_color(
|
287
|
+
result[:, 0:square_line_width], square_color, 0.5
|
288
|
+
)
|
289
|
+
if (
|
290
|
+
y % factor == 0
|
291
|
+
and tile_y == evolution_state.square_y
|
292
|
+
and evolution_state.square_x
|
293
|
+
<= tile_x
|
294
|
+
< evolution_state.square_x + evolution_state.max_square_size
|
295
|
+
):
|
296
|
+
result[0:square_line_width, :] = blend_color(
|
297
|
+
result[0:square_line_width, :], square_color, 0.5
|
298
|
+
)
|
299
|
+
|
300
|
+
if (
|
301
|
+
(x + 1) % factor == 0
|
302
|
+
and (x + 1) // factor
|
303
|
+
== evolution_state.square_x + evolution_state.max_square_size
|
304
|
+
and evolution_state.square_y
|
305
|
+
<= tile_y
|
306
|
+
< evolution_state.square_y + evolution_state.max_square_size
|
307
|
+
):
|
308
|
+
result[:, -square_line_width:] = blend_color(
|
309
|
+
result[:, -square_line_width:], square_color, 0.5
|
310
|
+
)
|
311
|
+
if (
|
312
|
+
(y + 1) % factor == 0
|
313
|
+
and (y + 1) // factor
|
314
|
+
== evolution_state.square_y + evolution_state.max_square_size
|
315
|
+
and evolution_state.square_x
|
316
|
+
<= tile_x
|
317
|
+
< evolution_state.square_x + evolution_state.max_square_size
|
318
|
+
):
|
319
|
+
result[-square_line_width:, :] = blend_color(
|
320
|
+
result[-square_line_width:, :], square_color, 0.5
|
321
|
+
)
|
209
322
|
else:
|
210
|
-
|
323
|
+
result = grayscale
|
211
324
|
factor = 2 ** (zoom - z)
|
212
325
|
width = 256 // factor
|
213
326
|
for xo in range(factor):
|
214
327
|
for yo in range(factor):
|
215
|
-
|
216
|
-
|
217
|
-
|
328
|
+
tile_x = x * factor + xo
|
329
|
+
tile_y = y * factor + yo
|
330
|
+
tile_xy = (tile_x, tile_y)
|
331
|
+
if tile_xy in tile_visits:
|
332
|
+
result[
|
218
333
|
yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
|
219
|
-
] =
|
220
|
-
|
221
|
-
|
334
|
+
] = color_strategy.color_image(
|
335
|
+
tile_xy,
|
336
|
+
grayscale[
|
337
|
+
yo * width : (yo + 1) * width,
|
338
|
+
xo * width : (xo + 1) * width,
|
339
|
+
],
|
340
|
+
)
|
341
|
+
|
342
|
+
if (
|
343
|
+
evolution_state.square_x is not None
|
344
|
+
and evolution_state.square_y is not None
|
345
|
+
):
|
346
|
+
if (
|
347
|
+
tile_x == evolution_state.square_x
|
348
|
+
and evolution_state.square_y
|
349
|
+
<= tile_y
|
350
|
+
< evolution_state.square_y
|
351
|
+
+ evolution_state.max_square_size
|
352
|
+
):
|
353
|
+
result[
|
354
|
+
yo * width : (yo + 1) * width,
|
355
|
+
xo * width : xo * width + square_line_width,
|
356
|
+
] = blend_color(
|
357
|
+
result[
|
358
|
+
yo * width : (yo + 1) * width,
|
359
|
+
xo * width : xo * width + square_line_width,
|
360
|
+
],
|
361
|
+
square_color,
|
362
|
+
0.5,
|
363
|
+
)
|
364
|
+
if (
|
365
|
+
tile_y == evolution_state.square_y
|
366
|
+
and evolution_state.square_x
|
367
|
+
<= tile_x
|
368
|
+
< evolution_state.square_x
|
369
|
+
+ evolution_state.max_square_size
|
370
|
+
):
|
371
|
+
result[
|
372
|
+
yo * width : yo * width + square_line_width,
|
373
|
+
xo * width : (xo + 1) * width,
|
374
|
+
] = blend_color(
|
375
|
+
result[
|
376
|
+
yo * width : yo * width + square_line_width,
|
377
|
+
xo * width : (xo + 1) * width,
|
378
|
+
],
|
379
|
+
square_color,
|
380
|
+
0.5,
|
381
|
+
)
|
382
|
+
|
383
|
+
if (
|
384
|
+
tile_x + 1
|
385
|
+
== evolution_state.square_x
|
386
|
+
+ evolution_state.max_square_size
|
387
|
+
and evolution_state.square_y
|
388
|
+
<= tile_y
|
389
|
+
< evolution_state.square_y
|
390
|
+
+ evolution_state.max_square_size
|
391
|
+
):
|
392
|
+
result[
|
393
|
+
yo * width : (yo + 1) * width,
|
394
|
+
(xo + 1) * width
|
395
|
+
- square_line_width : (xo + 1) * width,
|
396
|
+
] = blend_color(
|
397
|
+
result[
|
398
|
+
yo * width : (yo + 1) * width,
|
399
|
+
(xo + 1) * width
|
400
|
+
- square_line_width : (xo + 1) * width,
|
401
|
+
],
|
402
|
+
square_color,
|
403
|
+
0.5,
|
404
|
+
)
|
405
|
+
|
406
|
+
if (
|
407
|
+
tile_y + 1
|
408
|
+
== evolution_state.square_y
|
409
|
+
+ evolution_state.max_square_size
|
410
|
+
and evolution_state.square_x
|
411
|
+
<= tile_x
|
412
|
+
< evolution_state.square_x
|
413
|
+
+ evolution_state.max_square_size
|
414
|
+
):
|
415
|
+
result[
|
416
|
+
(yo + 1) * width
|
417
|
+
- square_line_width : (yo + 1) * width,
|
418
|
+
xo * width : (xo + 1) * width,
|
419
|
+
] = blend_color(
|
420
|
+
result[
|
421
|
+
(yo + 1) * width
|
422
|
+
- square_line_width : (yo + 1) * width,
|
423
|
+
xo * width : (xo + 1) * width,
|
424
|
+
],
|
425
|
+
square_color,
|
426
|
+
0.5,
|
427
|
+
)
|
428
|
+
if width >= 64:
|
429
|
+
result[yo * width, :, :] = 0.5
|
430
|
+
result[:, xo * width, :] = 0.5
|
222
431
|
f = io.BytesIO()
|
223
|
-
pl.imsave(f,
|
432
|
+
pl.imsave(f, result, format="png")
|
224
433
|
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
225
434
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
435
|
+
@blueprint.route("/<int:zoom>/info/<float:latitude>/<float:longitude>")
|
436
|
+
def info(zoom: int, latitude: float, longitude: float) -> dict:
|
437
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
438
|
+
evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
439
|
+
tile_xy = compute_tile(latitude, longitude, zoom)
|
440
|
+
if tile_xy in tile_visits:
|
441
|
+
tile_info = tile_visits[tile_xy]
|
442
|
+
first = DB.session.get_one(Activity, tile_info["first_id"])
|
443
|
+
last = DB.session.get_one(Activity, tile_info["last_id"])
|
444
|
+
result = {
|
445
|
+
"tile_xy": f"{tile_xy}",
|
446
|
+
"num_visits": len(tile_info["activity_ids"]),
|
447
|
+
"first_activity_id": first.id,
|
448
|
+
"first_activity_name": first.name,
|
449
|
+
"first_time": tile_info["first_time"].isoformat(),
|
450
|
+
"last_activity_id": last.id,
|
451
|
+
"last_activity_name": last.name,
|
452
|
+
"last_time": tile_info["last_time"].isoformat(),
|
453
|
+
"is_cluster": tile_xy in evolution_state.memberships,
|
454
|
+
"this_cluster_size": len(
|
455
|
+
evolution_state.clusters.get(
|
456
|
+
evolution_state.memberships.get(tile_xy, None), []
|
457
|
+
)
|
458
|
+
),
|
459
|
+
}
|
243
460
|
else:
|
244
|
-
|
245
|
-
|
246
|
-
tile_dict[tile] = {
|
247
|
-
"first_activity_id": str(tile_data["first_id"]),
|
248
|
-
"first_activity_name": DB.session.scalar(
|
249
|
-
sqlalchemy.select(Activity.name).where(
|
250
|
-
Activity.id == tile_data["first_id"]
|
251
|
-
)
|
252
|
-
),
|
253
|
-
"last_activity_id": str(tile_data["last_id"]),
|
254
|
-
"last_activity_name": DB.session.scalar(
|
255
|
-
sqlalchemy.select(Activity.name).where(
|
256
|
-
Activity.id == tile_data["last_id"]
|
257
|
-
)
|
258
|
-
),
|
259
|
-
"first_age_days": first_age_days,
|
260
|
-
"first_age_color": matplotlib.colors.to_hex(
|
261
|
-
cmap_first(max(1 - first_age_days / (2 * 365), 0.0))
|
262
|
-
),
|
263
|
-
"last_age_days": last_age_days,
|
264
|
-
"last_age_color": matplotlib.colors.to_hex(
|
265
|
-
cmap_last(max(1 - last_age_days / (2 * 365), 0.0))
|
266
|
-
),
|
267
|
-
"cluster": False,
|
268
|
-
"color": "#303030",
|
269
|
-
"first_visit": tile_data["first_time"].date().isoformat(),
|
270
|
-
"last_visit": tile_data["last_time"].date().isoformat(),
|
271
|
-
"num_visits": len(tile_data["activity_ids"]),
|
272
|
-
"square": False,
|
273
|
-
"tile": f"({zoom}, {tile[0]}, {tile[1]})",
|
274
|
-
}
|
461
|
+
result = {}
|
462
|
+
return result
|
275
463
|
|
276
|
-
|
277
|
-
if cluster_state.max_square_size:
|
278
|
-
for x in range(
|
279
|
-
cluster_state.square_x,
|
280
|
-
cluster_state.square_x + cluster_state.max_square_size,
|
281
|
-
):
|
282
|
-
for y in range(
|
283
|
-
cluster_state.square_y,
|
284
|
-
cluster_state.square_y + cluster_state.max_square_size,
|
285
|
-
):
|
286
|
-
tile_dict[(x, y)]["square"] = True
|
287
|
-
|
288
|
-
# Add cluster information.
|
289
|
-
for members in cluster_state.clusters.values():
|
290
|
-
for member in members:
|
291
|
-
tile_dict[member]["this_cluster_size"] = len(members)
|
292
|
-
tile_dict[member]["cluster"] = True
|
293
|
-
if len(cluster_state.cluster_evolution) > 0:
|
294
|
-
max_cluster_size = cluster_state.cluster_evolution["max_cluster_size"].iloc[-1]
|
295
|
-
else:
|
296
|
-
max_cluster_size = 0
|
297
|
-
num_cluster_tiles = len(cluster_state.memberships)
|
298
|
-
|
299
|
-
# Apply cluster colors.
|
300
|
-
cluster_cmap = matplotlib.colormaps["tab10"]
|
301
|
-
for color, members in zip(
|
302
|
-
itertools.cycle(map(cluster_cmap, [0, 1, 2, 3, 4, 5, 6, 8, 9])),
|
303
|
-
sorted(
|
304
|
-
cluster_state.clusters.values(),
|
305
|
-
key=lambda members: len(members),
|
306
|
-
reverse=True,
|
307
|
-
),
|
308
|
-
):
|
309
|
-
hex_color = matplotlib.colors.to_hex(color)
|
310
|
-
for member in members:
|
311
|
-
tile_dict[member]["color"] = hex_color
|
312
|
-
|
313
|
-
if cluster_state.max_square_size:
|
314
|
-
square_geojson = geojson.dumps(
|
315
|
-
geojson.FeatureCollection(
|
316
|
-
features=[
|
317
|
-
make_explorer_rectangle(
|
318
|
-
cluster_state.square_x,
|
319
|
-
cluster_state.square_y,
|
320
|
-
cluster_state.square_x + cluster_state.max_square_size,
|
321
|
-
cluster_state.square_y + cluster_state.max_square_size,
|
322
|
-
zoom,
|
323
|
-
)
|
324
|
-
]
|
325
|
-
)
|
326
|
-
)
|
327
|
-
else:
|
328
|
-
square_geojson = "{}"
|
329
|
-
|
330
|
-
try:
|
331
|
-
feature_collection = geojson.FeatureCollection(
|
332
|
-
features=[
|
333
|
-
make_explorer_tile(x, y, v, zoom) for (x, y), v in tile_dict.items()
|
334
|
-
]
|
335
|
-
)
|
336
|
-
explored_geojson = geojson.dumps(feature_collection)
|
337
|
-
except TypeError as e:
|
338
|
-
logger.error(f"Encountered TypeError while building GeoJSON: {e=}")
|
339
|
-
logger.error(f"{tile_dict = }")
|
340
|
-
raise
|
341
|
-
|
342
|
-
result = {
|
343
|
-
"explored_geojson": explored_geojson,
|
344
|
-
"max_cluster_size": max_cluster_size,
|
345
|
-
"num_cluster_tiles": num_cluster_tiles,
|
346
|
-
"num_tiles": len(tile_dict),
|
347
|
-
"square_size": cluster_state.max_square_size,
|
348
|
-
"square_x": cluster_state.square_x,
|
349
|
-
"square_y": cluster_state.square_y,
|
350
|
-
"square_geojson": square_geojson,
|
351
|
-
}
|
352
|
-
return result
|
464
|
+
return blueprint
|
353
465
|
|
354
466
|
|
355
467
|
def bounding_box_for_biggest_cluster(
|
356
|
-
clusters:
|
468
|
+
clusters: Iterable[list[tuple[int, int]]], zoom: int
|
357
469
|
) -> str:
|
358
470
|
biggest_cluster = max(clusters, key=lambda members: len(members))
|
359
471
|
min_x = min(x for x, y in biggest_cluster)
|
@@ -0,0 +1,30 @@
|
|
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.export import export_all
|
7
|
+
from ..authenticator import Authenticator
|
8
|
+
from ..authenticator import needs_authentication
|
9
|
+
|
10
|
+
|
11
|
+
def make_export_blueprint(authenticator: Authenticator) -> Blueprint:
|
12
|
+
blueprint = Blueprint("export", __name__, template_folder="templates")
|
13
|
+
|
14
|
+
@needs_authentication(authenticator)
|
15
|
+
@blueprint.route("/")
|
16
|
+
def index():
|
17
|
+
return render_template("export/index.html.j2")
|
18
|
+
|
19
|
+
@needs_authentication(authenticator)
|
20
|
+
@blueprint.route("/export")
|
21
|
+
def export():
|
22
|
+
meta_format = request.args["meta_format"]
|
23
|
+
activity_format = request.args["activity_format"]
|
24
|
+
return Response(
|
25
|
+
bytes(export_all(meta_format, activity_format)),
|
26
|
+
mimetype="application/zip",
|
27
|
+
headers={"Content-disposition": 'attachment; filename="export.zip"'},
|
28
|
+
)
|
29
|
+
|
30
|
+
return blueprint
|
@@ -25,6 +25,8 @@ from ...importers.strava_api import import_from_strava_api
|
|
25
25
|
from ...importers.strava_checkout import import_from_strava_checkout
|
26
26
|
from ..authenticator import Authenticator
|
27
27
|
from ..authenticator import needs_authentication
|
28
|
+
from ..flasher import Flasher
|
29
|
+
from ..flasher import FlashTypes
|
28
30
|
|
29
31
|
|
30
32
|
def make_upload_blueprint(
|
@@ -32,6 +34,7 @@ def make_upload_blueprint(
|
|
32
34
|
tile_visit_accessor: TileVisitAccessor,
|
33
35
|
config: Config,
|
34
36
|
authenticator: Authenticator,
|
37
|
+
flasher: Flasher,
|
35
38
|
) -> Blueprint:
|
36
39
|
blueprint = Blueprint("upload", __name__, template_folder="templates")
|
37
40
|
|
@@ -72,6 +75,12 @@ def make_upload_blueprint(
|
|
72
75
|
".tcx",
|
73
76
|
]
|
74
77
|
assert target_path.is_relative_to("Activities")
|
78
|
+
if target_path.exists():
|
79
|
+
flasher.flash_message(
|
80
|
+
f"An activity with path '{target_path}' already exists. Rename the file and try again.",
|
81
|
+
FlashTypes.DANGER,
|
82
|
+
)
|
83
|
+
return redirect(url_for(".index"))
|
75
84
|
file.save(target_path)
|
76
85
|
scan_for_activities(
|
77
86
|
repository,
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
let tile_layer = null
|
3
|
+
|
4
|
+
function changeColor(method) {
|
5
|
+
if (tile_layer) {
|
6
|
+
map.removeLayer(tile_layer)
|
7
|
+
}
|
8
|
+
tile_layer = L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=${method}`, {
|
9
|
+
maxZoom: 19,
|
10
|
+
attribution: map_tile_attribution
|
11
|
+
}).addTo(map)
|
12
|
+
}
|
13
|
+
|
14
|
+
let map = L.map('explorer-map', {
|
15
|
+
fullscreenControl: true,
|
16
|
+
center: [center_latitude, center_longitude],
|
17
|
+
zoom: 12
|
18
|
+
});
|
19
|
+
|
20
|
+
changeColor('cluster')
|
21
|
+
|
22
|
+
if (bbox) {
|
23
|
+
map.fitBounds(L.geoJSON(bbox).getBounds())
|
24
|
+
}
|
25
|
+
|
26
|
+
map.on('click', e => {
|
27
|
+
fetch(`/explorer/${zoom}/info/${e.latlng.lat}/${e.latlng.lng}`)
|
28
|
+
.then(response => response.json())
|
29
|
+
.then(data => {
|
30
|
+
if (!data.tile_xy) {
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
console.debug(data);
|
34
|
+
|
35
|
+
let lines = [
|
36
|
+
`<dt>Tile</dt>`,
|
37
|
+
`<dd>${data.tile_xy}</dd>`,
|
38
|
+
`<dt>First visit</dt>`,
|
39
|
+
`<dd>${data.first_time}</br><a href=/activity/${data.first_activity_id}>${data.first_activity_name}</a></dd>`,
|
40
|
+
`<dt>Last visit</dt>`,
|
41
|
+
`<dd>${data.last_time}</br><a href=/activity/${data.last_activity_id}>${data.last_activity_name}</a></dd>`,
|
42
|
+
`<dt>Number of visits</dt>`,
|
43
|
+
`<dd>${data.num_visits}</dd>`,
|
44
|
+
]
|
45
|
+
if (data.this_cluster_size) {
|
46
|
+
lines.push(`<dt>This cluster size</dt><dd>${data.this_cluster_size}</dd>`)
|
47
|
+
}
|
48
|
+
|
49
|
+
L.popup()
|
50
|
+
.setLatLng(e.latlng)
|
51
|
+
.setContent('<dl>' + lines.join('') + '</dl>')
|
52
|
+
.openOn(map);
|
53
|
+
}
|
54
|
+
);
|
55
|
+
});
|