geo-activity-playground 0.7.0__tar.gz → 0.8.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 (50) hide show
  1. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/PKG-INFO +4 -2
  2. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/__main__.py +3 -9
  3. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/activities.py +24 -15
  4. geo_activity_playground-0.8.0/geo_activity_playground/core/config.py +22 -0
  5. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/explorer/grid_file.py +74 -34
  6. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/importers/strava_api.py +2 -8
  7. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/activity_controller.py +4 -1
  8. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/explorer_controller.py +17 -11
  9. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/explorer.html.j2 +19 -7
  10. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/heatmap.html.j2 +1 -1
  11. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/pyproject.toml +3 -2
  12. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/__init__.py +0 -0
  13. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/__init__.py +0 -0
  14. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/activity_parsers.py +0 -0
  15. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/coordinates.py +0 -0
  16. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/heatmap.py +0 -0
  17. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/plots.py +0 -0
  18. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/sources.py +0 -0
  19. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/tasks.py +0 -0
  20. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/test_tiles.py +0 -0
  21. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/core/tiles.py +0 -0
  22. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/explorer/__init__.py +0 -0
  23. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/explorer/converters.py +0 -0
  24. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/explorer/video.py +0 -0
  25. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/heatmap.py +0 -0
  26. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/importers/directory.py +0 -0
  27. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/app.py +0 -0
  28. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/calendar_controller.py +0 -0
  29. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/eddington_controller.py +0 -0
  30. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  31. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/equipment_controller.py +0 -0
  32. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/heatmap_controller.py +0 -0
  33. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  34. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  35. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  36. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  37. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  38. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  39. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  40. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  41. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  42. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  43. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/activity.html.j2 +0 -0
  44. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/calendar-month.html.j2 +0 -0
  45. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
  46. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
  47. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
  48. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
  49. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  50. {geo_activity_playground-0.7.0 → geo_activity_playground-0.8.0}/geo_activity_playground/webui/templates/summary-statistics.html.j2 +0 -0
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
7
7
  Author-email: mu@martin-ueding.de
8
- Requires-Python: >=3.10,<3.12
8
+ Requires-Python: >=3.9,<3.12
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
11
12
  Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Requires-Dist: Pillow (>=9.2.0,<10.0.0)
@@ -27,6 +28,7 @@ Requires-Dist: requests (>=2.28.1,<3.0.0)
27
28
  Requires-Dist: scikit-learn (>=1.3.0,<2.0.0)
28
29
  Requires-Dist: scipy (>=1.8.1,<2.0.0)
29
30
  Requires-Dist: stravalib (>=1.3.3,<2.0.0)
31
+ Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
30
32
  Requires-Dist: tqdm (>=4.64.0,<5.0.0)
31
33
  Requires-Dist: vegafusion (>=1.4.3,<2.0.0)
32
34
  Requires-Dist: vegafusion-python-embed (>=1.4.3,<2.0.0)
@@ -1,16 +1,11 @@
1
1
  import argparse
2
2
  import os
3
3
  import pathlib
4
- import tomllib
5
4
 
6
5
  import coloredlogs
7
6
 
8
7
  from geo_activity_playground.core.activities import ActivityRepository
9
- from geo_activity_playground.core.sources import TimeSeriesSource
10
- from geo_activity_playground.explorer.grid_file import get_border_tiles
11
- from geo_activity_playground.explorer.grid_file import get_explored_tiles
12
- from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
13
- from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
8
+ from geo_activity_playground.core.config import get_config
14
9
  from geo_activity_playground.explorer.video import explorer_video_main
15
10
  from geo_activity_playground.heatmap import generate_heatmaps_per_cluster
16
11
  from geo_activity_playground.importers.directory import import_from_directory
@@ -78,11 +73,10 @@ def main() -> None:
78
73
 
79
74
  def make_activity_repository(basedir: pathlib.Path) -> ActivityRepository:
80
75
  os.chdir(basedir)
76
+ config = get_config()
81
77
  if pathlib.Path("Activities").exists():
82
78
  import_from_directory()
83
- elif pathlib.Path("config.toml").exists():
84
- with open("config.toml", "rb") as f:
85
- config = tomllib.load(f)
79
+ elif config:
86
80
  if "strava" in config:
87
81
  import_from_strava_api()
88
82
  return ActivityRepository()
@@ -2,16 +2,15 @@ import dataclasses
2
2
  import datetime
