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
@@ -12,6 +12,7 @@ from geo_activity_playground.core.config import import_old_config
12
12
  from geo_activity_playground.core.config import import_old_strava_config
13
13
  from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
14
14
  from geo_activity_playground.explorer.video import explorer_video_main
15
+ from geo_activity_playground.heatmap_video import main_heatmap_video
15
16
  from geo_activity_playground.webui.app import web_ui_main
16
17
  from geo_activity_playground.webui.upload_blueprint import scan_for_activities
17
18
 
@@ -80,6 +81,17 @@ def main() -> None:
80
81
  subparser = subparsers.add_parser("cache", help="Cache stuff")
81
82
  subparser.set_defaults(func=lambda options: main_cache(options.basedir))
82
83
 
84
+ subparser = subparsers.add_parser(
85
+ "heatmap-video", help="Create a video with the evolution of the heatmap"
86
+ )
87
+ subparser.add_argument("latitude", type=float)
88
+ subparser.add_argument("longitude", type=float)
89
+ subparser.add_argument("zoom", type=int)
90
+ subparser.add_argument("--decay", type=float, default=0.05)
91
+ subparser.add_argument("--video-width", type=int, default=1920)
92
+ subparser.add_argument("--video-height", type=int, default=1080)
93
+ subparser.set_defaults(func=main_heatmap_video)
94
+
83
95
  options = parser.parse_args()
