geo-activity-playground 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)
@@ -11,7 +11,7 @@ from PIL import Image
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
- def compute_tile(lat: float, lon: float, zoom: int = 14) -> tuple[int, int]:
14
+ def compute_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
15
15
  x = np.radians(lon)
16
16
  y = np.arcsinh(np.tan(np.radians(lat)))
17
17
  x = (1 + x / np.pi) / 2
@@ -19,25 +19,26 @@ def explorer_per_activity_cache_dir() -> pathlib.Path:
19
19
  return path
20
20
 
21
21
 
22
- def get_first_tiles(id, repository: ActivityRepository) -> pd.DataFrame:
23
- target_path = explorer_per_activity_cache_dir() / f"{id}.parquet"
22
+ def get_first_tiles(id, repository: ActivityRepository, zoom: int) -> pd.DataFrame:
23
+ target_path = explorer_per_activity_cache_dir() / str(zoom) / f"{id}.parquet"
24
24
  if target_path.exists():
25
25
  return pd.read_parquet(target_path)
26
26
  else:
27
27
  logger.info(f"Extracting tiles from activity {id} …")
28
28
  time_series = repository.get_time_series(id)
29
- tiles = tiles_from_points(time_series)
29
+ tiles = tiles_from_points(time_series, zoom)
30
30
  first_tiles = first_time_per_tile(tiles)
31
+ target_path.parent.mkdir(exist_ok=True, parents=True)
31
32
  first_tiles.to_parquet(target_path)
32
33
  return first_tiles
33
34
 
34
35
 
35
- def tiles_from_points(points: pd.DataFrame) -> pd.DataFrame:
36
+ def tiles_from_points(points: pd.DataFrame, zoom: int) -> pd.DataFrame:
36
37
  assert pd.api.types.is_dtype_equal(points["time"].dtype, "datetime64[ns, UTC]")
37
38
  new_rows = []
38
39
  for index, row in points.iterrows():
39
40
  if "latitude" in row.keys() and "longitude" in row.keys():
40
- tile = compute_tile(row["latitude"], row["longitude"])
41
+ tile = compute_tile(row["latitude"], row["longitude"], zoom)
41
42
  new_rows.append((row["time"],) + tile)
42
43
  return pd.DataFrame(new_rows, columns=["time", "tile_x", "tile_y"])
43
44
 
@@ -48,17 +49,17 @@ def first_time_per_tile(tiles: pd.DataFrame) -> pd.DataFrame:
48
49
 
49
50
 
50
51
  @functools.cache
51
- def get_tile_history(repository: ActivityRepository) -> pd.DataFrame:
52
+ def get_tile_history(repository: ActivityRepository, zoom: int) -> pd.DataFrame:
52
53
  logger.info("Building explorer tile history from all activities …")
53
54
 
54
- cache_file = pathlib.Path("Cache/first_time_per_tile.parquet")
55
+ cache_file = pathlib.Path(f"Cache/first_time_per_tile_{zoom}.parquet")
55
56
  if cache_file.exists():
56
57
  tiles = pd.read_parquet(cache_file)
57
58
  else:
58
59
  tiles = pd.DataFrame()
59
60
 
60
61
  with work_tracker(
61
- pathlib.Path("Cache/task_first_time_per_tile.json")
62
+ pathlib.Path(f"Cache/task_first_time_per_tile_{zoom}.json")
62
63
  ) as parsed_activities:
63
64
  for activity in repository.iter_activities(new_to_old=False):
64
65
  if activity.id in parsed_activities:
@@ -66,7 +67,7 @@ def get_tile_history(repository: ActivityRepository) -> pd.DataFrame:
66
67
  parsed_activities.add(activity.id)
67
68
 
68
69
  logger.info(f"Activity {activity.id} wasn't parsed yet, reading them …")
69
- shard = get_first_tiles(activity.id, repository)
70
+ shard = get_first_tiles(activity.id, repository, zoom)
70
71
  shard["activity_id"] = activity.id
71
72
  if not len(shard):
72
73
  continue
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import pathlib
2
3
  from typing import Iterator
3
4
 
@@ -6,30 +7,45 @@ 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
 
15
- def get_three_color_tiles(tiles: pd.DataFrame, repository: ActivityRepository) -> str:
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def get_three_color_tiles(
21
+ tiles: pd.DataFrame, repository: ActivityRepository, zoom: int
22
+ ) -> str:
16
23
  # Create array with visited tiles.
17
- a = np.zeros((2**14, 2**14), dtype=np.int8)
24
+ a = np.zeros((2**zoom, 2**zoom), dtype=np.int8)
18
25
  a[tiles["tile_x"], tiles["tile_y"]] = 1
19
26
 
20
- # Get cluster tiles via erosion.
21
- cluster = scipy.ndimage.binary_erosion(a)
22
- a[cluster] = 2
27
+ tile_dict = {
28
+ elem: {"cluster": False, "square": False}
29
+ for elem in zip(tiles["tile_x"], tiles["tile_y"])
30
+ }
31
+
32
+ for x, y in tile_dict.keys():
33
+ if (
34
+ (x + 1, y) in tile_dict
35
+ and (x - 1, y) in tile_dict
36
+ and (x, y + 1) in tile_dict
37
+ and (x, y - 1) in tile_dict
38
+ ):
39
+ tile_dict[(x, y)]["cluster"] = True
23
40
 
24
41
  # Compute biggest square.
25
42
  square_size = 1
26
43
  biggest = None
27
- tile_set = {elem for elem in zip(tiles["tile_x"], tiles["tile_y"])}
28
- for x, y in sorted(tile_set):
44
+ for x, y in sorted(tile_dict):
29
45
  while True:
30
46
  for i in range(square_size):
31
47
  for j in range(square_size):
32
- if (x + i, y + j) not in tile_set:
48
+ if (x + i, y + j) not in tile_dict:
33
49
  break
34
50
  else:
35
51
  continue
@@ -42,58 +58,94 @@ def get_three_color_tiles(tiles: pd.DataFrame, repository: ActivityRepository) -
42
58
 
43
59
  if biggest is not None:
44
60
  square_x, square_y, square_size = biggest
45
- a[square_x : square_x + square_size, square_y : square_y + square_size] = 3
46
-
47
- tile_metadata = {
48
- (row["tile_x"], row["tile_y"]): {
49
- "first_visit": row["time"].date().isoformat(),
50
- "activity_id": row["activity_id"],
51
- "activity_name": repository.get_activity_by_id(row["activity_id"]).name,
52
- }
53
- for index, row in tiles.iterrows()
54
- }
55
-
56
- # Find non-zero tiles.
57
- border_x, border_y = np.where(a)
58
- return geojson.dumps(
59
- geojson.FeatureCollection(
60
- features=[
61
- make_explorer_tile(
62
- x,
63
- y,
64
- {
65
- "color": {1: "red", 2: "green", 3: "blue"}[a[x, y]],
66
- **tile_metadata[(x, y)],
67
- },
68
- )
69
- for x, y in zip(border_x, border_y)
70
- ]
61
+ for x in range(square_x, square_x + square_size):
62
+ for y in range(square_y, square_y + square_size):
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
+ }
71
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"]]
72
79
  )
73
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])
74
91
 
75
- def get_border_tiles(tiles: pd.DataFrame) -> list[list[list[float]]]:
76
- a = np.zeros((2**14, 2**14), dtype=np.int8)
77
- a[tiles["tile_x"], tiles["tile_y"]] = 1
78
- dilated = scipy.ndimage.binary_dilation(a, iterations=2)
79
- border = dilated - a
80
- border_x, border_y = np.where(border)
81
- return make_grid_points(zip(border_x, border_y))
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
+
123
+
124
+ def get_border_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
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)
82
132
 
83
133
 
84
- def get_explored_tiles(tiles: pd.DataFrame) -> list[list[list[float]]]:
85
- return make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]))
134
+ def get_explored_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
135
+ return make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]), zoom)
86
136
 
87
137
 
88
- def make_explorer_tile(tile_x: int, tile_y: int, properties: dict) -> geojson.Feature:
138
+ def make_explorer_tile(
139
+ tile_x: int, tile_y: int, properties: dict, zoom: int
140
+ ) -> geojson.Feature:
89
141
  corners = [
90
142
  get_tile_upper_left_lat_lon(*args)
91
143
  for args in [
92
- (tile_x, tile_y, 14),
93
- (tile_x + 1, tile_y, 14),
94
- (tile_x + 1, tile_y + 1, 14),
95
- (tile_x, tile_y + 1, 14),
96
- (tile_x, tile_y, 14),
144
+ (tile_x, tile_y, zoom),
145
+ (tile_x + 1, tile_y, zoom),
146
+ (tile_x + 1, tile_y + 1, zoom),
147
+ (tile_x, tile_y + 1, zoom),
148
+ (tile_x, tile_y, zoom),
97
149
  ]
98
150
  ]
99
151
  return geojson.Feature(
@@ -103,16 +155,16 @@ def make_explorer_tile(tile_x: int, tile_y: int, properties: dict) -> geojson.Fe
103
155
 
104
156
 
105
157
  def make_grid_points(
106
- tile_iterator: Iterator[tuple[int, int]]
158
+ tile_iterator: Iterator[tuple[int, int]], zoom: int
107
159
  ) -> list[list[list[float]]]:
108
160
  result = []
109
161
  for tile_x, tile_y in tile_iterator:
110
162
  tile = [
111
- get_tile_upper_left_lat_lon(tile_x, tile_y, 14),
112
- get_tile_upper_left_lat_lon(tile_x + 1, tile_y, 14),
113
- get_tile_upper_left_lat_lon(tile_x + 1, tile_y + 1, 14),
114
- get_tile_upper_left_lat_lon(tile_x, tile_y + 1, 14),
115
- get_tile_upper_left_lat_lon(tile_x, tile_y, 14),
163
+ get_tile_upper_left_lat_lon(tile_x, tile_y, zoom),
164
+ get_tile_upper_left_lat_lon(tile_x + 1, tile_y, zoom),
165
+ get_tile_upper_left_lat_lon(tile_x + 1, tile_y + 1, zoom),
166
+ get_tile_upper_left_lat_lon(tile_x, tile_y + 1, zoom),
167
+ get_tile_upper_left_lat_lon(tile_x, tile_y, zoom),
116
168
  ]
117
169
  result.append(tile)
118
170
  return result
@@ -153,8 +205,8 @@ def make_grid_file_geojson(grid_points: list[list[list[float]]], stem: str) -> s
153
205
  return result
154
206
 
155
207
 
156
- def get_explored_geojson(repository: ActivityRepository) -> str:
157
- tiles = get_tile_history(repository)
208
+ def get_explored_geojson(repository: ActivityRepository, zoom: int) -> str:
209
+ tiles = get_tile_history(repository, zoom)
158
210
  return make_grid_file_geojson(
159
211
  make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]))
160
212
  )
@@ -211,7 +211,7 @@ def generate_heatmaps_per_cluster(repository: ActivityRepository) -> None:
211
211
  del arrays
212
212
 
213
213
  logger.info("Compute tiles for each point …")
214
- tiles = [compute_tile(lat, lon) for lat, lon in latlon]
214
+ tiles = [compute_tile(lat, lon, 14) for lat, lon in latlon]
215
215
 
216
216
  unique_tiles = set(tiles)
217
217
  unique_tiles_array = np.array(list(unique_tiles))
@@ -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
  )
@@ -33,19 +33,21 @@ def webui_main(repository: ActivityRepository) -> None:
33
33
  return render_template("index.html.j2", **entry_controller.render())
34
34
 
35
35
  @app.route("/activity/<id>")
36
- def activity(id: int):
36
+ def activity(id: str):
37
37
  return render_template(
38
38
  "activity.html.j2", **activity_controller.render_activity(int(id))
39
39
  )
40
40
 
41
41
  @app.route("/activity/<id>/track.json")
42
- def activity_track(id: int):
42
+ def activity_track(id: str):
43
43
  plot = activity_track_plot(repository.get_time_series(int(id)))
44
44
  return plot
45
45
 
46
- @app.route("/explorer")
47
- def explorer():
48
- return render_template("explorer.html.j2", **explorer_controller.render())
46
+ @app.route("/explorer/<zoom>")
47
+ def explorer(zoom: str):
48
+ return render_template(
49
+ "explorer.html.j2", **explorer_controller.render(int(zoom))
50
+ )
49
51
 
50
52
  @app.route("/summary-statistics")
51
53
  def summary_statistics():
@@ -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
@@ -15,20 +16,28 @@ class ExplorerController:
15
16
  self._repository = repository
16
17
 
17
18
  @functools.cache
18
- def render(self) -> dict:
19
- tiles = get_tile_history(self._repository)
19
+ def render(self, zoom: int) -> dict:
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)
26
+ explored = get_three_color_tiles(tiles, self._repository, zoom)
22
27
 
23
- points = get_border_tiles(tiles)
28
+ points = get_border_tiles(tiles, zoom)
24
29
  missing_tiles_geojson = make_grid_file_geojson(points, "missing_tiles")
25
30
  make_grid_file_gpx(points, "missing_tiles")
26
31
 
27
- points = get_explored_tiles(tiles)
32
+ points = get_explored_tiles(tiles, zoom)
28
33
  explored_tiles_geojson = make_grid_file_geojson(points, "explored")
29
34
  make_grid_file_gpx(points, "explored")
30
35
 