3
3
  import functools
4
4
  import logging
5
- import pathlib
6
- import tomllib
7
5
  from typing import Iterator
8
6
  from typing import Optional
9
7
 
10
8
  import geojson
11
9
  import matplotlib
12
- import numpy as np
13
10
  import pandas as pd
14
11
 
12
+ from geo_activity_playground.core.config import get_config
13
+
15
14
 
16
15
  logger = logging.getLogger(__name__)
17
16
 
@@ -105,25 +104,35 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
105
104
  def extract_heart_rate_zones(time_series: pd.DataFrame) -> Optional[pd.DataFrame]:
106
105
  if "heartrate" not in time_series:
107
106
  return None
108
- config_path = pathlib.Path("config.toml")
109
- if not config_path.exists():
110
- logger.warning("Missing a config, cannot extract heart rate zones.")
111
- return None
112
- with open(config_path, "rb") as f:
113
- config = tomllib.load(f)
114
-
107
+ config = get_config()
115
108
  try:
116
- birthyear = config["heart"]["birthyear"]
109
+ heart_config = config["heart"]
117
110
  except KeyError:
118
111
  logger.warning(
119
- "Missing config entry `heart.birthyear`, cannot determine heart rate zones."
112
+ "Missing config entry `heart`, cannot determine heart rate zones."
113
+ )
114
+ return None
115
+
116
+ birthyear = heart_config.get("birthyear", None)
117
+ maximum = heart_config.get("maximum", None)
118
+ resting = heart_config.get("resting", None)
119
+
120
+ if not maximum and birthyear:
121
+ age = time_series["time"].iloc[0].year - birthyear
122
+ maximum = 220 - age
123
+ if not resting:
124
+ resting = 0
125
+ if not maximum:
126
+ logger.warning(
127
+ "Missing config entry `heart.maximum` or `heart.birthyear`, cannot determine heart rate zones."
120
128
  )
121
129
  return None
122
130
 
123
- age = time_series["time"].iloc[0].year - birthyear
124
- max_rate = 220 - age
125
- zones: pd.Series = time_series["heartrate"] * 10 // max_rate - 4
131
+ zones: pd.Series = (time_series["heartrate"] - resting) * 10 // (
132
+ maximum - resting
133
+ ) - 4
126
134
  zones.loc[zones < 0] = 0
135
+ zones.loc[zones > 5] = 5
127
136
  df = pd.DataFrame({"heartzone": zones, "step": time_series["time"].diff()}).dropna()
128
137
  duration_per_zone = df.groupby("heartzone").sum()["step"].dt.total_seconds() / 60
129
138
  duration_per_zone.name = "minutes"
@@ -0,0 +1,22 @@
1
+ import functools
2
+ import logging
3
+ import pathlib
4
+
5
+
6
+ try:
7
+ import tomllib
8
+ except ModuleNotFoundError:
9
+ import tomli as tomllib
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @functools.cache
16
+ def get_config() -> dict:
17
+ config_path = pathlib.Path("config.toml")
18
+ if not config_path.exists():
19
+ logger.warning("Missing a config, some features might be missing.")
20
+ return {}
21
+ with open(config_path, "rb") as f:
22
+ return tomllib.load(f)
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import pathlib
2
3
  from typing import Iterator
3
4
 
@@ -6,12 +7,16 @@ import gpxpy
6
7
  import numpy as np
7
8
  import pandas as pd
8
9
  import scipy.ndimage
10
+ import sklearn.cluster
9
11
 
10
12
  from geo_activity_playground.core.activities import ActivityRepository
11
13
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
12
14
  from geo_activity_playground.explorer.converters import get_tile_history
13
15
 
14
16
 
17
+ logger = logging.getLogger(__name__)
18
+
19
+
15
20
  def get_three_color_tiles(
16
21
  tiles: pd.DataFrame, repository: ActivityRepository, zoom: int
17
22
  ) -> str:
