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.
Files changed (47) hide show
  1. geo_activity_playground/__main__.py +12 -0
  2. geo_activity_playground/core/activities.py +21 -13
  3. geo_activity_playground/core/raster_map.py +246 -0
  4. geo_activity_playground/core/tiles.py +6 -50
  5. geo_activity_playground/explorer/video.py +1 -1
  6. geo_activity_playground/heatmap_video.py +93 -0
  7. geo_activity_playground/importers/activity_parsers.py +1 -1
  8. geo_activity_playground/importers/directory.py +6 -15
  9. geo_activity_playground/webui/activity/blueprint.py +3 -10
  10. geo_activity_playground/webui/activity/controller.py +10 -71
  11. geo_activity_playground/webui/app.py +32 -22
  12. geo_activity_playground/webui/{auth/blueprint.py → auth_blueprint.py} +1 -1
  13. geo_activity_playground/webui/calendar/blueprint.py +2 -5
  14. geo_activity_playground/webui/{eddington/controller.py → eddington_blueprint.py} +17 -13
  15. geo_activity_playground/webui/equipment/blueprint.py +2 -8
  16. geo_activity_playground/webui/explorer/blueprint.py +2 -10
  17. geo_activity_playground/webui/heatmap/blueprint.py +36 -10
  18. geo_activity_playground/webui/heatmap/heatmap_controller.py +151 -71
  19. geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +30 -12
  20. geo_activity_playground/webui/{search/blueprint.py → search_blueprint.py} +1 -1
  21. geo_activity_playground/webui/settings/blueprint.py +1 -2
  22. geo_activity_playground/webui/square_planner_blueprint.py +118 -0
  23. geo_activity_playground/webui/{summary/controller.py → summary_blueprint.py} +23 -24
  24. geo_activity_playground/webui/templates/page.html.j2 +11 -0
  25. geo_activity_playground/webui/tile_blueprint.py +42 -0
  26. geo_activity_playground/webui/upload_blueprint.py +1 -3
  27. {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/METADATA +1 -1
  28. {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/RECORD +36 -43
  29. geo_activity_playground/core/heatmap.py +0 -194
  30. geo_activity_playground/webui/eddington/__init__.py +0 -0
  31. geo_activity_playground/webui/eddington/blueprint.py +0 -19
  32. geo_activity_playground/webui/square_planner/__init__.py +0 -0
  33. geo_activity_playground/webui/square_planner/blueprint.py +0 -38
  34. geo_activity_playground/webui/square_planner/controller.py +0 -101
  35. geo_activity_playground/webui/summary/__init__.py +0 -0
  36. geo_activity_playground/webui/summary/blueprint.py +0 -17
  37. geo_activity_playground/webui/tile/__init__.py +0 -0
  38. geo_activity_playground/webui/tile/blueprint.py +0 -32
  39. geo_activity_playground/webui/tile/controller.py +0 -36
  40. /geo_activity_playground/webui/{auth/templates → templates}/auth/index.html.j2 +0 -0
  41. /geo_activity_playground/webui/{eddington/templates → templates}/eddington/index.html.j2 +0 -0
  42. /geo_activity_playground/webui/{search/templates → templates}/search/index.html.j2 +0 -0
  43. /geo_activity_playground/webui/{square_planner/templates → templates}/square_planner/index.html.j2 +0 -0
  44. /geo_activity_playground/webui/{summary/templates → templates}/summary/index.html.j2 +0 -0
  45. {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/LICENSE +0 -0
  46. {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/WHEEL +0 -0
  47. {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.heatmap import convert_to_grayscale
13
- from geo_activity_playground.core.heatmap import GeoBounds
14
- from geo_activity_playground.core.heatmap import get_sensible_zoom_level
15
- from geo_activity_playground.core.heatmap import PixelBounds
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(self, kinds: list[str] = []) -> dict:
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
- return {
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
- "kinds_str": ";".join(kinds),
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
- def _get_counts(self, x: int, y: int, z: int, kind: str) -> np.ndarray:
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
- tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy")
85
- if tile_count_cache_path.exists():
86
- try:
87
- tile_counts = np.load(tile_count_cache_path)
88
- except ValueError:
89
- logger.warning(
90
- f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
91
- )
92
- tile_count_cache_path.unlink()
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"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
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
- parsed_activities.clear()
113
- for activity_id in activity_ids_kind:
114
- if activity_id in parsed_activities:
115
- continue
116
- parsed_activities.add(activity_id)
117
- time_series = self._repository.get_time_series(activity_id)
118
- for _, group in time_series.groupby("segment_id"):
119
- xy_pixels = (
120
- np.array(
121
- [group["x"] * 2**z - x, group["y"] * 2**z - y]
122
- ).T
123
- * OSM_TILE_SIZE
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
- im = Image.new("L", tile_pixels)
126
- draw = ImageDraw.Draw(im)
127
- pixels = list(map(int, xy_pixels.flatten()))
128
- draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
129
- aim = np.array(im)
130
- tile_counts += aim
131
- tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
132
- np.save(tmp_path, tile_counts)
133
- tile_count_cache_path.unlink(missing_ok=True)
134
- tmp_path.rename(tile_count_cache_path)
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, x: int, y: int, z: int, kinds: list[str]
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
- for kind in kinds:
143
- tile_counts += self._get_counts(x, y, z, kind)
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(self, x: int, y: int, z: int, kinds: list[str]) -> bytes:
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(f, self._render_tile_image(x, y, z, kinds), format="png")
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, north: float, east: float, south: float, west: float, kinds: list[str]
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.x_tile_min, tile_bounds.x_tile_max):
175
- for y in range(tile_bounds.y_tile_min, tile_bounds.y_tile_max):
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.y_tile_min
184
- j = x - tile_bounds.x_tile_min
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(x, y, tile_bounds.zoom, kinds)
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
- <div class="row mb-3">
11
- <div class="col">
12
- <form action="" method="GET">
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="{{ kind }}" name="kind"
16
- value="{{ kind }}" {{ 'checked' if kind in kinds else '' }} />
17
- <label class="form-check-label" for="{{ kind }}">{{ kind }}</label>
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
- <button type="submit" class="btn btn-primary">Show selected kinds</button>
21
- </form>
23
+ </div>
22
24
  </div>
23
- </div>
25
+
26
+ <div class="row mb-3">
27
+ <label class="col-sm-2 col-form-label">Date range</label>
28
+ <div class="col-sm-5">
29
+ <input type="date" id="date-start" name="date-start" value="{{ date_start }}" />
30
+ <label for="date-start" class="form-label">Start date</label>
31
+ </div>
32
+ <div class="col-sm-5">
33
+ <input type="date" id="date-end" name="date-end" value="{{ date_end }}" />
34
+ <label for="date-start" class="form-label">End date</label>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="mb-3">
39
+ <button type="submit" class="btn btn-primary">Filter heatmap</button>
40
+ </div>
41
+ </form>
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}/{{ kinds_str }}.png', {
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()}/{{ kinds_str }}`
68
+ `/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/heatmap.png?{{ extra_args|safe }}`
51
69
  }
52
70
  </script>
53
71
  </div>
@@ -7,7 +7,7 @@ from flask import render_template
7
7
  from flask import request
8
8
  from flask import Response
9
9
 
10
- from ...core.activities import ActivityRepository
10
+ from ..core.activities import ActivityRepository
11
11
 
12
12
 
13
13
  def reduce_or(selections):
@@ -58,8 +58,7 @@ def int_or_none(s: str) -> Optional[int]:
58
58
  return int(s)
59
59
  except ValueError as e:
60
60
  flash(f"Cannot parse integer from {s}: {e}", category="danger")
61
- else:
62
- return None
61
+ return None
63
62
 
64
63
 
65
64
  def make_settings_blueprint(
@@ -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
- class SummaryController:
16
- def __init__(self, repository: ActivityRepository, config: Config) -> None:
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
- @functools.cache
21
- def render(self) -> dict:
22
- kind_scale = make_kind_scale(self._repository.meta, self._config)
23
- df = embellished_activities(self._repository.meta)
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
- "plot_distance_heatmaps": plot_distance_heatmaps(df, self._config),
35
- "plot_monthly_distance": plot_monthly_distance(df, kind_scale),
36
- "plot_yearly_distance": plot_yearly_distance(year_kind_total, kind_scale),
37
- "plot_year_cumulative": plot_year_cumulative(df),
38
- "tabulate_year_kind_mean": tabulate_year_kind_mean(df)
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
- "plot_weekly_distance": plot_weekly_distance(df, kind_scale),
42
- "nominations": [
40
+ plot_weekly_distance=plot_weekly_distance(df, kind_scale),
41
+ nominations=[
43
42
  (
44
- self._repository.get_activity_by_id(activity_id),
43
+ repository.get_activity_by_id(activity_id),
45
44
  reasons,
46
45
  make_geojson_from_time_series(
47
- self._repository.get_time_series(activity_id)
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: