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.
@@ -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
- tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
168
- tile_histories = tile_visit_accessor.tile_state["tile_history"]
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 = tile_histories[zoom].median()
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
- tile_evolution_states[zoom].clusters.values(), zoom
218
+ tile_evolution_state.clusters.values(), zoom
182
219
  )
183
- if len(tile_evolution_states[zoom].memberships) > 0
220
+ if len(tile_evolution_state.memberships) > 0
184
221
  else {}
185
222
  ),
186
223
  },
187
- "plot_tile_evolution": plot_tile_evolution(tile_histories[zoom]),
224
+ "plot_tile_evolution": plot_tile_evolution(tile_history),
188
225
  "plot_cluster_evolution": plot_cluster_evolution(
189
- tile_evolution_states[zoom].cluster_evolution
226
+ tile_evolution_state.cluster_evolution
190
227
  ),
191
228
  "plot_square_evolution": plot_square_evolution(
192
- tile_evolution_states[zoom].square_evolution
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
- if (x // factor, y // factor) in tile_visits:
206
- map_tile = image_transforms["color"].transform_image(map_tile)
207
- else:
208
- map_tile = image_transforms["color"].transform_image(map_tile) / 1.2
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
- grayscale = image_transforms["color"].transform_image(map_tile) / 1.2
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
- tile = (x * factor + xo, y * factor + yo)
216
- if tile not in tile_visits:
217
- map_tile[
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
- ] = grayscale[
220
- yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
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, map_tile, format="png")
432
+ pl.imsave(f, result, format="png")
224
433
  return Response(bytes(f.getbuffer()), mimetype="image/png")
225
434
 
226
- return blueprint
227
-
228
-
229
- def get_three_color_tiles(
230
- tile_visits: dict,
231
- cluster_state: TileEvolutionState,
232
- zoom: int,
233
- ) -> str:
234
- logger.info("Generate data for explorer tile map …")
235
- today = datetime.date.today()
236
- cmap_first = matplotlib.colormaps["plasma"]
237
- cmap_last = matplotlib.colormaps["plasma"]
238
- tile_dict = {}
239
- for tile, tile_data in tile_visits.items():
240
- if not pd.isna(tile_data["first_time"]):
241
- first_age_days = (today - tile_data["first_time"].date()).days
242
- last_age_days = (today - tile_data["last_time"].date()).days
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
- first_age_days = 10000
245
- last_age_days = 10000
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
- # Mark biggest square.
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: list[list[tuple[int, int]]], zoom: int
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
+ });