31
36
  return {
32
- "explored_geojson": explored_geojson,
37
+ "center": {
38
+ "latitude": median_lat,
39
+ "longitude": median_lon,
40
+ },
41
+ "explored": explored,
33
42
  "missing_tiles_geojson": missing_tiles_geojson,
34
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,
@@ -62,7 +62,10 @@
62
62
  <a class="nav-link active" aria-current="page" href="/calendar">Calendar</a>
63
63
  </li>
64
64
  <li class="nav-item">
65
- <a class="nav-link active" aria-current="page" href="/explorer">Explorer</a>
65
+ <a class="nav-link active" aria-current="page" href="/explorer/14">Explorer</a>
66
+ </li>
67
+ <li class="nav-item">
68
+ <a class="nav-link active" aria-current="page" href="/explorer/17">Squadratinhos</a>
66
69
  </li>
67
70
  <li class="nav-item">
68
71
  <a class="nav-link active" aria-current="page" href="/heatmap">Heatmap</a>
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.6.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,29 +1,30 @@
1
1
  geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- geo_activity_playground/__main__.py,sha256=0xUcQkoRcPhLJX3uluIFs1GtRRdaqqz6GVAR8UNDmmI,3151
2
+ geo_activity_playground/__main__.py,sha256=bGDI5TTWJakCBqlet-kqdDOmBgh9JbmV7v99g6GgT3A,2745
3
3
  geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- geo_activity_playground/core/activities.py,sha256=LnT9vZgt7lkim6T9pVHxtF8MjFIivmSqCqiR5YMuuBU,4237
4
+ geo_activity_playground/core/activities.py,sha256=6f-pnIJ58ZMrcwXJ19LJUafmip9vUVPlGyBeVUVnIxw,4484
5
5
  geo_activity_playground/core/activity_parsers.py,sha256=CaQB4jXm-4PjrXR7AcLkgZt82zGl4oCyLYlY0Zfx4H8,2135
6
+ geo_activity_playground/core/config.py,sha256=GNHEIeFI3dNRiFSMburn5URZHx0qkiitvePAx2toYUQ,456
6
7
  geo_activity_playground/core/coordinates.py,sha256=SxU2xDPZD-KxL2VBM94_wUFKkcG27-wTLkuu22ByVps,573
7
8
  geo_activity_playground/core/heatmap.py,sha256=BPhRNcieHgc5gSZdKRtYcv8Ljfwgi-teLr6VVBVlTkg,1289
8
9
  geo_activity_playground/core/plots.py,sha256=7FX1QRT_haBq9lQsehaZQ_rXLcJNHO2Fx_bWuBXwPnw,2420
9
10
  geo_activity_playground/core/sources.py,sha256=_eZE_JHlOP0oMRkpXMjfGTBPSkWEfMzPovosc5QrSCE,188
10
11
  geo_activity_playground/core/tasks.py,sha256=vV8fOmBeIx0t_qXW5qvQ2Ut0JiAw6XZ-y_y9ZUkZJAA,301
11
12
  geo_activity_playground/core/test_tiles.py,sha256=ATIcCqwAQMpkPAZD9VO__uinpuuOyz4z2fAy2vpoMH8,410
12
- geo_activity_playground/core/tiles.py,sha256=0tmnDZmTz5vJyf4jqIYneTBpHOD50RdS2FREb0ckwXc,2404
13
+ geo_activity_playground/core/tiles.py,sha256=aTC3roH8JMaEAMC_hUAwOz7_Yvvw7n4Vc-9y8pN05Pk,2399
13
14
  geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- geo_activity_playground/explorer/converters.py,sha256=wF2eYuo2IQ-F2kmJc25KzRtO9QE6z6dyMxvDnqvcM3U,2910
15
- geo_activity_playground/explorer/grid_file.py,sha256=ae2ZvqpMHbmKIXg8U_jQcQEe66yBT8evlf0f5JXVas0,5166
15
+ geo_activity_playground/explorer/converters.py,sha256=UVU5qhnvAelY6rmqCnkqfDp1zYI_1LUatdJC0d16BWE,3051
16
+ geo_activity_playground/explorer/grid_file.py,sha256=lER2TAJjsG8BMjbdoXiwW9GXZLM8bH-9EeF7z7yckgg,6828
16
17
  geo_activity_playground/explorer/video.py,sha256=RGZik93ghfZuRILXf8pfUbPh5VV37_QRAR4FgOMfZqQ,4354
17
- geo_activity_playground/heatmap.py,sha256=EO-8eOVZ8eHJe0aqHWqKymhjvnBGyPUhwFzdXtV8vBU,8548
18
+ geo_activity_playground/heatmap.py,sha256=jg0yuFvYoSa8K0uGL6MCMhWFclXWO3DVyrsdSJcH380,8552
18
19
  geo_activity_playground/importers/directory.py,sha256=Hk9YYJA2ssWaOyc6WFxKNwTo5DWZxXCW661spCyI7kQ,2622
19
- geo_activity_playground/importers/strava_api.py,sha256=vacbspe0vR8e3duav1KOBB-ODebSOV_hF5RWMzNojOo,5865
20
- geo_activity_playground/webui/activity_controller.py,sha256=JMLLsh26SdwBkvOUEvKK-w05ZulPXlFOfxRRFdEU4w4,2848
21
- geo_activity_playground/webui/app.py,sha256=DZzd10EONXHN0nAmsLFJ0BGcKw8RNQLuOV-q2Z28bDg,3485
20
+ geo_activity_playground/importers/strava_api.py,sha256=3utIYnmw0nqY5oGBf-3VJPzGI1hTplYUfeWXoSuJ7gE,5749
21
+ geo_activity_playground/webui/activity_controller.py,sha256=sOYUYJGibYx6eGOxE17umSWubpybB8Xgcm07wB0v4LA,2941
22
+ geo_activity_playground/webui/app.py,sha256=9-TgDI1LojplwPxeorMqtObGWIw1sB5qepqWVHUl3s4,3532
22
23
  geo_activity_playground/webui/calendar_controller.py,sha256=maQ1RlrD99pncOts3ue5ye4OHr6WB-E40eAzs8ZxwPI,2239
23
24
  geo_activity_playground/webui/eddington_controller.py,sha256=b5mYkciv7Wkd5zord-WsdrV_8c-qpVi-8DG3jIUEKhs,2616
24
25
  geo_activity_playground/webui/entry_controller.py,sha256=TixdwCDMKhHUVrbc6sAIjpvtmouHXtbQN0N641TB__s,1851
25
26
  geo_activity_playground/webui/equipment_controller.py,sha256=2asTMDEYdMz8a_l5fXL1ULXoUaDHywoDl9mZnfZSn5Q,2011
26
- geo_activity_playground/webui/explorer_controller.py,sha256=UBt6aGsexZwz-1gw3UOAWAv-ek-bdI5nbweZTszvTZ0,1400
27
+ geo_activity_playground/webui/explorer_controller.py,sha256=8OD9h7-UzlSU53tpA7LD0VsHbvRTka9U4CFjiuNQzGU,1766
27
28
  geo_activity_playground/webui/heatmap_controller.py,sha256=twfp9IQwUz9qQCjO0zMwhWjQYmHRpHM-A8BPnCj3ErY,3636
28
29
  geo_activity_playground/webui/static/android-chrome-192x192.png,sha256=30rNfBHxdLYC0Wx4cDkPZY-V17ZQZIc4PPLQBdz_w1U,20069
29
30
  geo_activity_playground/webui/static/android-chrome-384x384.png,sha256=bgeqAdyvDZBMch7rVi3qSawf0Zr4Go0EG8Ws_B8NApY,49297
@@ -40,12 +41,12 @@ geo_activity_playground/webui/templates/calendar-month.html.j2,sha256=LVokl95lPl
40
41
  geo_activity_playground/webui/templates/calendar.html.j2,sha256=b7a_YWhqyN2GYU7g4wIckU3UURTzNuL5fGe5SibVKM8,1099
41
42
  geo_activity_playground/webui/templates/eddington.html.j2,sha256=yl75IzWeIkFpwPj8FjTrzJsz_f-qdETPmNnAGLPJuL8,487
42
43
  geo_activity_playground/webui/templates/equipment.html.j2,sha256=BwZzbZ2AuFuiM_Fxu2KOqvhcgHd9yr1xL76ihb_6YKc,1317
43
- geo_activity_playground/webui/templates/explorer.html.j2,sha256=RR6Odp8dWHmOUzrXfFjVV3_BKvWnXDeOk4URpKwmSCk,2747
44
- geo_activity_playground/webui/templates/heatmap.html.j2,sha256=H4Jd1eSjDr9lu-ni9I1ErFoXQLbZiyP5NnKYpmHLizY,722
44
+ geo_activity_playground/webui/templates/explorer.html.j2,sha256=fVxPxicgDRvgOu1138E6bJs81CFvf1VkHuzGYAWW1ok,3399
45
+ geo_activity_playground/webui/templates/heatmap.html.j2,sha256=M56IKATu3TdlwXUgTK9w-vfoAubBv9oJrE3ot-wEf84,726
45
46
  geo_activity_playground/webui/templates/index.html.j2,sha256=6b0cdqiGnqrC_hjEg-z6-IZFqXoZa7V0JurQ4Xd6YJw,1968
46
- geo_activity_playground/webui/templates/page.html.j2,sha256=Mg-T7npU7PFcBDpJLAkHprK52QDZtQ0xkvFl1HEkx5Q,4912
47
+ geo_activity_playground/webui/templates/page.html.j2,sha256=CWaDxYlXzJCWCuIsMma2-PCLrsEr38cSan01CGG00qU,5104
47
48
  geo_activity_playground/webui/templates/summary-statistics.html.j2,sha256=WylEkNplyXIt2bqYdZg93xupeP9wmaq5AWnpYaPkr-8,453
48
- geo_activity_playground-0.6.0.dist-info/METADATA,sha256=nk86mMHCs4vqSUD_m22QiqQSYP4_JMVHqSpkyPGhim0,1312
49
- geo_activity_playground-0.6.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
50
- geo_activity_playground-0.6.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
51
- geo_activity_playground-0.6.0.dist-info/RECORD,,
49
+ geo_activity_playground-0.8.0.dist-info/METADATA,sha256=lAYsRY6bF3_FhEX9MODLVrJvQH-SuP25fEppUVkqPH0,1425
50
+ geo_activity_playground-0.8.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
51
+ geo_activity_playground-0.8.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
52
+ geo_activity_playground-0.8.0.dist-info/RECORD,,