geo-activity-playground 0.34.2__py3-none-any.whl → 0.35.1__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 (25) hide show
  1. geo_activity_playground/__main__.py +12 -0
  2. geo_activity_playground/core/raster_map.py +250 -0
  3. geo_activity_playground/core/tiles.py +6 -50
  4. geo_activity_playground/explorer/video.py +1 -1
  5. geo_activity_playground/heatmap_video.py +93 -0
  6. geo_activity_playground/importers/activity_parsers.py +1 -1
  7. geo_activity_playground/webui/activity/blueprint.py +3 -10
  8. geo_activity_playground/webui/activity/controller.py +10 -71
  9. geo_activity_playground/webui/app.py +44 -26
  10. geo_activity_playground/webui/calendar/blueprint.py +2 -5
  11. geo_activity_playground/webui/eddington/blueprint.py +1 -4
  12. geo_activity_playground/webui/equipment/blueprint.py +2 -8
  13. geo_activity_playground/webui/explorer/blueprint.py +2 -10
  14. geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +8 -6
  15. geo_activity_playground/webui/heatmap/heatmap_controller.py +10 -12
  16. geo_activity_playground/webui/summary/blueprint.py +1 -4
  17. geo_activity_playground/webui/summary/controller.py +0 -1
  18. geo_activity_playground/webui/tile/blueprint.py +1 -4
  19. geo_activity_playground/webui/tile/controller.py +1 -1
  20. {geo_activity_playground-0.34.2.dist-info → geo_activity_playground-0.35.1.dist-info}/METADATA +1 -1
  21. {geo_activity_playground-0.34.2.dist-info → geo_activity_playground-0.35.1.dist-info}/RECORD +24 -23
  22. geo_activity_playground/core/heatmap.py +0 -194
  23. {geo_activity_playground-0.34.2.dist-info → geo_activity_playground-0.35.1.dist-info}/LICENSE +0 -0
  24. {geo_activity_playground-0.34.2.dist-info → geo_activity_playground-0.35.1.dist-info}/WHEEL +0 -0
  25. {geo_activity_playground-0.34.2.dist-info → geo_activity_playground-0.35.1.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",
@@ -0,0 +1,250 @@
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: int
46
+ y1: int
47
+ x2: int
48
+ y2: int
49
+
50
+ @property
51
+ def width(self) -> int:
52
+ return self.x2 - self.x1
53
+
54
+ @property
55
+ def height(self) -> int:
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 tile_bounds_from_geo_bounds(geo_bounds: GeoBounds) -> TileBounds:
95
+ x1, y1 = compute_tile_float(geo_bounds.lat_max, geo_bounds.lon_min)
96
+ x2, y2 = compute_tile_float(geo_bounds.lat_min, geo_bounds.lon_min)
97
+ return TileBounds(x1, y1, x2, y2)
98
+
99
+
100
+ def pixel_bounds_from_tile_bounds(tile_bounds: TileBounds) -> PixelBounds:
101
+ return PixelBounds(
102
+ int(tile_bounds.x1 * OSM_TILE_SIZE),
103
+ int(tile_bounds.y1 * OSM_TILE_SIZE),
104
+ int(tile_bounds.x2 * OSM_TILE_SIZE),
105
+ int(tile_bounds.y2 * OSM_TILE_SIZE),
106
+ )
107
+
108
+
109
+ def get_sensible_zoom_level(
110
+ bounds: GeoBounds, picture_size: tuple[int, int]
111
+ ) -> TileBounds:
112
+ zoom = OSM_MAX_ZOOM
113
+
114
+ while True:
115
+ x_tile_min, y_tile_max = map(
116
+ int, compute_tile_float(bounds.lat_min, bounds.lon_min, zoom)
117
+ )
118
+ x_tile_max, y_tile_min = map(
119
+ int, compute_tile_float(bounds.lat_max, bounds.lon_max, zoom)
120
+ )
121
+
122
+ x_tile_max += 1
123
+ y_tile_max += 1
124
+
125
+ if (x_tile_max - x_tile_min) * OSM_TILE_SIZE <= picture_size[0] and (
126
+ y_tile_max - y_tile_min
127
+ ) * OSM_TILE_SIZE <= picture_size[1]:
128
+ break
129
+
130
+ zoom -= 1
131
+
132
+ tile_count = (x_tile_max - x_tile_min) * (y_tile_max - y_tile_min)
133
+
134
+ if tile_count > MAX_TILE_COUNT:
135
+ raise RuntimeError("Zoom value too high, too many tiles to download")
136
+
137
+ return TileBounds(zoom, x_tile_min, y_tile_min, x_tile_max, y_tile_max)
138
+
139
+
140
+ @functools.lru_cache()
141
+ def get_tile(zoom: int, x: int, y: int, url_template: str) -> Image.Image:
142
+ destination = osm_tile_path(x, y, zoom, url_template)
143
+ if not destination.exists():
144
+ logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
145
+ url = url_template.format(x=x, y=y, zoom=zoom)
146
+ download_file(url, destination)
147
+ with Image.open(destination) as image:
148
+ image.load()
149
+ image = image.convert("RGB")
150
+ return image
151
+
152
+
153
+ def tile_bounds_around_center(
154
+ tile_center: tuple[float, float], pixel_size: tuple[int, int], zoom: int
155
+ ) -> TileBounds:
156
+ x, y = tile_center
157
+ width = pixel_size[0] / OSM_TILE_SIZE
158
+ height = pixel_size[1] / OSM_TILE_SIZE
159
+ return TileBounds(
160
+ zoom, x - width / 2, y - height / 2, x + width / 2, y + height / 2
161
+ )
162
+
163
+
164
+ def _paste_array(
165
+ target: np.ndarray, source: np.ndarray, offset_0: int, offset_1: int
166
+ ) -> None:
167
+ source_min_0 = 0
168
+ source_min_1 = 0
169
+ source_max_0 = source.shape[0]
170
+ source_max_1 = source.shape[1]
171
+
172
+ target_min_0 = offset_0
173
+ target_min_1 = offset_1
174
+ target_max_0 = offset_0 + source.shape[0]
175
+ target_max_1 = offset_1 + source.shape[1]
176
+
177
+ if target_min_1 < 0:
178
+ source_min_1 -= target_min_1
179
+ target_min_1 = 0
180
+ if target_min_0 < 0:
181
+ source_min_0 -= target_min_0
182
+ target_min_0 = 0
183
+ if target_max_1 > target.shape[1]:
184
+ a = target_max_1 - target.shape[1]
185
+ target_max_1 -= a
186
+ source_max_1 -= a
187
+ if target_max_0 > target.shape[0]:
188
+ a = target_max_0 - target.shape[0]
189
+ target_max_0 -= a
190
+ source_max_0 -= a
191
+
192
+ if source_max_1 < 0 or source_max_0 < 0:
193
+ return
194
+
195
+ target[target_min_0:target_max_0, target_min_1:target_max_1] = source[
196
+ source_min_0:source_max_0, source_min_1:source_max_1
197
+ ]
198
+
199
+
200
+ def map_image_from_tile_bounds(tile_bounds: TileBounds, config: Config) -> np.ndarray:
201
+ pixel_bounds = pixel_bounds_from_tile_bounds(tile_bounds)
202
+ background = np.zeros((pixel_bounds.height, pixel_bounds.width, 3))
203
+
204
+ north_west = np.array([tile_bounds.x1, tile_bounds.y1])
205
+ offset = north_west % 1
206
+ tile_anchor = north_west - offset
207
+ pixel_anchor = np.array([0, 0]) - np.array(offset * OSM_TILE_SIZE, dtype=np.int64)
208
+
209
+ num_tile_x = int(np.ceil(tile_bounds.width)) + 1
210
+ num_tile_y = int(np.ceil(tile_bounds.height)) + 1
211
+
212
+ for x in range(int(tile_anchor[0]), int(tile_anchor[0] + num_tile_x)):
213
+ for y in range(int(tile_anchor[1]), int(tile_anchor[1]) + num_tile_y):
214
+ tile = np.array(get_tile(tile_bounds.zoom, x, y, config.map_tile_url)) / 255
215
+ _paste_array(
216
+ background,
217
+ tile,
218
+ (y - int(tile_anchor[1])) * OSM_TILE_SIZE + int(pixel_anchor[1]),
219
+ (x - int(tile_anchor[0])) * OSM_TILE_SIZE + int(pixel_anchor[0]),
220
+ )
221
+
222
+ return background
223
+
224
+
225
+ def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
226
+ image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
227
+ image = np.dstack((image, image, image))
228
+ return image
229
+
230
+
231
+ def osm_tile_path(x: int, y: int, zoom: int, url_template: str) -> pathlib.Path:
232
+ base_dir = pathlib.Path("Open Street Map Tiles")
233
+ dir_for_source = base_dir / urllib.parse.quote_plus(url_template)
234
+ path = dir_for_source / f"{zoom}/{x}/{y}.png"
235
+ path.parent.mkdir(parents=True, exist_ok=True)
236
+ return path
237
+
238
+
239
+ def download_file(url: str, destination: pathlib.Path):
240
+ if not destination.parent.exists():
241
+ destination.parent.mkdir(exist_ok=True, parents=True)
242
+ r = requests.get(
243
+ url,
244
+ allow_redirects=True,
245
+ headers={"User-Agent": "Martin's Geo Activity Playground"},
246
+ )
247
+ assert r.ok
248
+ with open(destination, "wb") as f:
249
+ f.write(r.content)
250
+ 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":
@@ -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(
@@ -12,8 +12,6 @@ import pandas as pd
12
12
  from PIL import Image
13
13
  from PIL import ImageDraw
14
14
 
15
- from ...explorer.grid_file import make_grid_file_geojson
16
- from ...explorer.grid_file import make_grid_points
17
15
  from geo_activity_playground.core.activities import ActivityMeta
18
16
  from geo_activity_playground.core.activities import ActivityRepository
19
17
  from geo_activity_playground.core.activities import make_geojson_color_line
@@ -21,14 +19,13 @@ from geo_activity_playground.core.activities import make_geojson_from_time_serie
21
19
  from geo_activity_playground.core.activities import make_speed_color_bar
22
20
  from geo_activity_playground.core.config import Config
23
21
  from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
24
- from geo_activity_playground.core.heatmap import build_map_from_tiles_around_center
25
- from geo_activity_playground.core.heatmap import GeoBounds
26
- from geo_activity_playground.core.heatmap import OSM_MAX_ZOOM
27
- from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
28
- from geo_activity_playground.core.heatmap import PixelBounds
29
- from geo_activity_playground.core.heatmap import TileBounds
30
22
  from geo_activity_playground.core.privacy_zones import PrivacyZone
31
- from geo_activity_playground.core.tiles import compute_tile_float
23
+ from geo_activity_playground.core.raster_map import map_image_from_tile_bounds
24
+ from geo_activity_playground.core.raster_map import OSM_MAX_ZOOM
25
+ from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
26
+ from geo_activity_playground.core.raster_map import tile_bounds_around_center
27
+ from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
28
+ from geo_activity_playground.explorer.grid_file import make_grid_points
32
29
  from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
33
30
 
34
31
  logger = logging.getLogger(__name__)
@@ -412,62 +409,6 @@ def name_minutes_plot(meta: pd.DataFrame) -> str:
412
409
  )
413
410
 
414
411
 
415
- def make_pixel_bounds_square(bounds: PixelBounds) -> PixelBounds:
416
- x_radius = (bounds.x_max - bounds.x_min) // 2
417
- y_radius = (bounds.y_max - bounds.y_min) // 2
418
- x_center = (bounds.x_max + bounds.x_min) // 2
419
- y_center = (bounds.y_max + bounds.y_min) // 2
420
-
421
- radius = max(x_radius, y_radius)
422
-
423
- return PixelBounds(
424
- x_min=x_center - radius,
425
- y_min=y_center - radius,
426
- x_max=x_center + radius,
427
- y_max=y_center + radius,
428
- )
429
-
430
-
431
- def make_tile_bounds_square(bounds: TileBounds) -> TileBounds:
432
- x_radius = (bounds.x_tile_max - bounds.x_tile_min) / 2
433
- y_radius = (bounds.y_tile_max - bounds.y_tile_min) / 2
434
- x_center = (bounds.x_tile_max + bounds.x_tile_min) / 2
435
- y_center = (bounds.y_tile_max + bounds.y_tile_min) / 2
436
-
437
- radius = max(x_radius, y_radius)
438
-
439
- return TileBounds(
440
- zoom=bounds.zoom,
441
- x_tile_min=int(x_center - radius),
442
- y_tile_min=int(y_center - radius),
443
- x_tile_max=int(np.ceil(x_center + radius)),
444
- y_tile_max=int(np.ceil(y_center + radius)),
445
- )
446
-
447
-
448
- def get_crop_mask(geo_bounds: GeoBounds, tile_bounds: TileBounds) -> PixelBounds:
449
- min_x, min_y = compute_tile_float(
450
- geo_bounds.lat_max, geo_bounds.lon_min, tile_bounds.zoom
451
- )
452
- max_x, max_y = compute_tile_float(
453
- geo_bounds.lat_min, geo_bounds.lon_max, tile_bounds.zoom
454
- )
455
-
456
- crop_mask = PixelBounds(
457
- int((min_x - tile_bounds.x_tile_min) * OSM_TILE_SIZE),
458
- int((max_x - tile_bounds.x_tile_min) * OSM_TILE_SIZE),
459
- int((min_y - tile_bounds.y_tile_min) * OSM_TILE_SIZE),
460
- int((max_y - tile_bounds.y_tile_min) * OSM_TILE_SIZE),
461
- )
462
- crop_mask = make_pixel_bounds_square(crop_mask)
463
-
464
- return crop_mask
465
-
466
-
467
- def pixels_in_bounds(bounds: PixelBounds) -> int:
468
- return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
469
-
470
-
471
412
  def make_sharepic_base(time_series_list: list[pd.DataFrame], config: Config):
472
413
  all_time_series = pd.concat(time_series_list)
473
414
  tile_x = all_time_series["x"]
@@ -496,13 +437,11 @@ def make_sharepic_base(time_series_list: list[pd.DataFrame], config: Config):
496
437
  (tile_yz.max() + tile_yz.min()) / 2,
497
438
  )
