geo-activity-playground 0.35.0__tar.gz → 0.35.1__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 (132) hide show
  1. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/PKG-INFO +1 -1
  2. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/__main__.py +12 -0
  3. geo_activity_playground-0.35.1/geo_activity_playground/core/raster_map.py +250 -0
  4. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/tiles.py +6 -50
  5. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/explorer/video.py +1 -1
  6. geo_activity_playground-0.35.1/geo_activity_playground/heatmap_video.py +93 -0
  7. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/activity_parsers.py +1 -1
  8. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/blueprint.py +3 -10
  9. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/controller.py +10 -71
  10. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/app.py +44 -26
  11. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/calendar/blueprint.py +2 -5
  12. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/eddington/blueprint.py +1 -4
  13. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/equipment/blueprint.py +2 -8
  14. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/explorer/blueprint.py +2 -10
  15. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/heatmap/heatmap_controller.py +10 -12
  16. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/summary/blueprint.py +1 -4
  17. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/summary/controller.py +0 -1
  18. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/tile/blueprint.py +1 -4
  19. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/tile/controller.py +1 -1
  20. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/pyproject.toml +1 -1
  21. geo_activity_playground-0.35.0/geo_activity_playground/core/heatmap.py +0 -194
  22. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/LICENSE +0 -0
  23. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/__init__.py +0 -0
  24. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/__init__.py +0 -0
  25. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/activities.py +0 -0
  26. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/config.py +0 -0
  27. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/coordinates.py +0 -0
  28. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/enrichment.py +0 -0
  29. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/heart_rate.py +0 -0
  30. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/paths.py +0 -0
  31. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/privacy_zones.py +0 -0
  32. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/similarity.py +0 -0
  33. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/tasks.py +0 -0
  34. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/test_tiles.py +0 -0
  35. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/test_time_conversion.py +0 -0
  36. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/core/time_conversion.py +0 -0
  37. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/explorer/__init__.py +0 -0
  38. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/explorer/grid_file.py +0 -0
  39. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/explorer/tile_visits.py +0 -0
  40. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/__init__.py +0 -0
  41. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/csv_parser.py +0 -0
  42. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/directory.py +0 -0
  43. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/strava_api.py +0 -0
  44. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/strava_checkout.py +0 -0
  45. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  46. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/test_directory.py +0 -0
  47. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
  48. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/__init__.py +0 -0
  49. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/__init__.py +0 -0
  50. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -0
  51. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -0
  52. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
  53. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +0 -0
  54. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +0 -0
  55. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/auth/blueprint.py +0 -0
  56. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
  57. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/authenticator.py +0 -0
  58. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/calendar/__init__.py +0 -0
  59. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/calendar/controller.py +0 -0
  60. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
  61. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
  62. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/eddington/__init__.py +0 -0
  63. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/eddington/controller.py +0 -0
  64. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
  65. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/entry_controller.py +0 -0
  66. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/equipment/__init__.py +0 -0
  67. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/equipment/controller.py +0 -0
  68. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +0 -0
  69. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/explorer/__init__.py +0 -0
  70. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/explorer/controller.py +0 -0
  71. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
  72. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
  73. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
  74. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
  75. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/plot_util.py +0 -0
  76. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/search/blueprint.py +0 -0
  77. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -0
  78. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/blueprint.py +0 -0
  79. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/controller.py +0 -0
  80. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
  81. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
  82. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
  83. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
  84. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +0 -0
  85. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -0
  86. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
  87. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
  88. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
  89. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +0 -0
  90. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
  91. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
  92. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
  93. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
  94. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/square_planner/controller.py +0 -0
  95. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
  96. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +0 -0
  97. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/MarkerCluster.Default.css +0 -0
  98. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/MarkerCluster.css +0 -0
  99. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  100. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
  101. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  102. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
  103. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/bootstrap.bundle.min.js +0 -0
  104. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/bootstrap.min.css +0 -0
  105. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  106. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  107. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  108. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
  109. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
  110. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/favicon.svg +0 -0
  111. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/fullscreen.png +0 -0
  112. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  113. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/leaflet.css +0 -0
  114. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/leaflet.fullscreen.css +0 -0
  115. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/leaflet.js +0 -0
  116. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/leaflet.markercluster.js +0 -0
  117. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  118. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  119. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/table-sort.min.js +0 -0
  120. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/vega-embed@6 +0 -0
  121. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/vega-lite@4 +0 -0
  122. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/vega@5 +0 -0
  123. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
  124. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
  125. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/summary/__init__.py +0 -0
  126. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +0 -0
  127. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/templates/home.html.j2 +0 -0
  128. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  129. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/templates/upload/index.html.j2 +0 -0
  130. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/templates/upload/reload.html.j2 +0 -0
  131. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/tile/__init__.py +0 -0
  132. {geo_activity_playground-0.35.0 → geo_activity_playground-0.35.1}/geo_activity_playground/webui/upload_blueprint.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.35.0
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
@@ -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")