geo-activity-playground 0.31.0__tar.gz → 0.33.0__tar.gz

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 (117) hide show
  1. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/config.py +5 -1
  3. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/heatmap.py +61 -15
  4. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/tiles.py +8 -5
  5. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/directory.py +6 -2
  6. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/controller.py +50 -36
  7. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +1 -1
  8. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +1 -1
  9. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +3 -2
  10. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +2 -2
  11. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/app.py +14 -2
  12. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +20 -44
  13. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/heatmap/blueprint.py +5 -2
  14. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +14 -4
  15. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
  16. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/blueprint.py +44 -33
  17. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +11 -2
  18. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +1 -1
  19. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +1 -1
  20. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/summary/controller.py +9 -15
  21. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +18 -5
  22. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/templates/home.html.j2 +1 -1
  23. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/tile/blueprint.py +3 -2
  24. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/tile/controller.py +7 -3
  25. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/upload/controller.py +1 -2
  26. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/pyproject.toml +1 -1
  27. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/LICENSE +0 -0
  28. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/__init__.py +0 -0
  29. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/__main__.py +0 -0
  30. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/__init__.py +0 -0
  31. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/activities.py +0 -0
  32. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/coordinates.py +0 -0
  33. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/enrichment.py +0 -0
  34. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/heart_rate.py +0 -0
  35. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/paths.py +0 -0
  36. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  37. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/similarity.py +0 -0
  38. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/tasks.py +0 -0
  39. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/test_tiles.py +0 -0
  40. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
  41. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/core/time_conversion.py +0 -0
  42. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/explorer/__init__.py +0 -0
  43. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  44. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
  45. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/explorer/video.py +0 -0
  46. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/__init__.py +0 -0
  47. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
  48. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  49. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/strava_api.py +0 -0
  50. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
  51. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  52. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/test_directory.py +0 -0
  53. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  54. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/__init__.py +0 -0
  55. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
  56. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
  57. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -0
  58. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  59. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  60. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/authenticator.py +0 -0
  61. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  62. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
  63. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
  64. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  65. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  66. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  67. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
  68. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
  69. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  70. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  71. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  72. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
  73. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
  74. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
  75. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  76. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
  77. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
  78. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  79. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/plot_util.py +0 -0
  80. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/search/blueprint.py +0 -0
  81. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -0
  82. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/controller.py +0 -0
  83. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  84. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  85. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  86. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +0 -0
  87. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -0
  88. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  89. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  90. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +0 -0
  91. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  92. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  93. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  94. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  95. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
  96. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  97. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  98. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  99. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  100. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  101. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  102. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  103. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  104. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  105. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/favicon.svg +0 -0
  106. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  107. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  108. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  109. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  110. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
  111. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
  112. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  113. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
  114. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
  115. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
  116. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
  117. {geo_activity_playground-0.31.0 → geo_activity_playground-0.33.0}/geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.31.0
3
+ Version: 0.33.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -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()
@@ -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)
@@ -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:
@@ -458,43 +456,58 @@ def make_sharepic(
458
456
  activity: ActivityMeta,
459
457
  time_series: pd.DataFrame,
460
458
  sharepic_suppressed_fields: list[str],
459
+ config: Config,
461
460
  ) -> bytes:
462
- lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
461
+ tile_x = time_series["x"]
462
+ tile_y = time_series["y"]
463
+ tile_width = tile_x.max() - tile_x.min()
464
+ tile_height = tile_y.max() - tile_y.min()
465
+
466
+ target_width = 600
467
+ target_height = 600
468
+ footer_height = 100
469
+ target_map_height = target_height - footer_height
470
+
471
+ zoom = int(
472
+ min(
473
+ np.log2(target_width / tile_width / OSM_TILE_SIZE),
474
+ np.log2(target_map_height / tile_height / OSM_TILE_SIZE),
475
+ OSM_MAX_ZOOM,
476
+ )
477
+ )
463
478
 
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)
479
+ tile_xz = tile_x * 2**zoom
480
+ tile_yz = tile_y * 2**zoom
470
481
 
471
- crop_mask = get_crop_mask(geo_bounds, tile_bounds)
472
- assert pixels_in_bounds(crop_mask) <= 10_000_000, crop_mask
482
+ tile_xz_center = (
483
+ (tile_xz.max() + tile_xz.min()) / 2,
484
+ (tile_yz.max() + tile_yz.min()) / 2,
485
+ )
473
486
 
474
- background = background[
475
- crop_mask.y_min : crop_mask.y_max,
476
- crop_mask.x_min : crop_mask.x_max,
477
- :,
478
- ]
487
+ background = build_map_from_tiles_around_center(
488
+ tile_xz_center,
489
+ zoom,
490
+ (target_width, target_height),
491
+ (target_width, target_map_height),
492
+ config,
493
+ )
479
494
 