@@ -19,7 +24,10 @@ def get_three_color_tiles(
19
24
  a = np.zeros((2**zoom, 2**zoom), dtype=np.int8)
20
25
  a[tiles["tile_x"], tiles["tile_y"]] = 1
21
26
 
22
- tile_dict = {elem: 1 for elem in zip(tiles["tile_x"], tiles["tile_y"])}
27
+ tile_dict = {
28
+ elem: {"cluster": False, "square": False}
29
+ for elem in zip(tiles["tile_x"], tiles["tile_y"])
30
+ }
23
31
 
24
32
  for x, y in tile_dict.keys():
25
33
  if (
@@ -28,7 +36,7 @@ def get_three_color_tiles(
28
36
  and (x, y + 1) in tile_dict
29
37
  and (x, y - 1) in tile_dict
30
38
  ):
31
- tile_dict[(x, y)] = 2
39
+ tile_dict[(x, y)]["cluster"] = True
32
40
 
33
41
  # Compute biggest square.
34
42
  square_size = 1
@@ -52,43 +60,75 @@ def get_three_color_tiles(
52
60
  square_x, square_y, square_size = biggest
53
61
  for x in range(square_x, square_x + square_size):
54
62
  for y in range(square_y, square_y + square_size):
55
- tile_dict[(x, y)] = 3
56
-
57
- tile_metadata = {
58
- (row["tile_x"], row["tile_y"]): {
59
- "first_visit": row["time"].date().isoformat(),
60
- "activity_id": row["activity_id"],
61
- "activity_name": repository.get_activity_by_id(row["activity_id"]).name,
62
- }
63
- for index, row in tiles.iterrows()
64
- }
65
-
66
- # Find non-zero tiles.
67
- return geojson.dumps(
68
- geojson.FeatureCollection(
69
- features=[
70
- make_explorer_tile(
71
- x,
72
- y,
73
- {
74
- "color": {1: "red", 2: "green", 3: "blue"}[v],
75
- **tile_metadata[(x, y)],
76
- },
77
- zoom,
78
- )
79
- for (x, y), v in tile_dict.items()
80
- ]
63
+ tile_dict[(x, y)]["square"] = True
64
+
65
+ for index, row in tiles.iterrows():
66
+ tile_dict[(row["tile_x"], row["tile_y"])].update(
67
+ {
68
+ "first_visit": row["time"].date().isoformat(),
69
+ "activity_id": row["activity_id"],
70
+ "activity_name": repository.get_activity_by_id(row["activity_id"]).name,
71
+ "color": map_color(tile_dict[(row["tile_x"], row["tile_y"])]),
72
+ }
81
73
  )
74
+
75
+ num_cluster_tiles = sum(value["cluster"] for value in tile_dict.values())
76
+
77
+ cluster_tiles = np.array(
78
+ [tile for tile, value in tile_dict.items() if value["cluster"]]
82
79
  )
83
80
 
81
+ logger.info("Run DBSCAN cluster finding algorithm …")
82
+ dbscan = sklearn.cluster.DBSCAN(eps=1.1, min_samples=1)
83
+ labels = dbscan.fit_predict(cluster_tiles)
84
+ label_counts = dict(zip(*np.unique(labels, return_counts=True)))
85
+ max_cluster_size = max(
86
+ count for label, count in label_counts.items() if label != -1
87
+ )
88
+ for xy, label in zip(cluster_tiles, labels):
89
+ tile_dict[tuple(xy)]["cluster_id"] = int(label)
90
+ tile_dict[tuple(xy)]["this_cluster_size"] = int(label_counts[label])
91
+
92
+ # Find non-zero tiles.
93
+ result = {
94
+ "explored_geojson": geojson.dumps(
95
+ geojson.FeatureCollection(
96
+ features=[
97
+ make_explorer_tile(
98
+ x,
99
+ y,
100
+ tile_dict[(x, y)],
101
+ zoom,
102
+ )
103
+ for (x, y), v in tile_dict.items()
104
+ ]
105
+ )
106
+ ),
107
+ "max_cluster_size": max_cluster_size,
108
+ "num_cluster_tiles": num_cluster_tiles,
109
+ "num_tiles": len(tile_dict),
110
+ "square_size": square_size,
111
+ }
112
+ return result
113
+
114
+
115
+ def map_color(tile_meta: dict) -> str:
116
+ if tile_meta["square"]:
117
+ return "blue"
118
+ elif tile_meta["cluster"]:
119
+ return "green"
120
+ else:
121
+ return "red"
122
+
84
123
 
85
124
  def get_border_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
86
- a = np.zeros((2**zoom, 2**zoom), dtype=np.int8)
87
- a[tiles["tile_x"], tiles["tile_y"]] = 1
88
- dilated = scipy.ndimage.binary_dilation(a, iterations=2)
89
- border = dilated - a
90
- border_x, border_y = np.where(border)
91
- return make_grid_points(zip(border_x, border_y), zoom)
125
+ tile_set = set(zip(tiles["tile_x"], tiles["tile_y"]))
126
+ border_tiles = set()
127
+ for x, y in tile_set:
128
+ for neighbor in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
129
+ if neighbor not in tile_set:
130
+ border_tiles.add(neighbor)
131
+ return make_grid_points(border_tiles, zoom)
92
132
 
93
133
 
94
134
  def get_explored_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
@@ -4,22 +4,16 @@ import json
4
4
  import logging
5
5
  import pathlib
6
6
  import pickle
7
- import tomllib
8
7
  from typing import Any
9
8
 
10
9
  import pandas as pd
11
10
  from stravalib import Client
12
11
  from stravalib.exc import RateLimitExceeded
13
12
 
14
-
15
- logger = logging.getLogger(__name__)
13
+ from geo_activity_playground.core.config import get_config
16
14
 
17
15
 
18
- @functools.cache
19
- def get_config() -> dict:
20
- config_path = pathlib.Path("config.toml")
21
- with open(config_path, "rb") as f:
22
- return tomllib.load(f)
16
+ logger = logging.getLogger(__name__)
23
17
 
24
18
 
25
19
  def get_state(path: pathlib.Path) -> Any:
@@ -19,6 +19,7 @@ class ActivityController:
19
19
  activity = self._repository.get_activity_by_id(id)
20
20
 
21
21
  time_series = self._repository.get_time_series(id)
22
+ time_series["distance/km"] = time_series["distance"] / 1000
22
23
  line_json = make_geojson_from_time_series(time_series)
23
24
 
24
25
  result = {
@@ -40,7 +41,9 @@ def distance_time_plot(time_series: pd.DataFrame) -> str:
40
41
  return (
41
42
  alt.Chart(time_series, title="Distance")
42
43
  .mark_line()
43
- .encode(alt.X("time", title="Time"), alt.Y("distance", title="Distance / km"))
44
+ .encode(
45
+ alt.X("time", title="Time"), alt.Y("distance/km", title="Distance / km")
46
+ )
44
47
  .interactive()
45
48
  .to_json(format="vega")
46
49
  )
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
 
3
3
  from geo_activity_playground.core.activities import ActivityRepository
4
+ from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
4
5
  from geo_activity_playground.explorer.converters import get_tile_history
5
6
  from geo_activity_playground.explorer.grid_file import get_border_tiles
6
7
  from geo_activity_playground.explorer.grid_file import get_explored_geojson
@@ -17,21 +18,26 @@ class ExplorerController:
17
18
  @functools.cache
18
19
  def render(self, zoom: int) -> dict:
19
20
  tiles = get_tile_history(self._repository, zoom)
21
+ medians = tiles.median()
22
+ median_lat, median_lon = get_tile_upper_left_lat_lon(
23
+ medians["tile_x"], medians["tile_y"], zoom
24
+ )
20
25
 
21
- explored_geojson = get_three_color_tiles(tiles, self._repository, zoom)
26
+ explored = get_three_color_tiles(tiles, self._repository, zoom)
22
27
 
23
- if zoom <= 14:
24
- points = get_border_tiles(tiles, zoom)
25
- missing_tiles_geojson = make_grid_file_geojson(points, "missing_tiles")
26
- make_grid_file_gpx(points, "missing_tiles")
28
+ points = get_border_tiles(tiles, zoom)
29
+ missing_tiles_geojson = make_grid_file_geojson(points, "missing_tiles")
30
+ make_grid_file_gpx(points, "missing_tiles")
27
31
 
28
- points = get_explored_tiles(tiles, zoom)
29
- explored_tiles_geojson = make_grid_file_geojson(points, "explored")
30
- make_grid_file_gpx(points, "explored")
31
- else:
32
- missing_tiles_geojson = {}
32
+ points = get_explored_tiles(tiles, zoom)
33
+ explored_tiles_geojson = make_grid_file_geojson(points, "explored")
34
+ make_grid_file_gpx(points, "explored")
33
35
 
34
36
  return {
35
- "explored_geojson": explored_geojson,
37
+ "center": {
38
+ "latitude": median_lat,
39
+ "longitude": median_lon,
40
+ },
41
+ "explored": explored,
36
42
  "missing_tiles_geojson": missing_tiles_geojson,
37
43
  }
@@ -4,6 +4,11 @@
4
4
  <div class="row mb-3">
5
5
  <div class="col">
6
6
  <h1>Explorer Tiles</h1>
7
+ <p>You have {{ explored.num_tiles }} explored tiles. There are {{ explored.num_cluster_tiles }} cluster tiles in
8
+ total. Your largest cluster consists of {{ explored.max_cluster_size }} tiles. Your largest square has size
9
+ {{
10
+ explored.square_size }}².
11
+ </p>
7
12
  </div>
8
13
  </div>
9
14
 
@@ -22,14 +27,21 @@
22
27
  <script>
23
28
  function onEachFeature(feature, layer) {
24
29
  if (feature.properties && feature.properties.first_visit) {
25
- layer.bindPopup(`First visited on ${feature.properties.first_visit} as part of <a href=/activity/${feature.properties.activity_id}>${feature.properties.activity_name}</a>.`)
30
+ let lines = [`First visit: ${feature.properties.first_visit}`,
31
+ `First activity: <a href=/activity/${feature.properties.activity_id}>${feature.properties.activity_name}</a>`]
32
+ if (feature.properties.this_cluster_size) {
33
+ lines.push(`This cluster size: ${feature.properties.this_cluster_size}`)
34
+ }
35
+ layer.bindPopup(lines.join('<br />'))
26
36
  }
27
37
  }
28
38
 
29
- let explorer_geojson = {{ explored_geojson| safe}}
39
+ let explorer_geojson = {{ explored.explored_geojson| safe}}
30
40
  let map = L.map('explorer-map', {
31
- fullscreenControl: true
32
- });
41
+ fullscreenControl: true,
42
+ center: [{{ center.latitude }}, {{ center.longitude }}],
43
+ zoom: 10
44
+ })
33
45
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
34
46
  maxZoom: 19,
35
47
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
@@ -38,7 +50,6 @@
38
50
  style: function (feature) { return { color: feature.properties.color, fillColor: feature.properties.color, weight: 1 } },
39
51
  onEachFeature: onEachFeature
40
52
  }).addTo(map)
41
- map.fitBounds(explorer_layer.getBounds())
42
53
  </script>
43
54
  </div>
44
55
  </div>
@@ -59,14 +70,15 @@
59
70
  <script>
60
71
  let missing_geojson = {{ missing_tiles_geojson| safe}}
61
72
  let missing_map = L.map('missing-map', {
62
- fullscreenControl: true
73
+ fullscreenControl: true,
74
+ center: [{{ center.latitude }}, {{ center.longitude }}],
75
+ zoom: 10
63
76
  });
64
77
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
65
78
  maxZoom: 19,
66
79
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
67
80
  }).addTo(missing_map)
