geo-activity-playground 0.31.0__py3-none-any.whl → 0.34.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 (55) hide show
  1. geo_activity_playground/__main__.py +1 -1
  2. geo_activity_playground/core/activities.py +1 -1
  3. geo_activity_playground/core/config.py +5 -1
  4. geo_activity_playground/core/heatmap.py +61 -15
  5. geo_activity_playground/core/tiles.py +8 -5
  6. geo_activity_playground/explorer/video.py +2 -1
  7. geo_activity_playground/importers/directory.py +6 -2
  8. geo_activity_playground/importers/strava_checkout.py +8 -2
  9. geo_activity_playground/webui/activity/blueprint.py +7 -0
  10. geo_activity_playground/webui/activity/controller.py +127 -43
  11. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +6 -2
  12. geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +1 -1
  13. geo_activity_playground/webui/activity/templates/activity/name.html.j2 +3 -2
  14. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +2 -2
  15. geo_activity_playground/webui/app.py +15 -3
  16. geo_activity_playground/webui/entry_controller.py +1 -1
  17. geo_activity_playground/webui/equipment/controller.py +1 -1
  18. geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +20 -44
  19. geo_activity_playground/webui/heatmap/blueprint.py +5 -2
  20. geo_activity_playground/webui/heatmap/heatmap_controller.py +14 -4
  21. geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
  22. geo_activity_playground/webui/settings/blueprint.py +44 -33
  23. geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +11 -2
  24. geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +1 -1
  25. geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +1 -1
  26. geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +1 -0
  27. geo_activity_playground/webui/static/MarkerCluster.Default.css +60 -0
  28. geo_activity_playground/webui/static/MarkerCluster.css +14 -0
  29. geo_activity_playground/webui/static/bootstrap.min.css +6 -0
  30. geo_activity_playground/webui/static/fullscreen.png +0 -0
  31. geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  32. geo_activity_playground/webui/static/leaflet.css +661 -0
  33. geo_activity_playground/webui/static/leaflet.fullscreen.css +40 -0
  34. geo_activity_playground/webui/static/leaflet.js +6 -0
  35. geo_activity_playground/webui/static/leaflet.markercluster.js +3 -0
  36. geo_activity_playground/webui/static/table-sort.min.js +8 -0
  37. geo_activity_playground/webui/static/vega-embed@6 +7 -0
  38. geo_activity_playground/webui/static/vega-lite@4 +2 -0
  39. geo_activity_playground/webui/static/vega@5 +2 -0
  40. geo_activity_playground/webui/summary/controller.py +20 -25
  41. geo_activity_playground/webui/summary/templates/summary/index.html.j2 +18 -5
  42. geo_activity_playground/webui/templates/home.html.j2 +4 -3
  43. geo_activity_playground/webui/templates/page.html.j2 +14 -16
  44. geo_activity_playground/webui/tile/blueprint.py +3 -2
  45. geo_activity_playground/webui/tile/controller.py +7 -3
  46. geo_activity_playground/webui/{upload/controller.py → upload_blueprint.py} +41 -35
  47. {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/METADATA +5 -6
  48. {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/RECORD +53 -41
  49. geo_activity_playground/webui/upload/__init__.py +0 -0
  50. geo_activity_playground/webui/upload/blueprint.py +0 -44
  51. /geo_activity_playground/webui/{upload/templates → templates}/upload/index.html.j2 +0 -0
  52. /geo_activity_playground/webui/{upload/templates → templates}/upload/reload.html.j2 +0 -0
  53. {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/LICENSE +0 -0
  54. {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/WHEEL +0 -0
  55. {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,7 @@ 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
15
  from geo_activity_playground.webui.app import web_ui_main
16
- from geo_activity_playground.webui.upload.controller import scan_for_activities
16
+ from geo_activity_playground.webui.upload_blueprint import scan_for_activities
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -165,7 +165,7 @@ class ActivityRepository:
165
165
  return df
166
166
 
167
167
  def save(self) -> None:
168
- self._meta.to_parquet(activities_file())
168
+ self.meta.to_parquet(activities_file())
169
169
 
170
170
 
171
171
  def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
@@ -21,14 +21,16 @@ logger = logging.getLogger(__name__)
21
21
  @dataclasses.dataclass
22
22
  class Config:
23
23
  birth_year: Optional[int] = None
24
- color_scheme_for_counts: str = "viridis"
24
+ color_scheme_for_counts: str = "teals"
25
25
  color_scheme_for_kind: str = "category10"
26
+ color_scheme_for_heatmap: str = "hot"
26
27
  equipment_offsets: dict[str, float] = dataclasses.field(default_factory=dict)
27
28
  explorer_zoom_levels: list[int] = dataclasses.field(
28
29
  default_factory=lambda: [14, 17]
29
30
  )
30
31
  heart_rate_resting: int = 0
31
32
  heart_rate_maximum: Optional[int] = None
33
+ ignore_suffixes: list[str] = dataclasses.field(default_factory=list)
32
34
  kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
33
35
  kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
34
36
  metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
@@ -42,6 +44,8 @@ class Config:
42
44
  strava_client_code: Optional[str] = None
43
45
  time_diff_threshold_seconds: Optional[int] = 30
44
46
  upload_password: Optional[str] = None
47
+ map_tile_url: str = "https://tile.openstreetmap.org/{zoom}/{x}/{y}.png"
48
+ map_tile_attribution: str = '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | <a href="https://www.openstreetmap.org/fixthemap">Correct Map</a>'
45
49
 
46
50
 
47
51
  class ConfigAccessor:
@@ -6,6 +6,7 @@ import logging
6
6
 
7
7
  import numpy as np
8
8
 
9
+ from geo_activity_playground.core.config import Config
9
10
  from geo_activity_playground.core.tiles import compute_tile_float
10
11
  from geo_activity_playground.core.tiles import get_tile
11
12
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
@@ -123,21 +124,66 @@ def get_sensible_zoom_level(
123
124
  )
124
125
 
125
126
 
126
- def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
127
- background = np.zeros((*PixelBounds.from_tile_bounds(tile_bounds).shape, 3))
128
-
129
- for x in range(tile_bounds.x_tile_min, tile_bounds.x_tile_max):
130
- for y in range(tile_bounds.y_tile_min, tile_bounds.y_tile_max):
131
- tile = np.array(get_tile(tile_bounds.zoom, x, y)) / 255
132
-
133
- i = y - tile_bounds.y_tile_min
134
- j = x - tile_bounds.x_tile_min
135
-
136
- background[
137
- i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
138
- j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
139
- :,
140
- ] = tile[:, :, :3]
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
+ ]
141
187
 
142
188
  return background
143
189
 
@@ -3,6 +3,7 @@ import logging
3
3
  import math
4
4
  import pathlib
5
5
  import time
6
+ import urllib.parse
6
7
  from typing import Iterator
7
8
  from typing import Optional
8
9
 
@@ -13,8 +14,10 @@ from PIL import Image
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
16
- def osm_tile_path(x: int, y: int, zoom: int) -> pathlib.Path:
17
- path = pathlib.Path("Open Street Map Tiles") / f"{zoom}/{x}/{y}.png"
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"
18
21
  path.parent.mkdir(parents=True, exist_ok=True)
19
22
  return path
20
23
 
@@ -62,11 +65,11 @@ def download_file(url: str, destination: pathlib.Path):
62
65
 
63
66
 
64
67
  @functools.lru_cache()
65
- def get_tile(zoom: int, x: int, y: int) -> Image.Image:
66
- destination = osm_tile_path(x, y, zoom)
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)
67
70
  if not destination.exists():
68
71
  logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
69
- url = f"https://tile.openstreetmap.org/{zoom}/{x}/{y}.png"
72
+ url = url_template.format(x=x, y=y, zoom=zoom)
70
73
  download_file(url, destination)
71
74
  with Image.open(destination) as image:
72
75
  image.load()
@@ -9,13 +9,14 @@ from typing import Tuple
9
9
 
10
10
  import numpy as np
11
11
  import pandas as pd
12
- import scipy.interpolate
13
12
  from PIL import Image
14
13
  from PIL import ImageEnhance
15
14
  from tqdm import tqdm
16
15
 
17
16
  from ..core.tiles import get_tile
18
17
 
18
+ # import scipy.interpolate
19
+
19
20
 
20
21
  def build_image(
21
22
  center_x: float,
@@ -10,6 +10,7 @@ from typing import Optional
10
10
  from tqdm import tqdm
11
11
 
12
12
  from geo_activity_playground.core.activities import ActivityMeta
13
+ from geo_activity_playground.core.config import Config
13
14
  from geo_activity_playground.core.paths import activity_extracted_dir
14
15
  from geo_activity_playground.core.paths import activity_extracted_meta_dir
15
16
  from geo_activity_playground.core.paths import activity_extracted_time_series_dir
@@ -24,13 +25,16 @@ ACTIVITY_DIR = pathlib.Path("Activities")
24
25
 
25
26
 
26
27
  def import_from_directory(
27
- metadata_extraction_regexes: list[str], num_processes: Optional[int]
28
+ metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
28
29
  ) -> None:
29
30
 
30
31
  activity_paths = [
31
32
  path
32
33
  for path in ACTIVITY_DIR.rglob("*.*")
33
- if path.is_file() and path.suffixes and not path.stem.startswith(".")
34
+ if path.is_file()
35
+ and path.suffixes
36
+ and not path.stem.startswith(".")
37
+ and not path.suffix in config.ignore_suffixes
34
38
  ]
35
39
  work_tracker = WorkTracker(activity_extracted_dir() / "work-tracker-extract.pickle")
36
40
  new_activity_paths = work_tracker.filter(activity_paths)
@@ -146,7 +146,7 @@ def float_with_comma_or_period(x: str) -> Optional[float]:
146
146
 
147
147
  def import_from_strava_checkout() -> None:
148
148
  checkout_path = pathlib.Path("Strava Export")
149
- with open(checkout_path / "activities.csv") as f:
149
+ with open(checkout_path / "activities.csv", encoding="utf-8") as f:
150
150
  rows = parse_csv(f.read())
151
151
  header = rows[0]
152
152
 
@@ -159,9 +159,15 @@ def import_from_strava_checkout() -> None:
159
159
 
160
160
  if header[0] == EXPECTED_COLUMNS[0]:
161
161
  dayfirst = False
162
- if header[0] == "Aktivitäts-ID":
162
+ elif header[0] == "Aktivitäts-ID":
163
163
  header = EXPECTED_COLUMNS
164
164
  dayfirst = True
165
+ else:
166
+ logger.error(
167
+ f"You are trying to import a Strava checkout where the `activities.csv` contains an unexpected header format. In order to import this, we need to map these to the English ones. Unfortunately Strava often changes the number of columns. This means that the program needs to be updated to match the new Strava export format. Please go to https://github.com/martin-ueding/geo-activity-playground/issues and open a new issue and share the following output in the ticket:"
168
+ )
169
+ print(header)
170
+ sys.exit(1)
165
171
 
166
172
  table = {
167
173
  header[i]: [rows[r][i] for r in range(1, len(rows))] for i in range(len(header))
@@ -55,6 +55,13 @@ def make_activity_blueprint(
55
55
  **activity_controller.render_day(int(year), int(month), int(day)),
56
56
  )
57
57
 
58
+ @blueprint.route("/day-sharepic/<year>/<month>/<day>/sharepic.png")
59
+ def day_sharepic(year: str, month: str, day: str):
60
+ return Response(
61
+ activity_controller.render_day_sharepic(int(year), int(month), int(day)),
62
+ mimetype="image/png",
63
+ )
64
+
58
65
  @blueprint.route("/name/<name>")
59
66
  def name(name: str):
60
67
  return render_template(
@@ -21,11 +21,9 @@ from geo_activity_playground.core.activities import make_geojson_from_time_serie
21
21
  from geo_activity_playground.core.activities import make_speed_color_bar
22
22
  from geo_activity_playground.core.config import Config
23
23
  from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
24
- from geo_activity_playground.core.heatmap import add_margin_to_geo_bounds
25
- from geo_activity_playground.core.heatmap import build_map_from_tiles
24
+ from geo_activity_playground.core.heatmap import build_map_from_tiles_around_center
26
25
  from geo_activity_playground.core.heatmap import GeoBounds
27
- from geo_activity_playground.core.heatmap import get_bounds
28
- from geo_activity_playground.core.heatmap import get_sensible_zoom_level
26
+ from geo_activity_playground.core.heatmap import OSM_MAX_ZOOM
29
27
  from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
30
28
  from geo_activity_playground.core.heatmap import PixelBounds
31
29
  from geo_activity_playground.core.heatmap import TileBounds
@@ -128,7 +126,7 @@ class ActivityController:
128
126
  if len(time_series) == 0:
129
127
  time_series = self._repository.get_time_series(id)
130
128
  return make_sharepic(
131
- activity, time_series, self._config.sharepic_suppressed_fields
129
+ activity, time_series, self._config.sharepic_suppressed_fields, self._config
132
130
  )
133
131
 
134
132
  def render_day(self, year: int, month: int, day: int) -> dict:
@@ -172,8 +170,24 @@ class ActivityController:
172
170
  "date": datetime.date(year, month, day).isoformat(),
173
171
  "total_distance": activities_that_day["distance_km"].sum(),
174
172
  "total_elapsed_time": activities_that_day["elapsed_time"].sum(),
173
+ "day": day,
174
+ "month": month,
175
+ "year": year,
175
176
  }
176
177
 
178
+ def render_day_sharepic(self, year: int, month: int, day: int) -> bytes:
179
+ meta = self._repository.meta
180
+ selection = meta["start"].dt.date == datetime.date(year, month, day)
181
+ activities_that_day = meta.loc[selection]
182
+
183
+ time_series = [
184
+ self._repository.get_time_series(activity_id)
185
+ for activity_id in activities_that_day["id"]
186
+ ]
187
+ assert len(activities_that_day) > 0
188
+ assert len(time_series) > 0
189
+ return (make_day_sharepic(activities_that_day, time_series, self._config),)
190
+
177
191
  def render_all(self) -> dict:
178
192
  cmap = matplotlib.colormaps["Dark2"]
179
193
  fc = geojson.FeatureCollection(
@@ -454,47 +468,74 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
454
468
  return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
455
469
 
456
470
 
457
- def make_sharepic(
458
- activity: ActivityMeta,
459
- time_series: pd.DataFrame,
460
- sharepic_suppressed_fields: list[str],
461
- ) -> bytes:
462
- lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
471
+ def make_sharepic_base(time_series_list: list[pd.DataFrame], config: Config):
472
+ all_time_series = pd.concat(time_series_list)
473
+ tile_x = all_time_series["x"]
474
+ tile_y = all_time_series["y"]
475
+ tile_width = tile_x.max() - tile_x.min()
476
+ tile_height = tile_y.max() - tile_y.min()
463
477
 
464
- geo_bounds = get_bounds(lat_lon_data)
465
- geo_bounds = add_margin_to_geo_bounds(geo_bounds)
466
- tile_bounds = get_sensible_zoom_level(geo_bounds, (1500, 1500))
467
- tile_bounds = make_tile_bounds_square(tile_bounds)
468
- background = build_map_from_tiles(tile_bounds)
469
- # background = convert_to_grayscale(background)
478
+ target_width = 600
479
+ target_height = 600
480
+ footer_height = 100
481
+ target_map_height = target_height - footer_height
470
482
 
471
- crop_mask = get_crop_mask(geo_bounds, tile_bounds)
472
- assert pixels_in_bounds(crop_mask) <= 10_000_000, crop_mask
483
+ zoom = int(
484
+ min(
485
+ np.log2(target_width / tile_width / OSM_TILE_SIZE),
486
+ np.log2(target_map_height / tile_height / OSM_TILE_SIZE),
487
+ OSM_MAX_ZOOM,
488
+ )
489
+ )
473
490
 
474
- background = background[
475
- crop_mask.y_min : crop_mask.y_max,
476
- crop_mask.x_min : crop_mask.x_max,
477
- :,
478
- ]
491
+ tile_xz = tile_x * 2**zoom
492
+ tile_yz = tile_y * 2**zoom
493
+
494
+ tile_xz_center = (
495
+ (tile_xz.max() + tile_xz.min()) / 2,
496
+ (tile_yz.max() + tile_yz.min()) / 2,
497
+ )
498
+
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,
505
+ )
479
506
 
480
507
  img = Image.fromarray((background * 255).astype("uint8"), "RGB")
481
508
  draw = ImageDraw.Draw(img, mode="RGBA")
482
509
 
483
- for _, group in time_series.groupby("segment_id"):
484
- xs, ys = compute_tile_float(
485
- group["latitude"], group["longitude"], tile_bounds.zoom
486
- )
487
- yx = list(
488
- (
489
- int((x - tile_bounds.x_tile_min) * OSM_TILE_SIZE - crop_mask.x_min),
490
- int((y - tile_bounds.y_tile_min) * OSM_TILE_SIZE - crop_mask.y_min),
510
+ for time_series in time_series_list:
511
+ for _, group in time_series.groupby("segment_id"):
512
+ yx = list(
513
+ zip(
514
+ (tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
515
+ (tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE
516
+ + target_map_height / 2,
517
+ )
491
518
  )
492
- for x, y in zip(xs, ys)
493
- )
494
519
 
495
- draw.line(yx, fill="red", width=4)
520
+ draw.line(yx, fill="red", width=4)
521
+
522
+ return img
496
523
 
497
- draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
524
+
525
+ def make_sharepic(
526
+ activity: ActivityMeta,
527
+ time_series: pd.DataFrame,
528
+ sharepic_suppressed_fields: list[str],
529
+ config: Config,
530
+ ) -> bytes:
531
+ footer_height = 100
532
+
533
+ img = make_sharepic_base([time_series], config)
534
+
535
+ draw = ImageDraw.Draw(img, mode="RGBA")
536
+ draw.rectangle(
537
+ [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
538
+ )
498
539
 
499
540
  facts = {
500
541
  "kind": f"{activity['kind']}",
@@ -515,19 +556,62 @@ def make_sharepic(
515
556
  if not key in sharepic_suppressed_fields
516
557
  }
517
558
 
518
- draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
559
+ draw.text(
560
+ (35, img.height - footer_height + 10),
561
+ " ".join(facts.values()),
562
+ font_size=20,
563
+ )
564
+
565
+ draw.text(
566
+ (img.width - 250, img.height - 20),
567
+ "Map: © Open Street Map Contributors",
568
+ font_size=14,
569
+ )
570
+
571
+ f = io.BytesIO()
572
+ img.save(f, format="png")
573
+ return bytes(f.getbuffer())
574
+
575
+
576
+ def make_day_sharepic(
577
+ activities: pd.DataFrame,
578
+ time_series_list: list[pd.DataFrame],
579
+ config: Config,
580
+ ) -> bytes:
581
+ footer_height = 100
519
582
 
520
- # img_array = np.array(img) / 255
583
+ img = make_sharepic_base(time_series_list, config)
584
+
585
+ draw = ImageDraw.Draw(img, mode="RGBA")
586
+ draw.rectangle(
587
+ [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
588
+ )
521
589
 
522
- # weight = np.dstack([img_array[:, :, 0]] * 3)
590
+ date = activities.iloc[0]["start"].date()
591
+ distance_km = activities["distance_km"].sum()
592
+ elapsed_time: pd.Timedelta = activities["elapsed_time"].sum()
593
+ elapsed_time = elapsed_time.round("s")
523
594
 
524
- # background = (1 - weight) * background + img_array
525
- # background[background > 1.0] = 1.0
526
- # background[background < 0.0] = 0.0
595
+ facts = {
596
+ "date": f"{date}",
597
+ "distance_km": f"{distance_km:.1f} km",
598
+ "elapsed_time": re.sub(r"^0 days ", "", f"{elapsed_time}"),
599
+ }
600
+
601
+ draw.text(
602
+ (35, img.height - footer_height + 10),
603
+ " ".join(facts.values()),
604
+ font_size=20,
605
+ )
606
+
607
+ draw.text(
608
+ (img.width - 250, img.height - 20),
609
+ "Map: © Open Street Map Contributors",
610
+ font_size=14,
611
+ )
527
612
 
528
613
  f = io.BytesIO()
529
614
  img.save(f, format="png")
530
- # pl.imsave(f, background, format="png")
531
615
  return bytes(f.getbuffer())
532
616
 
533
617
 
@@ -17,7 +17,7 @@
17
17
  });
18
18
  L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
19
19
  maxZoom: 19,
20
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
20
+ attribution: '{{ map_tile_attribution|safe }}'
21
21
  }).addTo(map);
22
22
 
23
23
  let geojson = L.geoJSON({{ geojson| safe }}, {
@@ -30,7 +30,7 @@
30
30
  <ol>
31
31
  {% for activity in activities %}
32
32
  <li><span style="color: {{ activity['color'] }};">█</span> <a
33
- href="{{ url_for('activity.show', id=activity.id) }}">{{
33
+ href="{{ url_for('.show', id=activity.id) }}">{{
34
34
  activity.name }}</a></li>
35
35
  {% endfor %}
36
36
  </ol>
@@ -80,4 +80,8 @@
80
80
  </div>
81
81
  </div>
82
82
 
83
+ <h2>Share picture</h2>
84
+
85
+ <p><img src="{{ url_for('.day_sharepic', year=year, month=month, day=day) }}" /></p>
86
+
83
87
  {% endblock %}
@@ -21,7 +21,7 @@
21
21
  });
22
22
  L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
23
23
  maxZoom: 19,
24
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
24
+ attribution: '{{ map_tile_attribution|safe }}'
25
25
  }).addTo(map);
26
26
 
27
27
  let geojson = L.geoJSON({{ geojson| safe }}, {
@@ -17,7 +17,7 @@
17
17
  });
18
18
  L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
19
19
  maxZoom: 19,
20
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
20
+ attribution: '{{ map_tile_attribution|safe }}'
21
21
  }).addTo(map);
22
22
 
23
23
  let geojson = L.geoJSON({{ geojson| safe }}, {
@@ -64,7 +64,8 @@
64
64
  <tbody>
65
65
  {% for activity in activities %}
66
66
  <tr>
67
- <td><span style="color: {{ activity['color'] }};">█</span> <a href="{{ url_for('activity.show', id=activity.id) }}">{{
67
+ <td><span style="color: {{ activity['color'] }};">█</span> <a
68
+ href="{{ url_for('activity.show', id=activity.id) }}">{{
68
69
  activity.name }}</a></td>
69
70
  <td>{{ activity.start|dt }}</td>
70
71
  <td>{{ activity.distance_km | round(1) }}</td>
@@ -52,7 +52,7 @@
52
52
  });
53
53
  L.tileLayer('/tile/pastel/{z}/{x}/{y}.png', {
54
54
  maxZoom: 19,
55
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
55
+ attribution: '{{ map_tile_attribution|safe }}'
56
56
  }).addTo(map);
57
57
 
58
58
  let geojson = L.geoJSON({{ color_line_geojson| safe }}, {
@@ -160,7 +160,7 @@
160
160
  })
161
161
  L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
162
162
  maxZoom: 19,
163
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
163
+ attribution: '{{ map_tile_attribution|safe }}'
164
164
  }).addTo(map)
165
165
 
166
166
  let geojson_layer = L.geoJSON(geojson).addTo(map)
@@ -3,6 +3,8 @@ import importlib
3
3
  import json
4
4
  import pathlib
5
5
  import secrets
6
+ import shutil
7
+ import urllib.parse
6
8
 
7
9
  from flask import Flask
8
10
  from flask import render_template
@@ -20,7 +22,7 @@ from .search.blueprint import make_search_blueprint
20
22
  from .square_planner.blueprint import make_square_planner_blueprint
21
23
  from .summary.blueprint import make_summary_blueprint
22
24
  from .tile.blueprint import make_tile_blueprint
23
- from .upload.blueprint import make_upload_blueprint
25
+ from .upload_blueprint import make_upload_blueprint
24
26
  from geo_activity_playground.core.config import Config
25
27
  from geo_activity_playground.core.config import ConfigAccessor
26
28
  from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
@@ -97,7 +99,8 @@ def web_ui_main(
97
99
  url_prefix="/explorer",
98
100
  )
99
101
  app.register_blueprint(
100
- make_heatmap_blueprint(repository, tile_visit_accessor), url_prefix="/heatmap"
102
+ make_heatmap_blueprint(repository, tile_visit_accessor, config_accessor()),
103
+ url_prefix="/heatmap",
101
104
  )
102
105
  app.register_blueprint(
103
106
  make_settings_blueprint(config_accessor, authenticator),
@@ -115,7 +118,7 @@ def web_ui_main(
115
118
  make_summary_blueprint(repository, config_accessor()),
116
119
  url_prefix="/summary",
117
120
  )
118
- app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
121
+ app.register_blueprint(make_tile_blueprint(config_accessor()), url_prefix="/tile")
119
122
  app.register_blueprint(
120
123
  make_upload_blueprint(
121
124
  repository, tile_visit_accessor, config_accessor(), authenticator
@@ -123,11 +126,20 @@ def web_ui_main(
123
126
  url_prefix="/upload",
124
127
  )
125
128
 
129
+ base_dir = pathlib.Path("Open Street Map Tiles")
130
+ dir_for_source = base_dir / urllib.parse.quote_plus(config_accessor().map_tile_url)
131
+ if base_dir.exists() and not dir_for_source.exists():
132
+ subdirs = base_dir.glob("*")
133
+ dir_for_source.mkdir()
134
+ for subdir in subdirs:
135
+ shutil.move(subdir, dir_for_source)
136
+
126
137
  @app.context_processor
127
138
  def inject_global_variables() -> dict:
128
139
  return {
129
140
  "version": _try_get_version(),
130
141
  "num_activities": len(repository),
142
+ "map_tile_attribution": config_accessor().map_tile_attribution,
131
143
  }
132
144
 
133
145
  app.run(host=host, port=port)
@@ -16,10 +16,10 @@ class EntryController:
16
16
  self._config = config
17
17
 
18
18
  def render(self) -> dict:
19
- kind_scale = make_kind_scale(self._repository.meta, self._config)
20
19
  result = {"latest_activities": []}
21
20
 
22
21
  if len(self._repository):
22
+ kind_scale = make_kind_scale(self._repository.meta, self._config)
23
23
  result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
24
24
  self._repository.meta, kind_scale
25
25
  )
@@ -87,7 +87,7 @@ class EquipmentController:
87
87
  )
88
88
  .mark_bar()
89
89
  .encode(
90
- alt.X("kind", title="Year"),
90
+ alt.X("kind", title="Kind"),
91
91
  alt.Y("sum(distance_km)", title="Distance / km"),
92
92
  )
93
93
  .to_json(format="vega")