480
495
  img = Image.fromarray((background * 255).astype("uint8"), "RGB")
481
496
  draw = ImageDraw.Draw(img, mode="RGBA")
482
497
 
483
498
  for _, group in time_series.groupby("segment_id"):
484
- xs, ys = compute_tile_float(
485
- group["latitude"], group["longitude"], tile_bounds.zoom
486
- )
487
499
  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),
500
+ zip(
501
+ (tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
502
+ (tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE + target_map_height / 2,
491
503
  )
492
- for x, y in zip(xs, ys)
493
504
  )
494
505
 
495
506
  draw.line(yx, fill="red", width=4)
496
507
 
497
- draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
508
+ draw.rectangle(
509
+ [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
510
+ )
498
511
 
499
512
  facts = {
500
513
  "kind": f"{activity['kind']}",
@@ -515,19 +528,20 @@ def make_sharepic(
515
528
  if not key in sharepic_suppressed_fields
516
529
  }
517
530
 
518
- draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
519
-
520
- # img_array = np.array(img) / 255
521
-
522
- # weight = np.dstack([img_array[:, :, 0]] * 3)
531
+ draw.text(
532
+ (35, img.height - footer_height + 10),
533
+ " ".join(facts.values()),
534
+ font_size=20,
535
+ )
523
536
 
524
- # background = (1 - weight) * background + img_array
525
- # background[background > 1.0] = 1.0
526
- # background[background < 0.0] = 0.0
537
+ draw.text(
538
+ (img.width - 250, img.height - 20),
539
+ "Map: © Open Street Map Contributors",
540
+ font_size=14,
541
+ )
527
542
 
528
543
  f = io.BytesIO()
529
544
  img.save(f, format="png")
530
- # pl.imsave(f, background, format="png")
531
545
  return bytes(f.getbuffer())
532
546
 
533
547
 
@@ -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 }}, {
@@ -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
@@ -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)
@@ -73,7 +73,7 @@
73
73
  })
74
74
  L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
75
75
  maxZoom: 19,
76
- attribution: '&copy; <a href=" http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
76
+ attribution: '{{ map_tile_attribution|safe }}'
77
77
  }).addTo(map)