498
439
 
499
- background = build_map_from_tiles_around_center(
500
- tile_xz_center,
501
- zoom,
502
- (target_width, target_height),
503
- (target_width, target_map_height),
504
- config,
440
+ tile_bounds = tile_bounds_around_center(
441
+ tile_xz_center, (target_width, target_height - footer_height), zoom
505
442
  )
443
+ tile_bounds.y2 += footer_height / OSM_TILE_SIZE
444
+ background = map_image_from_tile_bounds(tile_bounds, config)
506
445
 
507
446
  img = Image.fromarray((background * 255).astype("uint8"), "RGB")
508
447
  draw = ImageDraw.Draw(img, mode="RGBA")
@@ -9,25 +9,34 @@ import urllib.parse
9
9
  from flask import Flask
10
10
  from flask import render_template
11
11
 
12
- from ..core.activities import ActivityRepository
13
- from ..explorer.tile_visits import TileVisitAccessor
14
- from .activity.blueprint import make_activity_blueprint
15
- from .calendar.blueprint import make_calendar_blueprint
16
- from .eddington.blueprint import make_eddington_blueprint
17
- from .entry_controller import EntryController
18
- from .equipment.blueprint import make_equipment_blueprint
19
- from .explorer.blueprint import make_explorer_blueprint
20
- from .heatmap.blueprint import make_heatmap_blueprint
21
- from .search.blueprint import make_search_blueprint
22
- from .square_planner.blueprint import make_square_planner_blueprint
23
- from .summary.blueprint import make_summary_blueprint
24
- from .tile.blueprint import make_tile_blueprint
25
- from .upload_blueprint import make_upload_blueprint
12
+ from geo_activity_playground.core.activities import ActivityRepository
26
13
  from geo_activity_playground.core.config import Config
27
14
  from geo_activity_playground.core.config import ConfigAccessor
15
+ from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
16
+ from geo_activity_playground.webui.activity.blueprint import make_activity_blueprint
17
+ from geo_activity_playground.webui.activity.controller import ActivityController
28
18
  from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
29
19
  from geo_activity_playground.webui.authenticator import Authenticator
20
+ from geo_activity_playground.webui.calendar.blueprint import make_calendar_blueprint
21
+ from geo_activity_playground.webui.calendar.controller import CalendarController
22
+ from geo_activity_playground.webui.eddington.blueprint import make_eddington_blueprint
23
+ from geo_activity_playground.webui.eddington.controller import EddingtonController
24
+ from geo_activity_playground.webui.entry_controller import EntryController
25
+ from geo_activity_playground.webui.equipment.blueprint import make_equipment_blueprint
26
+ from geo_activity_playground.webui.equipment.controller import EquipmentController
27
+ from geo_activity_playground.webui.explorer.blueprint import make_explorer_blueprint
28
+ from geo_activity_playground.webui.explorer.controller import ExplorerController
29
+ from geo_activity_playground.webui.heatmap.blueprint import make_heatmap_blueprint
30
+ from geo_activity_playground.webui.search.blueprint import make_search_blueprint
30
31
  from geo_activity_playground.webui.settings.blueprint import make_settings_blueprint
32
+ from geo_activity_playground.webui.square_planner.blueprint import (
33
+ make_square_planner_blueprint,
34
+ )
35
+ from geo_activity_playground.webui.summary.blueprint import make_summary_blueprint
36
+ from geo_activity_playground.webui.summary.controller import SummaryController
37
+ from geo_activity_playground.webui.tile.blueprint import make_tile_blueprint
38
+ from geo_activity_playground.webui.tile.controller import TileController
39
+ from geo_activity_playground.webui.upload_blueprint import make_upload_blueprint
31
40
 
32
41
 
33
42
  def route_start(app: Flask, repository: ActivityRepository, config: Config) -> None:
@@ -77,26 +86,35 @@ def web_ui_main(
77
86
 
78
87
  authenticator = Authenticator(config_accessor())
79
88
 
89
+ config = config_accessor()
90
+ summary_controller = SummaryController(repository, config)
91
+ tile_controller = TileController(config)
92
+ activity_controller = ActivityController(repository, tile_visit_accessor, config)
93
+ calendar_controller = CalendarController(repository)
94
+ equipment_controller = EquipmentController(repository, config)
95
+ eddington_controller = EddingtonController(repository)
96
+ explorer_controller = ExplorerController(
97
+ repository, tile_visit_accessor, config_accessor
98
+ )
99
+
80
100
  route_start(app, repository, config_accessor())
81
101
 
82
102
  app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
83
-
84
103
  app.register_blueprint(
85
- make_activity_blueprint(
86
- repository, tile_visit_accessor, config_accessor(), authenticator
87
- ),
104
+ make_activity_blueprint(activity_controller, repository, authenticator),
88
105
  url_prefix="/activity",
89
106
  )
90
- app.register_blueprint(make_calendar_blueprint(repository), url_prefix="/calendar")
91
107
  app.register_blueprint(
92
- make_eddington_blueprint(repository), url_prefix="/eddington"
108
+ make_calendar_blueprint(calendar_controller), url_prefix="/calendar"
109
+ )
110
+ app.register_blueprint(
111
+ make_eddington_blueprint(eddington_controller), url_prefix="/eddington"
93
112
  )
94
113
  app.register_blueprint(
95
- make_equipment_blueprint(repository, config_accessor()), url_prefix="/equipment"
114
+ make_equipment_blueprint(equipment_controller), url_prefix="/equipment"
96
115
  )
97
116
  app.register_blueprint(
98
- make_explorer_blueprint(repository, tile_visit_accessor, config_accessor),
99
- url_prefix="/explorer",
117
+ make_explorer_blueprint(explorer_controller), url_prefix="/explorer"
100
118
  )
101
119
  app.register_blueprint(
102
120
  make_heatmap_blueprint(repository, tile_visit_accessor, config_accessor()),
@@ -115,10 +133,10 @@ def web_ui_main(
115
133
  url_prefix="/search",
116
134
  )
117
135
  app.register_blueprint(
118
- make_summary_blueprint(repository, config_accessor()),
119
- url_prefix="/summary",
136
+ make_summary_blueprint(summary_controller), url_prefix="/summary"
120
137
  )
121
- app.register_blueprint(make_tile_blueprint(config_accessor()), url_prefix="/tile")
138
+
139
+ app.register_blueprint(make_tile_blueprint(tile_controller), url_prefix="/tile")
122
140
  app.register_blueprint(
123
141
  make_upload_blueprint(
124
142
  repository, tile_visit_accessor, config_accessor(), authenticator
@@ -1,15 +1,12 @@
1
1
  from flask import Blueprint
2
2
  from flask import render_template
3
3
 
4
- from ...core.activities import ActivityRepository
5
- from .controller import CalendarController
4
+ from geo_activity_playground.webui.calendar.controller import CalendarController
6
5
 
7
6
 
8
- def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
7
+ def make_calendar_blueprint(calendar_controller: CalendarController) -> Blueprint:
9
8
  blueprint = Blueprint("calendar", __name__, template_folder="templates")
10
9
 
11
- calendar_controller = CalendarController(repository)
12
-
13
10
  @blueprint.route("/")
14
11
  def index():
15
12
  return render_template(
@@ -1,15 +1,12 @@
1
1
  from flask import Blueprint
2
2
  from flask import render_template
3
3
 
4
- from ...core.activities import ActivityRepository
5
4
  from .controller import EddingtonController
6
5
 
7
6
 
8
- def make_eddington_blueprint(repository: ActivityRepository) -> Blueprint:
7
+ def make_eddington_blueprint(eddington_controller: EddingtonController) -> Blueprint:
9
8
  blueprint = Blueprint("eddington", __name__, template_folder="templates")
10
9
 
11
- eddington_controller = EddingtonController(repository)
12
-
13
10
  @blueprint.route("/")
14
11
  def index():
15
12
  return render_template(
@@ -1,18 +1,12 @@
1
1
  from flask import Blueprint
2
2
  from flask import render_template
3
3
 
4
- from ...core.activities import ActivityRepository
5
- from .controller import EquipmentController
6
- from geo_activity_playground.core.config import Config
4
+ from geo_activity_playground.webui.equipment.controller import EquipmentController
7
5
 
8
6
 
9
- def make_equipment_blueprint(
10
- repository: ActivityRepository, config: Config
11
- ) -> Blueprint:
7
+ def make_equipment_blueprint(equipment_controller: EquipmentController) -> Blueprint:
12
8
  blueprint = Blueprint("equipment", __name__, template_folder="templates")
13
9
 
14
- equipment_controller = EquipmentController(repository, config)
15
-
16
10
  @blueprint.route("/")
17
11
  def index():
18
12
  return render_template(
@@ -4,20 +4,12 @@ from flask import render_template
4
4
  from flask import Response
5
5
  from flask import url_for
6
6
 
7
- from ...core.activities import ActivityRepository
8
- from ...explorer.tile_visits import TileVisitAccessor
9
- from .controller import ExplorerController
10
- from geo_activity_playground.core.config import ConfigAccessor
7
+ from geo_activity_playground.webui.explorer.controller import ExplorerController
11
8
 
12
9
 
13
10
  def make_explorer_blueprint(
14
- repository: ActivityRepository,
15
- tile_visit_accessor: TileVisitAccessor,
16
- config_accessor: ConfigAccessor,
11
+ explorer_controller: ExplorerController,
17
12
  ) -> Blueprint:
18
- explorer_controller = ExplorerController(
19
- repository, tile_visit_accessor, config_accessor
20
- )
21
13
  blueprint = Blueprint("explorer", __name__, template_folder="templates")
22
14
 
23
15
  @blueprint.route("/<zoom>")
@@ -34,16 +34,15 @@
34
34
  <div class="row mb-3">
35
35
  <div class="col">
36
36
  <div class="btn-group mb-3" role="group">
37
- <button type="button" class="btn btn-secondary" onclick="changeColor('cluster')">Cluster</button>
38
- <button type="button" class="btn btn-secondary" onclick="changeColor('first')">First Visit</button>
39
- <button type="button" class="btn btn-secondary" onclick="changeColor('last')">Last Visit</button>
37
+ <button type="button" class="btn btn-primary" onclick="changeColor('cluster')">Cluster</button>
38
+ <button type="button" class="btn btn-primary" onclick="changeColor('first')">First Visit</button>
39
+ <button type="button" class="btn btn-primary" onclick="changeColor('last')">Last Visit</button>
40
40
  </div>
41
41
  <div id="explorer-map" class="mb-1" style="height: 800px;"></div>
42
42
  <p>Download tiles in visible area: <a href="#" onclick="downloadAs('explored.geojson')">Explored as GeoJSON</a>,
43
43
  <a href="#" onclick="downloadAs('explored.gpx')"">Explored as GPX</a>, <a href=" #"
44
44
  onclick="downloadAs('missing.geojson')">Missing as GeoJSON</a> or <a href="#"
45
45
  onclick="downloadAs('missing.gpx')"">Missing as GPX</a>.</p>
46
-
47
46
  <script>
48
47
  function onEachFeature(feature, layer) {
49
48
  if (feature.properties && feature.properties.first_visit) {
@@ -71,10 +70,11 @@
71
70
  center: [{{ center.latitude }}, {{ center.longitude }}],
72
71
  zoom: 10
73
72
  })
74
- L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
73
+ let tile_layer = L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
75
74
  maxZoom: 19,
76
75
  attribution: '{{ map_tile_attribution|safe }}'
77
- }).addTo(map)
76
+ });
77
+ tile_layer.addTo(map)
78
78
  let explorer_layer_cluster_color = L.geoJSON(explorer_geojson, {
79
79
  style: function (feature) {
80
80
  return {
@@ -115,6 +115,8 @@
115
115
  bounds = map.getBounds(); zoom = "{{ zoom }}"
116
116
  window.location.href = `/explorer/${zoom}/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/${suffix}`
117
117
  } </script>
118
+ <p></p><button type="button" class="btn btn-secondary btn-sm"
119
+ onclick="map.removeLayer(tile_layer)">Remove map background</button>
118
120
  </div>
119
121
  </div>
120
122
 
@@ -9,12 +9,12 @@ from PIL import ImageDraw
9
9
 
10
10
  from geo_activity_playground.core.activities import ActivityRepository
11
11
  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
12
+ from geo_activity_playground.core.raster_map import convert_to_grayscale
13
+ from geo_activity_playground.core.raster_map import GeoBounds
14
+ from geo_activity_playground.core.raster_map import get_sensible_zoom_level
15
+ from geo_activity_playground.core.raster_map import get_tile
16
+ from geo_activity_playground.core.raster_map import PixelBounds
16
17
  from geo_activity_playground.core.tasks import work_tracker
17
- from geo_activity_playground.core.tiles import get_tile
18
18
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
19
19
  from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
20
20
  from geo_activity_playground.webui.explorer.controller import (
@@ -117,9 +117,7 @@ class HeatmapController:
117
117
  time_series = self._repository.get_time_series(activity_id)
118
118
  for _, group in time_series.groupby("segment_id"):
119
119
  xy_pixels = (
120
- np.array(
121
- [group["x"] * 2**z - x, group["y"] * 2**z - y]
122
- ).T
120
+ np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
123
121
  * OSM_TILE_SIZE
124
122
  )
125
123
  im = Image.new("L", tile_pixels)
@@ -171,8 +169,8 @@ class HeatmapController:
171
169
  pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
172
170
 
173
171
  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):
172
+ for x in range(tile_bounds.x1, tile_bounds.x2):
173
+ for y in range(tile_bounds.y1, tile_bounds.y2):
176
174
  tile = (
177
175
  np.array(
178
176
  get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
@@ -180,8 +178,8 @@ class HeatmapController:
180
178
  / 255
181
179
  )
182
180
 
183
- i = y - tile_bounds.y_tile_min
184
- j = x - tile_bounds.x_tile_min
181
+ i = y - tile_bounds.y1
182
+ j = x - tile_bounds.x1
185
183
 
186
184
  background[
187
185
  i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
@@ -1,13 +1,10 @@
1
1
  from flask import Blueprint
2
2
  from flask import render_template
3
3
 
4
- from ...core.activities import ActivityRepository
5
4
  from .controller import SummaryController
6
- from geo_activity_playground.core.config import Config
7
5
 
8
6
 
9
- def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
10
- summary_controller = SummaryController(repository, config)
7
+ def make_summary_blueprint(summary_controller: SummaryController) -> Blueprint:
11
8
  blueprint = Blueprint("summary", __name__, template_folder="templates")
12
9
 
13
10
  @blueprint.route("/")
@@ -1,7 +1,6 @@
1
1
  import collections
2
2
  import datetime
3
3
  import functools
4
- from typing import Optional
5
4
 
6
5
  import altair as alt
7
6
  import pandas as pd
@@ -2,14 +2,11 @@ from flask import Blueprint
2
2
  from flask import Response
3
3
 
4
4
  from .controller import TileController
5
- from geo_activity_playground.core.config import Config
6
5
 
7
6
 
8
- def make_tile_blueprint(config: Config) -> Blueprint:
7
+ def make_tile_blueprint(tile_controller: TileController) -> Blueprint:
9
8
  blueprint = Blueprint("tiles", __name__, template_folder="templates")
10
9
 
11
- tile_controller = TileController(config)
12
-
13
10
  @blueprint.route("/color/<z>/<x>/<y>.png")
14
11
  def tile_color(x: str, y: str, z: str):
15
12
  return Response(
@@ -4,7 +4,7 @@ import matplotlib.pyplot as pl
4
4
  import numpy as np
5
5
 
6
6
  from geo_activity_playground.core.config import Config
7
- from geo_activity_playground.core.tiles import get_tile
7
+ from geo_activity_playground.core.raster_map import get_tile
8
8
 
9
9
 
10
10
  class TileController:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.34.2
3
+ Version: 0.35.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -1,26 +1,27 @@
1
1
  geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- geo_activity_playground/__main__.py,sha256=qbKjs7IsJwF2gB3b0lh7tGt6uraPqq5w-xGBIxrgoHI,3988
2
+ geo_activity_playground/__main__.py,sha256=vFKB0Jjk8ImDHqok6Fddb5CRWHEnxyn5r4Zs0A1EsCQ,4585
3
3
  geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  geo_activity_playground/core/activities.py,sha256=MiQev1L7NnSVnxgbArlT-FpWTESTwnKh7pyT1tcEHqU,6926
5
5
  geo_activity_playground/core/config.py,sha256=uqiwk7CgcuGx8JemHSsRKjRwyNT1YTb7V0gX0OJhfaI,5109
6
6
  geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
7
7
  geo_activity_playground/core/enrichment.py,sha256=fUmk6avy_rqePlHmJQFTQhAxjgIRaxxmq18N2OSXBBg,7771
8
8
  geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
9
- geo_activity_playground/core/heatmap.py,sha256=iTxefUTjTToPrKpVbauJHXkqxpNppXOEK6vvKuNkHkk,5906
10
9
  geo_activity_playground/core/paths.py,sha256=RBeUi38riP_msTGPy1TsPRNiblzE-lFivaJSLULE8b0,2503
11
10
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
11
+ geo_activity_playground/core/raster_map.py,sha256=yoq5s883BEA2v5C6EYVynkOwynJMka9d2rmoa-W1uIE,7178
12
12
  geo_activity_playground/core/similarity.py,sha256=Jo8jRViuORCxdIGvyaflgsQhwu9S_jn10a450FRL18A,3159
13
13
  geo_activity_playground/core/tasks.py,sha256=aMDBWJqp6ek2ao6G6Xa8GOSZbcQqXoWL74SGRowRPIk,2942
14
14
  geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
15
15
  geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
16
- geo_activity_playground/core/tiles.py,sha256=qUe4h3rzHJb8xThAgUKSLElt8S6zID_OuaWXDkWLwAU,3539
16
+ geo_activity_playground/core/tiles.py,sha256=lV6X1Uc9XQecu2LALIvxpnMcLsVtWx7JczJ5a_S1eZE,2139
17
17
  geo_activity_playground/core/time_conversion.py,sha256=x5mXG6Y4GtdX7CBmwucGNSWBp9JQJDbZ7u0JkdUY1Vs,379
18
18
  geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  geo_activity_playground/explorer/grid_file.py,sha256=k6j6KBEk2a2BY-onE8SV5TJsERGGyOrlY4as__meWpA,3304
20
20
  geo_activity_playground/explorer/tile_visits.py,sha256=CSHAjgzKWe1iB-zvaqgsR5Z_lFycpWqUfxnPCAWvYaU,14173
21
- geo_activity_playground/explorer/video.py,sha256=35-mMEvD8phnc2xbWdwCHhl_uMIUogHrnFwrTfk2Yj8,4392
21
+ geo_activity_playground/explorer/video.py,sha256=7j6Qv3HG6On7Tn7xh7Olwrx_fbQnfzS7CeRg3TEApHg,4397
22
+ geo_activity_playground/heatmap_video.py,sha256=Oc3EAAsW27zhG28Rmy3i2GoN1rjm1UFgb53eSHr9GP8,3503
22
23
  geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- geo_activity_playground/importers/activity_parsers.py,sha256=XNQs0ziqAcVqIoiLAX5Ndmhb6v__29XdjUPvNvc7oBI,11082
24
+ geo_activity_playground/importers/activity_parsers.py,sha256=5MvtjmXruPAoUO1KlDycoj677NeETgODiyxsFjXNppg,11103
24
25
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
25
26
  geo_activity_playground/importers/directory.py,sha256=CA-vFOMm8G4MSM_Q09OwQKduCApL2PWaxLTVxgw_xpw,5908
26
27
  geo_activity_playground/importers/strava_api.py,sha256=cJCZsLemhOlxTtZh0z_npidgae9SD5HyEUry2uvem_A,7775
@@ -30,38 +31,38 @@ geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQ
30
31
  geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
31
32
  geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
33
  geo_activity_playground/webui/activity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- geo_activity_playground/webui/activity/blueprint.py,sha256=2Fa_6a86Aa3Yr3hQdkRylQqqVusyyVzPd1x3il5ZXNk,4176
34
- geo_activity_playground/webui/activity/controller.py,sha256=0HXQZAD9Muf62jV-GqQKJPq5qNgdbV-KEcPmHX_8iGo,21429
34
+ geo_activity_playground/webui/activity/blueprint.py,sha256=J2f6zzBtwkrem51WDakLXKahVcOxMT2JKbbFgu0VFws,3914
35
+ geo_activity_playground/webui/activity/controller.py,sha256=Gps0qGa3vd3xvBW4EIpwoQxYbkYItFfzlz0tzaj8HCA,19508
35
36
  geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=wkYmcnIsMlvE9wgKemYCNU6jwsk5IJvg8pcBA2OMh00,2795
36
37
  geo_activity_playground/webui/activity/templates/activity/edit.html.j2,sha256=ckcTTxcQOhmvvAGNTEOtWCUG4LhvO4HfQImlIa5qKs8,1530
37
38
  geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=_ZDg1ruW-9UMJfOudy1-uY_-IcSSaagq7tPCih5Bb8g,1079
38
39
  geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=tKviMqMouHEGv3xBQVIsJgwj_hjwAsmGVefM3UMqlYg,2437
39
40
  geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=bEx37UGSTeeJl7gN4fjyOpINFQwZ5Zm-HOKpLqcJGfs,6905
40
- geo_activity_playground/webui/app.py,sha256=KghChBgJvRZ9Cx7Z5wmqx6X0Q6GH-2hi97eaKEE_Zvc,5118
41
+ geo_activity_playground/webui/app.py,sha256=MXjd5iOYY7q_wGYgMKOj0T2z7cJ9KxpevVGHD5V95ws,6521
41
42
  geo_activity_playground/webui/auth/blueprint.py,sha256=Lx-ZvMnfHLC1CMre1xPQI3k_pCtQoZvgRhtmafULzoE,812
42
43
  geo_activity_playground/webui/auth/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
43
44
  geo_activity_playground/webui/authenticator.py,sha256=k278OEVuOfAmTGT4F2X4pqSTwwkK_FA87EIhAeysEqc,1416
44
45
  geo_activity_playground/webui/calendar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- geo_activity_playground/webui/calendar/blueprint.py,sha256=rlnhgU2DWAcdLMRq7m77NzrM_aDyp4s3kuuQHuzjHhg,782
46
+ geo_activity_playground/webui/calendar/blueprint.py,sha256=Kjyx9lkJoM5tL8vNba0Y7HhJuFRVId4F-ONZsXpKyyg,721
46
47
  geo_activity_playground/webui/calendar/controller.py,sha256=QpSAkR2s1sbLSu6P_fNNTccgGglOzEH2PIv1XwKxeVY,2778
47
48
  geo_activity_playground/webui/calendar/templates/calendar/index.html.j2,sha256=xoR6R4cUgTQNRHQv3m3f4Bc-yCjJEsJj5d4_CWlJsRo,1427
48
49
  geo_activity_playground/webui/calendar/templates/calendar/month.html.j2,sha256=sRIiNo_Rp9CHary6e-lnpKJKOuAonoDEBvKMxzbTLQE,1802
49
50
  geo_activity_playground/webui/eddington/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- geo_activity_playground/webui/eddington/blueprint.py,sha256=evIvueLfDWVTxJ9pRguqmZ9-Pybd2WmBRst_-7vX2QA,551
51
+ geo_activity_playground/webui/eddington/blueprint.py,sha256=2VUiH0HXqp4hK86PmcyOtZ2WTlunJqmsArOYRpVLaAc,452
51
52
  geo_activity_playground/webui/eddington/controller.py,sha256=ly7JSkSS79kO4CL_xugB62uRuuWKVqOjbN-pheelv94,2910
52
53
  geo_activity_playground/webui/eddington/templates/eddington/index.html.j2,sha256=XHKeUymQMS5x00PLOVlg-nSRCz_jHB2pvD8QunULWJ4,1839
53
54
  geo_activity_playground/webui/entry_controller.py,sha256=McxbyouKWHJ3a2R9agPazZoG7VHiFO1RvnkBr08dMH8,2168
54
55
  geo_activity_playground/webui/equipment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- geo_activity_playground/webui/equipment/blueprint.py,sha256=_NIhRJuJNbXpEd_nEPo01AqnUqPgo1vawFn7E3yoeng,636
56
+ geo_activity_playground/webui/equipment/blueprint.py,sha256=kJhCSdakke_UhT2RIP-fMoAMaC1oFttRoCPeiAIaB6g,491
56
57
  geo_activity_playground/webui/equipment/controller.py,sha256=lMivui3EBUnkYZf9Lgv1kHZ0c7IxRAza-ox8YOz3ONY,4079
57
58
  geo_activity_playground/webui/equipment/templates/equipment/index.html.j2,sha256=fvRaDbCuiSZ8AzJTpu1dk8FTAGZ2yfsLhprtVYHFZWo,1802
58
59
  geo_activity_playground/webui/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- geo_activity_playground/webui/explorer/blueprint.py,sha256=EKnBs8llqT6Wy1uac18dF2epp3TebF9p3iGlSbj6Vl0,2337
60
+ geo_activity_playground/webui/explorer/blueprint.py,sha256=iLmdayavR7B1_l67VvkMGsaYeABSXEe9IwTydK_-Owg,2027
60
61
  geo_activity_playground/webui/explorer/controller.py,sha256=pIzWh0TpLJgKQZlS325-QT7nA1q9ms7fRqQIp24PNfo,11705
61
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=aGos75uUyjevDWKSyQwVyvuGHIY6qoGASMbgU6k71YU,6707
62
+ geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=3t9ikAF6oMvEaVlS3Kb1tj9ngomIQlatzqPnqVsEDKA,6908
62
63
  geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
64
  geo_activity_playground/webui/heatmap/blueprint.py,sha256=ZEImDIwT3uiDIKapqCU49llvyqG79n7ZEu1GHgoLZqo,1558
64
- geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=Qw9MGW3TFWlG2JkA_r9RHgYq4hvPiJaZeAg5D9lIFC0,7821
65
+ geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=sUTfM-wwQXe4B4tyuQka9s9bfXLVyexaF0yyM8_q2xk,7728
65
66
  geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=BMjqbZ-btSFawuNDxgZxOkF5JvhD_p9DOBJ-4-1IKnU,1833
66
67
  geo_activity_playground/webui/plot_util.py,sha256=pTTQoqOCkLVjkgOit7mbry28kMruZIL8amZozSzEpxQ,283
67
68
  geo_activity_playground/webui/search/blueprint.py,sha256=7TDsiqEowMyHNlFImk-hCGso69KOieG4rfJnLRHpRz8,3300
@@ -114,19 +115,19 @@ geo_activity_playground/webui/static/vega@5,sha256=5DLHUaY2P0ph2mKSDMfX69E88J2Cl
114
115
  geo_activity_playground/webui/static/web-app-manifest-192x192.png,sha256=eEImN6iWfSv-EnSNPL5WbX84PKakse_8VZMBPWWye3o,13582
115
116
  geo_activity_playground/webui/static/web-app-manifest-512x512.png,sha256=vU9oQ4HnQerFDZVzcAT9twj4_Doc6_9v9wVvoRI-f_E,48318
116
117
  geo_activity_playground/webui/summary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
- geo_activity_playground/webui/summary/blueprint.py,sha256=tfA2aPF19yKwkQOb5lPQBySoQYYhTn49Iuh0SYvsGP8,593
118
- geo_activity_playground/webui/summary/controller.py,sha256=FyPdC98maX0P5sJ4j5Z7-ZSiirLh_jmu_PszKXqTV8A,9425
118
+ geo_activity_playground/webui/summary/blueprint.py,sha256=7Yc8aQ9zTy8Rd0-yI_jV3cvQi5cwSe_Wn5O1JSvHq0s,416
119
+ geo_activity_playground/webui/summary/controller.py,sha256=CJZKGaVXJenuoIbypzbH_MwMKBICGr_F4Adu9LMWtPU,9397
119
120
  geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=ctOx3Qjx6nRDpUtFf1DlJhK_gtU77Vwx_S6euLz9-W4,5183
120
121
  geo_activity_playground/webui/templates/home.html.j2,sha256=IdCqI_LLcYrpUjjCO-tbXR4s05XYrPOateiJ4idF3bo,2202
121
122
  geo_activity_playground/webui/templates/page.html.j2,sha256=Mt1M0hw1V7sPnQmo6kRO_e49B4to30eDkAf-J35xXas,10624
122
123
  geo_activity_playground/webui/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
123
124
  geo_activity_playground/webui/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
124
125
  geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
- geo_activity_playground/webui/tile/blueprint.py,sha256=q0sw_F8L367Df01yjZijikEIglFBgg9lN61sbTAEOKQ,1018
126
- geo_activity_playground/webui/tile/controller.py,sha256=XjUTbyAMeQET1D3mFtT8r5-xMcMOaELPZWtQ1Xp7Cuw,1428
126
+ geo_activity_playground/webui/tile/blueprint.py,sha256=WTkqeOhagnDCquyBtOsZQLzI41arIEdGBe46jnMEkNg,934
127
+ geo_activity_playground/webui/tile/controller.py,sha256=UY_dVQrjSYCXTkVjgQwQumltPPIUOQfdnN8nJv1m54I,1433
127
128
  geo_activity_playground/webui/upload_blueprint.py,sha256=topLI9ytDUFkqCc9AlOqDkjhABUwnPJ1tX_7XrBPbxc,4412
128
- geo_activity_playground-0.34.2.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
129
- geo_activity_playground-0.34.2.dist-info/METADATA,sha256=zJSlC24LGmWQ63Ozp6X6ZGqgFbTzr5M7qBVerF_JwHc,1573
130
- geo_activity_playground-0.34.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
131
- geo_activity_playground-0.34.2.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
132
- geo_activity_playground-0.34.2.dist-info/RECORD,,
129
+ geo_activity_playground-0.35.1.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
130
+ geo_activity_playground-0.35.1.dist-info/METADATA,sha256=tXXmYUBsCfdgXnO_D5nXqvc8xIv237xH0Upkgdw6r_A,1573
131
+ geo_activity_playground-0.35.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
132
+ geo_activity_playground-0.35.1.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
133
+ geo_activity_playground-0.35.1.dist-info/RECORD,,
@@ -1,194 +0,0 @@
1
- """
2
- This code is based on https://github.com/remisalmon/Strava-local-heatmap.
3
- """
4
- import dataclasses
5
- import logging
6
-
7
- import numpy as np
8
-
9
- from geo_activity_playground.core.config import Config
10
- from geo_activity_playground.core.tiles import compute_tile_float
11
- from geo_activity_playground.core.tiles import get_tile
12
- from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
13
-
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- @dataclasses.dataclass
19
- class GeoBounds:
20
- lat_min: float
21
- lon_min: float
22
- lat_max: float
23
- lon_max: float
24
-
25
-
26
- def get_bounds(lat_lon_data: np.ndarray) -> GeoBounds:
27
- return GeoBounds(*np.min(lat_lon_data, axis=0), *np.max(lat_lon_data, axis=0))
28
-
29
-
30
- def add_margin(lower: float, upper: float) -> tuple[float, float]:
31
- spread = upper - lower
32
- margin = spread / 20
33
- return max(0.0, lower - margin), upper + margin
34
-
35
-
36
- def add_margin_to_geo_bounds(bounds: GeoBounds) -> GeoBounds:
37
- lat_min, lat_max = add_margin(bounds.lat_min, bounds.lat_max)
38
- lon_min, lon_max = add_margin(bounds.lon_min, bounds.lon_max)
39
- return GeoBounds(lat_min, lon_min, lat_max, lon_max)
40
-
41
-
42
- OSM_TILE_SIZE = 256 # OSM tile size in pixel
43
- OSM_MAX_ZOOM = 19 # OSM maximum zoom level
44
- MAX_TILE_COUNT = 2000 # maximum number of tiles to download
45
-
46
-
47
- @dataclasses.dataclass
48
- class TileBounds:
49
- zoom: int
50
- x_tile_min: int
51
- x_tile_max: int
52
- y_tile_min: int
53
- y_tile_max: int
54
-
55
-
56
- @dataclasses.dataclass
57
- class PixelBounds:
58
- x_min: int
59
- x_max: int
60
- y_min: int
61
- y_max: int
62
-
63
- @classmethod
64
- def from_tile_bounds(cls, tile_bounds: TileBounds) -> "PixelBounds":
65
- return cls(
66
- int(tile_bounds.x_tile_min) * OSM_TILE_SIZE,
67
- int(tile_bounds.x_tile_max) * OSM_TILE_SIZE,
68
- int(tile_bounds.y_tile_min) * OSM_TILE_SIZE,
69
- int(tile_bounds.y_tile_max) * OSM_TILE_SIZE,
70
- )
71
-
72
- @property
73
- def shape(self) -> tuple[int, int]:
74
- return (
75
- self.y_max - self.y_min,
76
- self.x_max - self.x_min,
77
- )
78
-
79
-
80
- def geo_bounds_from_tile_bounds(tile_bounds: TileBounds) -> GeoBounds:
81
- lat_max, lon_min = get_tile_upper_left_lat_lon(
82
- tile_bounds.x_tile_min, tile_bounds.y_tile_min, tile_bounds.zoom
83
- )
84
- lat_min, lon_max = get_tile_upper_left_lat_lon(
85
- tile_bounds.x_tile_max, tile_bounds.y_tile_max, tile_bounds.zoom
86
- )
87
- return GeoBounds(lat_min, lon_min, lat_max, lon_max)
88
-
89
-
90
- def get_sensible_zoom_level(
91
- bounds: GeoBounds, picture_size: tuple[int, int]
92
- ) -> TileBounds:
93
- zoom = OSM_MAX_ZOOM
94
-
95
- while True:
96
- x_tile_min, y_tile_max = map(
97
- int, compute_tile_float(bounds.lat_min, bounds.lon_min, zoom)
98
- )
99
- x_tile_max, y_tile_min = map(
100
- int, compute_tile_float(bounds.lat_max, bounds.lon_max, zoom)
101
- )
102
-
103
- x_tile_max += 1
104
- y_tile_max += 1
105
-
106
- if (x_tile_max - x_tile_min) * OSM_TILE_SIZE <= picture_size[0] and (
107
- y_tile_max - y_tile_min
108
- ) * OSM_TILE_SIZE <= picture_size[1]:
109
- break
110
-
111
- zoom -= 1
112
-
113
- tile_count = (x_tile_max - x_tile_min) * (y_tile_max - y_tile_min)
114
-
115
- if tile_count > MAX_TILE_COUNT:
116
- raise RuntimeError("Zoom value too high, too many tiles to download")
117
-
118
- return TileBounds(
119
- zoom=zoom,
120
- x_tile_min=x_tile_min,
121
- x_tile_max=x_tile_max,
122
- y_tile_min=y_tile_min,
123
- y_tile_max=y_tile_max,
124
- )
125
-
126
-
127
- def build_map_from_tiles_around_center(
128
- center: tuple[float, float],
129
- zoom: int,
130
- target: tuple[int, int],
131
- inner_target: tuple[int, int],
132
- config: Config,
133
- ) -> np.ndarray:
134
- background = np.zeros((target[1], target[0], 3))
135
-
136
- # We will work with the center point and have it in terms of tiles `t` and also in terms of pixels `p`. At the start we know that the tile center must be in the middle of the image.
137
- t = np.array(center)
138
- p = np.array([inner_target[0] / 2, inner_target[1] / 2])
139
-
140
- # Shift both such that they are in the top-left corner of an even tile.
141
- t_offset = np.array([center[0] % 1, center[1] % 1])
142
- t -= t_offset
143
- p -= t_offset * OSM_TILE_SIZE
144
-
145
- # Shift until we have left the image.
146
- shift = np.ceil(p / OSM_TILE_SIZE)
147
- p -= shift * OSM_TILE_SIZE
148
- t -= shift
149
-
150
- num_tiles = np.ceil(np.array(target) / OSM_TILE_SIZE) + 1
151
-
152
- for x in range(int(t[0]), int(t[0] + num_tiles[0])):
153
- for y in range(int(t[1]), int(t[1]) + int(num_tiles[1])):
154
- source_x_min = 0
155
- source_y_min = 0
156
- source_x_max = source_x_min + OSM_TILE_SIZE
157
- source_y_max = source_y_min + OSM_TILE_SIZE
158
-
159
- target_x_min = (x - int(t[0])) * OSM_TILE_SIZE + int(p[0])
160
- target_y_min = (y - int(t[1])) * OSM_TILE_SIZE + int(p[1])
161
- target_x_max = target_x_min + OSM_TILE_SIZE
162
- target_y_max = target_y_min + OSM_TILE_SIZE
163
-
164
- if target_x_min < 0:
165
- source_x_min -= target_x_min
166
- target_x_min = 0
167
- if target_y_min < 0:
168
- source_y_min -= target_y_min
169
- target_y_min = 0
170
- if target_x_max > target[0]:
171
- a = target_x_max - target[0]
172
- target_x_max -= a
173
- source_x_max -= a
174
- if target_y_max > target[1]:
175
- a = target_y_max - target[1]
176
- target_y_max -= a
177
- source_y_max -= a
178
-
179
- if source_x_max < 0 or source_y_max < 0:
180
- continue
181
-
182
- tile = np.array(get_tile(zoom, x, y, config.map_tile_url)) / 255
183
-
184
- background[target_y_min:target_y_max, target_x_min:target_x_max] = tile[
185
- source_y_min:source_y_max, source_x_min:source_x_max, :3
186
- ]
187
-
188
- return background
189
-
190
-
191
- def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
192
- image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
193
- image = np.dstack((image, image, image))
194
- return image