68
81
  let missing_layer = L.geoJSON(missing_geojson).addTo(missing_map)
69
- missing_map.fitBounds(missing_layer.getBounds())
70
82
  </script>
71
83
  </div>
72
84
  </div>
@@ -15,7 +15,7 @@
15
15
  let map = L.map('heatmap', {
16
16
  fullscreenControl: true,
17
17
  center: [{{ center.latitude }}, {{ center.longitude }}],
18
- zoom: 12
18
+ zoom: 12
19
19
  });
20
20
  L.tileLayer('/heatmap/tile/{z}/{x}/{y}.png', {
21
21
  maxZoom: 19,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "geo-activity-playground"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Analysis of geo data activities like rides, runs or hikes."
5
5
  authors = ["Martin Ueding <mu@martin-ueding.de>"]
6
6
  license = "MIT"
@@ -9,7 +9,7 @@ license = "MIT"
9
9
  geo-activity-playground = "geo_activity_playground.__main__:main"
10
10
 
11
11
  [tool.poetry.dependencies]
12
- python = "^3.10,<3.12"
12
+ python = "^3.9,<3.12"
13
13
 
14
14
  altair = "^5.1.2"
15
15
  appdirs = "^1.4.4"
@@ -28,6 +28,7 @@ requests = "^2.28.1"
28
28
  scikit-learn = "^1.3.0"
29
29
  scipy = "^1.8.1"
30
30
  stravalib = "^1.3.3"
31
+ tomli = { version = "^2.0.1", python = "<3.11" }
31
32
  tqdm = "^4.64.0"
32
33
  vegafusion = "^1.4.3"
33
34
  vegafusion-python-embed = "^1.4.3"