84
96
  coloredlogs.install(
85
97
  fmt="%(asctime)s %(name)s %(levelname)s %(message)s",
@@ -3,6 +3,7 @@ import functools
3
3
  import json
4
4
  import logging
5
5
  import pickle
6
+ from collections.abc import Callable
6
7
  from typing import Any
7
8
  from typing import Iterator
8
9
  from typing import Optional
@@ -180,41 +181,48 @@ def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
180
181
  return geojson.dumps(fc)
181
182
 
182
183
 
183
- def make_geojson_color_line(time_series: pd.DataFrame) -> str:
184
- speed_without_na = time_series["speed"].dropna()
185
- low = min(speed_without_na)
186
- high = max(speed_without_na)
187
- clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
184
+ def inter_quartile_range(values):
185
+ return np.quantile(values, 0.75) - np.quantile(values, 0.25)
186
+
188
187
 
188
+ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
189
+ low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
189
190
  cmap = matplotlib.colormaps["viridis"]
190
191
  features = [
191
192
  geojson.Feature(
192
193
  geometry=geojson.LineString(
193
194
  coordinates=[
194
195
  [row["longitude"], row["latitude"]],
195
- [next["longitude"], next["latitude"]],
196
+ [next_row["longitude"], next_row["latitude"]],
196
197
  ]
197
198
  ),
198
199
  properties={
199
- "speed": next["speed"] if np.isfinite(next["speed"]) else 0.0,
200
- "color": matplotlib.colors.to_hex(cmap(clamp_speed(next["speed"]))),
200
+ "speed": next_row["speed"] if np.isfinite(next_row["speed"]) else 0.0,
201
+ "color": matplotlib.colors.to_hex(cmap(clamp_speed(next_row["speed"]))),
201
202
  },
202
203
  )
203
204
  for _, group in time_series.groupby("segment_id")
204
- for (_, row), (_, next) in zip(group.iterrows(), group.iloc[1:].iterrows())
205
+ for (_, row), (_, next_row) in zip(group.iterrows(), group.iloc[1:].iterrows())
205
206
  ]
206
207
  feature_collection = geojson.FeatureCollection(features)
207
208
  return geojson.dumps(feature_collection)
208
209
 
209
210
 
210
211
  def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
211
- speed_without_na = time_series["speed"].dropna()
212
- low = min(speed_without_na)
213
- high = max(speed_without_na)
212
+ low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
214
213
  cmap = matplotlib.colormaps["viridis"]
215
- clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
216
214
  colors = [
217
215
  (f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
218
216
  for speed in np.linspace(low, high, 10)
219
217
  ]
220
218
  return {"low": low, "high": high, "colors": colors}
219
+
220
+
221
+ def _make_speed_clamp(speeds: pd.Series) -> tuple[float, float, Callable]:
222
+ speed_without_na = speeds.dropna()
223
+ low = min(speed_without_na)
224
+ high = min(
225
+ max(speed_without_na),
226
+ np.median(speed_without_na) + 1.5 * inter_quartile_range(speed_without_na),
227
+ )
228
+ return low, high, lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
@@ -0,0 +1,246 @@
1
+ import collections
2
+ import dataclasses
3
+ import functools
4
+ import logging
5
+ import pathlib
6
+ import time
7
+ import urllib.parse
8
+
9
+ import numpy as np
10
+ import requests
11
+ from PIL import Image
12
+
13
+ from geo_activity_playground.core.config import Config
14
+ from geo_activity_playground.core.tiles import compute_tile_float
15
+ from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ OSM_TILE_SIZE = 256 # OSM tile size in pixel
22
+ OSM_MAX_ZOOM = 19 # OSM maximum zoom level
23
+ MAX_TILE_COUNT = 2000 # maximum number of tiles to download
24
+
25
+ ## Basic data types ##
26
+
27
+
28
+ @dataclasses.dataclass
29
+ class GeoBounds:
30
+ """
31
+ Models an area on the globe as a rectangle of latitude and longitude.
32
+
33
+ Latitude goes from South Pole (-90°) to North Pole (+90°). Longitude goes from West (-180°) to East (+180°). Be careful when converting latitude to Y-coordinates as increasing latitude will mean decreasing Y.
34
+ """
35
+
36
+ lat_min: float
37
+ lon_min: float
38
+ lat_max: float
39
+ lon_max: float
40
+
41
+
42
+ @dataclasses.dataclass
43
+ class TileBounds:
44
+ zoom: int
45
+ x1: float
46
+ y1: float
47
+ x2: float
48
+ y2: float
49
+
50
+ @property
51
+ def width(self) -> float:
52
+ return self.x2 - self.x1
53
+
54
+ @property
55
+ def height(self) -> float:
56
+ return self.y2 - self.y1
57
+
58
+
59
+ @dataclasses.dataclass
60
+ class PixelBounds:
61
+ x1: int
62
+ y1: int
63
+ x2: int
64
+ y2: int
65
+
66
+ @classmethod
67
+ def from_tile_bounds(cls, tile_bounds: TileBounds) -> "PixelBounds":
68
+ return pixel_bounds_from_tile_bounds(tile_bounds)
69
+
70
+ @property
71
+ def width(self) -> int:
72
+ return self.x2 - self.x1
73
+
74
+ @property
75
+ def height(self) -> int:
76
+ return self.y2 - self.y1
77
+
78
+ @property
79
+ def shape(self) -> tuple[int, int]:
80
+ return self.height, self.width
81
+
82
+
83
+ @dataclasses.dataclass
84
+ class RasterMapImage:
85
+ image: np.ndarray
86
+ tile_bounds: TileBounds
87
+ geo_bounds: GeoBounds
88
+ pixel_bounds: PixelBounds
89
+
90
+
91
+ ## Converter functions ##
92
+
93
+
94
+ def pixel_bounds_from_tile_bounds(tile_bounds: TileBounds) -> PixelBounds:
95
+ return PixelBounds(
96
+ int(tile_bounds.x1 * OSM_TILE_SIZE),
97
+ int(tile_bounds.y1 * OSM_TILE_SIZE),
98
+ int(tile_bounds.x2 * OSM_TILE_SIZE),
99
+ int(tile_bounds.y2 * OSM_TILE_SIZE),
100
+ )
101
+
102
+
103
+ def get_sensible_zoom_level(
104
+ bounds: GeoBounds, picture_size: tuple[int, int]
105
+ ) -> TileBounds:
106
+ zoom = OSM_MAX_ZOOM
107
+
108
+ while True:
109
+ x_tile_min, y_tile_max = map(
110
+ int, compute_tile_float(bounds.lat_min, bounds.lon_min, zoom)
111
+ )
112
+ x_tile_max, y_tile_min = map(
113
+ int, compute_tile_float(bounds.lat_max, bounds.lon_max, zoom)
114
+ )
115
+
116
+ x_tile_max += 1
117
+ y_tile_max += 1
118
+
119
+ if (x_tile_max - x_tile_min) * OSM_TILE_SIZE <= picture_size[0] and (
120
+ y_tile_max - y_tile_min
121
+ ) * OSM_TILE_SIZE <= picture_size[1]:
122
+ break
123
+
124
+ zoom -= 1
125
+
126
+ tile_count = (x_tile_max - x_tile_min) * (y_tile_max - y_tile_min)
127
+
128
+ if tile_count > MAX_TILE_COUNT:
129
+ raise RuntimeError("Zoom value too high, too many tiles to download")
130
+
131
+ return TileBounds(zoom, x_tile_min, y_tile_min, x_tile_max, y_tile_max)
132
+
133
+
134
+ @functools.lru_cache()
135
+ def get_tile(zoom: int, x: int, y: int, url_template: str) -> Image.Image:
136
+ destination = osm_tile_path(x, y, zoom, url_template)
137
+ if not destination.exists():
138
+ logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
139
+ url = url_template.format(x=x, y=y, zoom=zoom)
140
+ download_file(url, destination)
141
+ with Image.open(destination) as image:
142
+ image.load()
143
+ image = image.convert("RGB")
144
+ return image
145
+
146
+
147
+ def tile_bounds_around_center(
148
+ tile_center: tuple[float, float], pixel_size: tuple[int, int], zoom: int
149
+ ) -> TileBounds:
150
+ x, y = tile_center
151
+ width = pixel_size[0] / OSM_TILE_SIZE
152
+ height = pixel_size[1] / OSM_TILE_SIZE
153
+ return TileBounds(
154
+ zoom, x - width / 2, y - height / 2, x + width / 2, y + height / 2
155
+ )
156
+
157
+
158
+ def _paste_array(
159
+ target: np.ndarray, source: np.ndarray, offset_0: int, offset_1: int
160
+ ) -> None:
161
+ source_min_0 = 0
162
+ source_min_1 = 0
163
+ source_max_0 = source.shape[0]
164
+ source_max_1 = source.shape[1]
165
+
166
+ target_min_0 = offset_0
167
+ target_min_1 = offset_1
168
+ target_max_0 = offset_0 + source.shape[0]
169
+ target_max_1 = offset_1 + source.shape[1]
170
+
171
+ if target_min_1 < 0:
172
+ source_min_1 -= target_min_1
173
+ target_min_1 = 0
174
+ if target_min_0 < 0:
175
+ source_min_0 -= target_min_0
176
+ target_min_0 = 0
177
+ if target_max_1 > target.shape[1]:
178
+ a = target_max_1 - target.shape[1]
179
+ target_max_1 -= a
180
+ source_max_1 -= a
181
+ if target_max_0 > target.shape[0]:
182
+ a = target_max_0 - target.shape[0]
183
+ target_max_0 -= a
184
+ source_max_0 -= a
185
+
186
+ if source_max_1 < 0 or source_max_0 < 0:
187
+ return
188
+
189
+ target[target_min_0:target_max_0, target_min_1:target_max_1] = source[
190
+ source_min_0:source_max_0, source_min_1:source_max_1
191
+ ]
192
+
193
+
194
+ def map_image_from_tile_bounds(tile_bounds: TileBounds, config: Config) -> np.ndarray:
195
+ pixel_bounds = pixel_bounds_from_tile_bounds(tile_bounds)
196
+ background = np.zeros((pixel_bounds.height, pixel_bounds.width, 3))
197
+
198
+ north_west = np.array([tile_bounds.x1, tile_bounds.y1])
199
+ offset = north_west % 1
200
+ tile_anchor = north_west - offset
201
+ pixel_anchor: np.ndarray = np.array([0, 0]) - np.array(
202
+ offset * OSM_TILE_SIZE, dtype=np.int64
203
+ )
204
+
205
+ num_tile_x = int(np.ceil(tile_bounds.width)) + 1
206
+ num_tile_y = int(np.ceil(tile_bounds.height)) + 1
207
+
208
+ for x in range(int(tile_anchor[0]), int(tile_anchor[0] + num_tile_x)):
209
+ for y in range(int(tile_anchor[1]), int(tile_anchor[1]) + num_tile_y):
210
+ tile = np.array(get_tile(tile_bounds.zoom, x, y, config.map_tile_url)) / 255
211
+ _paste_array(
212
+ background,
213
+ tile,
214
+ (y - int(tile_anchor[1])) * OSM_TILE_SIZE + int(pixel_anchor[1]),
215
+ (x - int(tile_anchor[0])) * OSM_TILE_SIZE + int(pixel_anchor[0]),
216
+ )
217
+
218
+ return background
219
+
220
+
221
+ def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
222
+ image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
223
+ image = np.dstack((image, image, image))
224
+ return image
225
+
226
+
227
+ def osm_tile_path(x: int, y: int, zoom: int, url_template: str) -> pathlib.Path:
228
+ base_dir = pathlib.Path("Open Street Map Tiles")
229
+ dir_for_source = base_dir / urllib.parse.quote_plus(url_template)
230
+ path = dir_for_source / f"{zoom}/{x}/{y}.png"
231
+ path.parent.mkdir(parents=True, exist_ok=True)
232
+ return path
233
+
234
+
235
+ def download_file(url: str, destination: pathlib.Path):
236
+ if not destination.parent.exists():
237
+ destination.parent.mkdir(exist_ok=True, parents=True)
238
+ r = requests.get(
239
+ url,
240
+ allow_redirects=True,
241
+ headers={"User-Agent": "Martin's Geo Activity Playground"},
242
+ )
243
+ assert r.ok
244
+ with open(destination, "wb") as f:
245
+ f.write(r.content)
246
+ time.sleep(0.1)
@@ -1,34 +1,12 @@
1
- import functools
2
1
  import logging
3
2
  import math
4
- import pathlib
5
- import time
6
- import urllib.parse
7
3
  from typing import Iterator
8
4
  from typing import Optional
9
5
 
10
6
  import numpy as np
11
- import requests
12
- from PIL import Image
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- def osm_tile_path(x: int, y: int, zoom: int, url_template: str) -> pathlib.Path:
18
- base_dir = pathlib.Path("Open Street Map Tiles")
19
- dir_for_source = base_dir / urllib.parse.quote_plus(url_template)
20
- path = dir_for_source / f"{zoom}/{x}/{y}.png"
21
- path.parent.mkdir(parents=True, exist_ok=True)
22
- return path
23
7
 
24
8
 
25
- def compute_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
26
- x = np.radians(lon)
27
- y = np.arcsinh(np.tan(np.radians(lat)))
28
- x = (1 + x / np.pi) / 2
29
- y = (1 - y / np.pi) / 2
30
- n = 2**zoom
31
- return int(x * n), int(y * n)
9
+ logger = logging.getLogger(__name__)
32
10
 
33
11
 
34
12
  def compute_tile_float(lat: float, lon: float, zoom: int) -> tuple[float, float]:
@@ -40,6 +18,11 @@ def compute_tile_float(lat: float, lon: float, zoom: int) -> tuple[float, float]
40
18
  return x * n, y * n
41
19
 
42
20
 
21
+ def compute_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
22
+ x, y = compute_tile_float(lat, lon, zoom)
23
+ return int(x), int(y)
24
+
25
+
43
26
  def get_tile_upper_left_lat_lon(
44
27
  tile_x: int, tile_y: int, zoom: int
45
28
  ) -> tuple[float, float]:
@@ -50,33 +33,6 @@ def get_tile_upper_left_lat_lon(
50
33
  return lat_deg, lon_deg
51
34
 
52
35
 
53
- def download_file(url: str, destination: pathlib.Path):
54
- if not destination.parent.exists():
55
- destination.parent.mkdir(exist_ok=True, parents=True)
56
- r = requests.get(
57
- url,
58
- allow_redirects=True,
59
- headers={"User-Agent": "Martin's Geo Activity Playground"},
60
- )
61
- assert r.ok
62
- with open(destination, "wb") as f:
63
- f.write(r.content)
64
- time.sleep(0.1)
65
-
66
-
67
- @functools.lru_cache()
68
- def get_tile(zoom: int, x: int, y: int, url_template: str) -> Image.Image:
69
- destination = osm_tile_path(x, y, zoom, url_template)
70
- if not destination.exists():
71
- logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
72
- url = url_template.format(x=x, y=y, zoom=zoom)
73
- download_file(url, destination)
74
- with Image.open(destination) as image:
75
- image.load()
76
- image = image.convert("RGB")
77
- return image
78
-
79
-
80
36
  def xy_to_latlon(x: float, y: float, zoom: int) -> tuple[float, float]:
81
37
  """
82
38
  Returns (lat, lon) in degree from OSM coordinates (x,y) rom https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
@@ -13,7 +13,7 @@ from PIL import Image
13
13
  from PIL import ImageEnhance
14
14
  from tqdm import tqdm
15
15
 
16
- from ..core.tiles import get_tile
16
+ from ..core.raster_map import get_tile
17
17
 
18
18
  # import scipy.interpolate
19
19
 
@@ -0,0 +1,93 @@
1
+ import collections
2
+ import os
3
+ import pathlib
4
+
5
+ import matplotlib.pyplot as pl
6
+ import numpy as np
7
+ import pandas as pd
8
+ from PIL import Image
9
+ from PIL import ImageDraw
10
+ from tqdm import tqdm
11
+
12
+ from geo_activity_playground.core.activities import ActivityRepository
13
+ from geo_activity_playground.core.config import ConfigAccessor
14
+ from geo_activity_playground.core.raster_map import convert_to_grayscale
15
+ from geo_activity_playground.core.raster_map import map_image_from_tile_bounds
16
+ from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
17
+ from geo_activity_playground.core.raster_map import tile_bounds_around_center
18
+ from geo_activity_playground.core.tiles import compute_tile_float
19
+
20
+
21
+ def main_heatmap_video(options) -> None:
22
+ zoom: int = options.zoom
23
+ print(options)
24
+ video_size = options.video_width, options.video_height
25
+ os.chdir(options.basedir)
26
+
27
+ repository = ActivityRepository()
28
+ repository.reload()
29
+ assert len(repository) > 0
30
+ config_accessor = ConfigAccessor()
31
+
32
+ center_xy = compute_tile_float(options.latitude, options.longitude, zoom)
33
+
34
+ tile_bounds = tile_bounds_around_center(center_xy, video_size, zoom)
35
+ background = map_image_from_tile_bounds(tile_bounds, config_accessor())
36
+
37
+ background = convert_to_grayscale(background)
38
+ background = 1.0 - background # invert colors
39
+
40
+ activities_per_day = collections.defaultdict(set)
41
+ for activity in tqdm(
42
+ repository.iter_activities(), desc="Gather activities per day"
43
+ ):
44
+ activities_per_day[activity["start"].date()].add(activity["id"])
45
+
46
+ running_counts = np.zeros(background.shape[:2], np.float64)
47
+
48
+ output_dir = pathlib.Path("Heatmap Video")
49
+ output_dir.mkdir(exist_ok=True)
50
+
51
+ first_day = min(activities_per_day)
52
+ last_day = max(activities_per_day)
53
+ days = pd.date_range(first_day, last_day)
54
+ for current_day in tqdm(days, desc="Generate video frames"):
55
+ for activity_id in activities_per_day[current_day.date()]:
56
+ im = Image.new("L", video_size)
57
+ draw = ImageDraw.Draw(im)
58
+
59
+ time_series = repository.get_time_series(activity_id)
60
+ for _, group in time_series.groupby("segment_id"):
61
+ tile_xz = group["x"] * 2**zoom
62
+ tile_yz = group["y"] * 2**zoom
63
+
64
+ xy_pixels = list(
65
+ zip(
66
+ (tile_xz - center_xy[0]) * OSM_TILE_SIZE
67
+ + options.video_width / 2,
68
+ (tile_yz - center_xy[1]) * OSM_TILE_SIZE
69
+ + options.video_height / 2,
70
+ )
71
+ )
72
+ pixels = [int(value) for t in xy_pixels for value in t]
73
+ draw.line(pixels, fill=1, width=max(3, 6 * (zoom - 17)))
74
+ aim = np.array(im)
75
+ running_counts += aim
76
+
77
+ tile_counts = np.sqrt(running_counts) / 5
78
+ tile_counts[tile_counts > 1.0] = 1.0
79
+
80
+ cmap = pl.get_cmap(config_accessor().color_scheme_for_heatmap)
81
+ data_color = cmap(tile_counts)
82
+ data_color[data_color == cmap(0.0)] = 0.0 # remove background color
83
+
84
+ rendered = np.zeros_like(background)
85
+ for c in range(3):
86
+ rendered[:, :, c] = (1.0 - data_color[:, :, c]) * background[
87
+ :, :, c
88
+ ] + data_color[:, :, c]
89
+
90
+ img = Image.fromarray((rendered * 255).astype("uint8"), "RGB")
91
+ img.save(output_dir / f"{current_day.date()}.png", format="png")
92
+
93
+ running_counts *= 1 - options.decay
@@ -24,7 +24,7 @@ class ActivityParseError(BaseException):
24
24
 
25
25
 
26
26
  def read_activity(path: pathlib.Path) -> tuple[ActivityMeta, pd.DataFrame]:
27
- suffixes = path.suffixes
27
+ suffixes = [s.lower() for s in path.suffixes]
28
28
  metadata = ActivityMeta()
29
29
 
30
30
  if suffixes[-1] == ".gz":
@@ -25,7 +25,7 @@ ACTIVITY_DIR = pathlib.Path("Activities")
25
25
 
26
26
 
27
27
  def import_from_directory(
28
- metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
28
+ metadata_extraction_regexes: list[str], config: Config
29
29
  ) -> None:
30
30
 
31
31
  activity_paths = [
@@ -63,20 +63,11 @@ def import_from_directory(
63
63
  del file_hashes[deleted_file]
64
64
  work_tracker.discard(deleted_file)
65
65
 
66
- if num_processes == 1:
67
- paths_with_errors = []
68
- for path in tqdm(new_activity_paths, desc="Parse activity metadata (serially)"):
69
- errors = _cache_single_file(path)
70
- if errors:
71
- paths_with_errors.append(errors)
72
- else:
73
- with multiprocessing.Pool(num_processes) as pool:
74
- paths_with_errors = tqdm(
75
- pool.imap(_cache_single_file, new_activity_paths),
76
- desc="Parse activity metadata (concurrently)",
77
- total=len(new_activity_paths),
78
- )
79
- paths_with_errors = [error for error in paths_with_errors if error]
66
+ paths_with_errors = []
67
+ for path in tqdm(new_activity_paths, desc="Parse activity metadata (serially)"):
68
+ errors = _cache_single_file(path)
69
+ if errors:
70
+ paths_with_errors.append(errors)
80
71
 
81
72
  for path in tqdm(new_activity_paths, desc="Collate activity metadata"):
82
73
  activity_id = get_file_hash(path)
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import urllib.parse
3
- from collections.abc import Collection
4
3
 
5
4
  from flask import Blueprint
6
5
  from flask import redirect
@@ -9,26 +8,20 @@ from flask import request
9
8
  from flask import Response
10
9
  from flask import url_for
11
10
 
12
- from ...core.activities import ActivityRepository
13
- from ...explorer.tile_visits import TileVisitAccessor
14
- from .controller import ActivityController
15
- from geo_activity_playground.core.config import Config
11
+ from geo_activity_playground.core.activities import ActivityRepository
16
12
  from geo_activity_playground.core.paths import activity_meta_override_dir
17
- from geo_activity_playground.core.privacy_zones import PrivacyZone
13
+ from geo_activity_playground.webui.activity.controller import ActivityController
18
14
  from geo_activity_playground.webui.authenticator import Authenticator
19
15
  from geo_activity_playground.webui.authenticator import needs_authentication
20
16
 
21
17
 
22
18
  def make_activity_blueprint(
19
+ activity_controller: ActivityController,
23
20
  repository: ActivityRepository,
24
- tile_visit_accessor: TileVisitAccessor,
25
- config: Config,
26
21
  authenticator: Authenticator,
27
22
  ) -> Blueprint:
28
23
  blueprint = Blueprint("activity", __name__, template_folder="templates")
29
24
 
30
- activity_controller = ActivityController(repository, tile_visit_accessor, config)
31
-
32
25
  @blueprint.route("/all")
33
26
  def all():
34
27
  return render_template(