78
78
  let explorer_layer_cluster_color = L.geoJSON(explorer_geojson, {
79
79
  style: function (feature) {
@@ -87,58 +87,34 @@
87
87
  let explorer_layer_first_age_color = L.geoJSON(explorer_geojson, {
88
88
  style: function (feature) {
89
89
  return {
90
- color: "#440154", fillColor: feature.properties.first_age_color,
91
- weight: 0.5
90
+ color: " #440154", fillColor: feature.properties.first_age_color, weight: 0.5
92
91
  }
93
92
  },
94
93
  onEachFeature: onEachFeature
95
- })
96
- let explorer_layer_last_age_color = L.geoJSON(explorer_geojson, {
97
- style: function (feature) {
98
- return {
99
- color: "#440154", fillColor: feature.properties.last_age_color, weight:
100
- 0.5
101
- }
102
- },
103
- onEachFeature: onEachFeature
104
- })
105
-
106
- let bbox = {{ center.bbox| safe }}
107
- if (bbox) {
108
- map.fitBounds(L.geoJSON(bbox).getBounds())
109
- }
110
-
111
- let explorer_square_layer = L.geoJSON(square_geojson,
112
- {
113
- style: function (feature) {
94
+ }) let explorer_layer_last_age_color = L.geoJSON(explorer_geojson, {
95
+ style:
96
+ function (feature) {
114
97
  return {
115
- color: "blue", fill: false, weight: 2
98
+ color: "#440154", fillColor: feature.properties.last_age_color, weight:
99
+ 0.5
116
100
  }
117
- }
118
- }
119
- ).addTo(map)
120
-
121
- active_layer = explorer_layer_cluster_color
122
-
123
- function changeColor(method) {
101
+ }, onEachFeature: onEachFeature
102
+ }) let bbox = {{ center.bbox| safe }} if (bbox) {
103
+ map.fitBounds(L.geoJSON(bbox).getBounds())
104
+ } let explorer_square_layer = L.geoJSON(square_geojson, {
105
+ style: function (feature) { return { color: "blue", fill: false, weight: 2 } }
106
+ }).addTo(map)
107
+ active_layer = explorer_layer_cluster_color function changeColor(method) {
124
108
  map.removeLayer(active_layer)
125
- if (method == "cluster") {
126
- active_layer = explorer_layer_cluster_color
127
- } else if (method == "first") {
109
+ if (method == "cluster") { active_layer = explorer_layer_cluster_color } else if (method == "first") {
128
110
  active_layer = explorer_layer_first_age_color
129
111
  } else if (method == "last") {
130
112
  active_layer = explorer_layer_last_age_color
131
- }
132
- map.addLayer(active_layer)
133
- }
134
-
135
- function downloadAs(suffix) {
136
- bounds = map.getBounds()
137
- zoom = "{{ zoom }}"
138
- window.location.href =
139
- `/explorer/${zoom}/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/${suffix}`
140
- }
141
- </script>
113
+ } map.addLayer(active_layer)
114
+ } function downloadAs(suffix) {
115
+ bounds = map.getBounds() zoom = "{{ zoom }}"
116
+ window.location.href = `/explorer/${zoom}/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/${suffix}`
117
+ } </script>
142
118
  </div>
143
119
  </div>
144
120
 
@@ -6,12 +6,15 @@ from flask import Response
6
6
  from ...core.activities import ActivityRepository
7
7
  from ...explorer.tile_visits import TileVisitAccessor
8
8
  from .heatmap_controller import HeatmapController
9
+ from geo_activity_playground.core.config import Config
9
10
 
10
11
 
11
12
  def make_heatmap_blueprint(
12
- repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
13
+ repository: ActivityRepository,
14
+ tile_visit_accessor: TileVisitAccessor,
15
+ config: Config,
13
16
  ) -> Blueprint:
14
- heatmap_controller = HeatmapController(repository, tile_visit_accessor)
17
+ heatmap_controller = HeatmapController(repository, tile_visit_accessor, config)
15
18
  blueprint = Blueprint("heatmap", __name__, template_folder="templates")
16
19
 
17
20
  @blueprint.route("/")
@@ -8,6 +8,7 @@ from PIL import Image
8
8
  from PIL import ImageDraw
9
9
 
10
10
  from geo_activity_playground.core.activities import ActivityRepository
11
+ from geo_activity_playground.core.config import Config
11
12
  from geo_activity_playground.core.heatmap import convert_to_grayscale
12
13
  from geo_activity_playground.core.heatmap import GeoBounds
13
14
  from geo_activity_playground.core.heatmap import get_sensible_zoom_level
@@ -29,10 +30,14 @@ OSM_TILE_SIZE = 256 # OSM tile size in pixel
29
30
 
30
31
  class HeatmapController:
31
32
  def __init__(
32
- self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
33
+ self,
34
+ repository: ActivityRepository,
35
+ tile_visit_accessor: TileVisitAccessor,
36
+ config: Config,
33
37
  ) -> None:
34
38
  self._repository = repository
35
39
  self._tile_visit_accessor = tile_visit_accessor
40
+ self._config = config
36
41
 
37
42
  self.tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
38
43
  self.tile_evolution_states = self._tile_visit_accessor.tile_state[
@@ -140,11 +145,11 @@ class HeatmapController:
140
145
  tile_counts = np.sqrt(tile_counts) / 5
141
146
  tile_counts[tile_counts > 1.0] = 1.0
142
147
 
143
- cmap = pl.get_cmap("hot")
148
+ cmap = pl.get_cmap(self._config.color_scheme_for_heatmap)
144
149
  data_color = cmap(tile_counts)
145
150
  data_color[data_color == cmap(0.0)] = 0.0 # remove background color
146
151
 
147
- map_tile = np.array(get_tile(z, x, y)) / 255
152
+ map_tile = np.array(get_tile(z, x, y, self._config.map_tile_url)) / 255
148
153
  map_tile = convert_to_grayscale(map_tile)
149
154
  map_tile = 1.0 - map_tile # invert colors
150
155
  for c in range(3):
@@ -168,7 +173,12 @@ class HeatmapController:
168
173
  background = np.zeros((*pixel_bounds.shape, 3))
169
174
  for x in range(tile_bounds.x_tile_min, tile_bounds.x_tile_max):
170
175
  for y in range(tile_bounds.y_tile_min, tile_bounds.y_tile_max):
171
- tile = np.array(get_tile(tile_bounds.zoom, x, y)) / 255
176
+ tile = (
177
+ np.array(
178
+ get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
179
+ )
180
+ / 255
181
+ )
172
182
 
173
183
  i = y - tile_bounds.y_tile_min
174
184
  j = x - tile_bounds.x_tile_min
@@ -35,7 +35,7 @@
35
35
  });
36
36
  L.tileLayer('/heatmap/tile/{z}/{x}/{y}/{{ kinds_str }}.png', {
37
37
  maxZoom: 19,
38
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
38
+ attribution: '{{ map_tile_attribution|safe }}'
39
39
  }).addTo(map)
40
40
 
41
41
  let bbox = {{ center.bbox| safe }}