geo-activity-playground 0.13.0__tar.gz → 0.14.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 (60) hide show
  1. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/PKG-INFO +2 -1
  2. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/__main__.py +23 -2
  3. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/activities.py +53 -0
  4. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/activity_parsers.py +78 -44
  5. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/heatmap.py +0 -42
  6. geo_activity_playground-0.14.1/geo_activity_playground/core/tasks.py +49 -0
  7. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/tiles.py +10 -1
  8. geo_activity_playground-0.14.1/geo_activity_playground/explorer/grid_file.py +102 -0
  9. geo_activity_playground-0.14.1/geo_activity_playground/explorer/tile_visits.py +266 -0
  10. geo_activity_playground-0.14.1/geo_activity_playground/importers/directory.py +109 -0
  11. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/importers/strava_api.py +51 -51
  12. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/app.py +25 -22
  13. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/entry_controller.py +22 -20
  14. geo_activity_playground-0.14.1/geo_activity_playground/webui/explorer_controller.py +286 -0
  15. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/heatmap_controller.py +19 -12
  16. geo_activity_playground-0.14.1/geo_activity_playground/webui/summary_controller.py +58 -0
  17. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/explorer.html.j2 +5 -3
  18. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/summary-statistics.html.j2 +2 -2
  19. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/pyproject.toml +5 -4
  20. geo_activity_playground-0.13.0/geo_activity_playground/core/plots.py +0 -81
  21. geo_activity_playground-0.13.0/geo_activity_playground/core/sources.py +0 -10
  22. geo_activity_playground-0.13.0/geo_activity_playground/core/tasks.py +0 -17
  23. geo_activity_playground-0.13.0/geo_activity_playground/explorer/clusters.py +0 -272
  24. geo_activity_playground-0.13.0/geo_activity_playground/explorer/converters.py +0 -135
  25. geo_activity_playground-0.13.0/geo_activity_playground/explorer/grid_file.py +0 -234
  26. geo_activity_playground-0.13.0/geo_activity_playground/importers/directory.py +0 -107
  27. geo_activity_playground-0.13.0/geo_activity_playground/webui/explorer_controller.py +0 -121
  28. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/LICENSE +0 -0
  29. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/__init__.py +0 -0
  30. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/__init__.py +0 -0
  31. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/cache_migrations.py +0 -0
  32. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/config.py +0 -0
  33. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/coordinates.py +0 -0
  34. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/test_tiles.py +0 -0
  35. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/explorer/__init__.py +0 -0
  36. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/explorer/video.py +0 -0
  37. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/heatmap.py +0 -0
  38. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/activity_controller.py +0 -0
  39. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/calendar_controller.py +0 -0
  40. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/eddington_controller.py +0 -0
  41. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/equipment_controller.py +0 -0
  42. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/grayscale_tile_controller.py +0 -0
  43. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  44. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  45. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  46. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  47. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  48. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  49. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
  50. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  51. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  52. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  53. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/activity.html.j2 +0 -0
  54. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/calendar-month.html.j2 +0 -0
  55. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
  56. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
  57. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
  58. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/heatmap.html.j2 +0 -0
  59. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
  60. {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.13.0
3
+ Version: 0.14.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -35,3 +35,4 @@ Requires-Dist: tqdm (>=4.64.0,<5.0.0)
35
35
  Requires-Dist: vegafusion (>=1.4.3,<2.0.0)
36
36
  Requires-Dist: vegafusion-python-embed (>=1.4.3,<2.0.0)
37
37
  Requires-Dist: vl-convert-python (>=1.0.1,<2.0.0)
38
+ Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
@@ -1,12 +1,16 @@
1
1
  import argparse
2
+ import logging
2
3
  import os
3
4
  import pathlib
4
5
 
5
6
  import coloredlogs
6
7
 
7
8
  from geo_activity_playground.core.activities import ActivityRepository
9
+ from geo_activity_playground.core.activities import embellish_time_series
8
10
  from geo_activity_playground.core.cache_migrations import apply_cache_migrations
9
11
  from geo_activity_playground.core.config import get_config
12
+ from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
13
+ from geo_activity_playground.explorer.tile_visits import compute_tile_visits
10
14
  from geo_activity_playground.explorer.video import explorer_video_main
11
15
  from geo_activity_playground.heatmap import generate_heatmaps_per_cluster
12
16
  from geo_activity_playground.importers.directory import import_from_directory
@@ -56,7 +60,17 @@ def main() -> None:
56
60
 
57
61
  subparser = subparsers.add_parser("serve", help="Launch webserver")
58
62
  subparser.set_defaults(
59
- func=lambda options: webui_main(make_activity_repository(options.basedir))
63
+ func=lambda options: webui_main(
64
+ make_activity_repository(options.basedir),
65
+ host=options.host,
66
+ port=options.port,
67
+ )
68
+ )
69
+ subparser.add_argument(
70
+ "--host", default="127.0.0.1", help="IP address to listen on"
71
+ )
72
+ subparser.add_argument(
73
+ "--port", default=5000, type=int, help="the port to run listen on"
60
74
  )
61
75
 
62
76
  subparser = subparsers.add_parser("cache", help="Cache stuff")
@@ -69,6 +83,9 @@ def main() -> None:
69
83
  fmt="%(asctime)s %(name)s %(levelname)s %(message)s",
70
84
  level=options.loglevel.upper(),
71
85
  )
86
+
87
+ logging.getLogger("stravalib.protocol.ApiV3").setLevel(logging.WARNING)
88
+
72
89
  options.func(options)
73
90
 
74
91
 
@@ -81,4 +98,8 @@ def make_activity_repository(basedir: pathlib.Path) -> ActivityRepository:
81
98
  elif config:
82
99
  if "strava" in config:
83
100
  import_from_strava_api()
84
- return ActivityRepository()
101
+ repository = ActivityRepository()
102
+ embellish_time_series(repository)
103
+ compute_tile_visits(repository)
104
+ compute_tile_evolution()
105
+ return repository
@@ -10,8 +10,10 @@ import geojson
10
10
  import matplotlib
11
11
  import numpy as np
12
12
  import pandas as pd
13
+ from tqdm import tqdm
13
14
 
14
15
  from geo_activity_playground.core.config import get_config
16
+ from geo_activity_playground.core.tasks import WorkTracker
15
17
  from geo_activity_playground.core.tiles import compute_tile_float
16
18
 
17
19
 
@@ -43,6 +45,10 @@ class ActivityRepository:
43
45
  self.meta["kind"].fillna("Unknown", inplace=True)
44
46
  self.meta["equipment"].fillna("Unknown", inplace=True)
45
47
 
48
+ @property
49
+ def activity_ids(self) -> set[int]:
50
+ return set(self.meta["id"])
51
+
46
52
  def iter_activities(self, new_to_old=True) -> Iterator[ActivityMeta]:
47
53
  direction = -1 if new_to_old else 1
48
54
  for id, row in self.meta[::direction].iterrows():
@@ -103,6 +109,53 @@ class ActivityRepository:
103
109
  return df
104
110
 
105
111
 
112
+ def embellish_time_series(repository: ActivityRepository) -> None:
113
+ work_tracker = WorkTracker("embellish-time-series")
114
+ activities_to_process = work_tracker.filter(repository.activity_ids)
115
+ for activity_id in tqdm(activities_to_process, desc="Embellish time series data"):
116
+ path = pathlib.Path(f"Cache/Activity Timeseries/{activity_id}.parquet")
117
+ df = pd.read_parquet(path)
118
+ df.name = id
119
+ changed = False
120
+ if pd.api.types.is_dtype_equal(df["time"].dtype, "int64"):
121
+ start = repository.get_activity_by_id(activity_id).start
122
+ time = df["time"]
123
+ del df["time"]
124
+ df["time"] = [start + datetime.timedelta(seconds=t) for t in time]
125
+ changed = True
126
+ assert pd.api.types.is_dtype_equal(df["time"].dtype, "datetime64[ns, UTC]")
127
+
128
+ if "distance" in df.columns:
129
+ if "distance/km" not in df.columns:
130
+ df["distance/km"] = df["distance"] / 1000
131
+ changed = True
132
+
133
+ if "speed" not in df.columns:
134
+ df["speed"] = (
135
+ df["distance"].diff()
136
+ / (df["time"].diff().dt.total_seconds() + 1e-3)
137
+ * 3.6
138
+ )
139
+ changed = True
140
+
141
+ if "x" not in df.columns:
142
+ x, y = compute_tile_float(df["latitude"], df["longitude"], 0)
143
+ df["x"] = x
144
+ df["y"] = y
145
+ changed = True
146
+
147
+ if "segment_id" not in df.columns:
148
+ time_diff = (df["time"] - df["time"].shift(1)).dt.total_seconds()
149
+ jump_indices = time_diff >= 30
150
+ df["segment_id"] = np.cumsum(jump_indices)
151
+ changed = True
152
+
153
+ if changed:
154
+ df.to_parquet(path)
155
+ work_tracker.mark_done(activity_id)
156
+ work_tracker.close()
157
+
158
+
106
159
  def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
107
160
  line = geojson.LineString(
108
161
  [
@@ -9,12 +9,59 @@ import fitdecode
9
9
  import gpxpy
10
10
  import pandas as pd
11
11
  import tcxreader.tcxreader
12
+ import xmltodict
12
13
 
13
14
 
14
15
  class ActivityParseError(BaseException):
15
16
  pass
16
17
 
17
18
 
19
+ def read_activity(path: pathlib.Path) -> pd.DataFrame:
20
+ suffixes = path.suffixes
21
+ if suffixes[-1] == ".gz":
22
+ opener = gzip.open
23
+ file_type = suffixes[-2]
24
+ else:
25
+ opener = open
26
+ file_type = suffixes[-1]
27
+
28
+ if file_type == ".gpx":
29
+ try:
30
+ df = read_gpx_activity(path, opener)
31
+ except gpxpy.gpx.GPXXMLSyntaxException as e:
32
+ raise ActivityParseError("Syntax error while parsing GPX file") from e
33
+ elif file_type == ".fit":
34
+ df = read_fit_activity(path, opener)
35
+ elif file_type == ".tcx":
36
+ try:
37
+ df = read_tcx_activity(path, opener)
38
+ except xml.etree.ElementTree.ParseError as e:
39
+ raise ActivityParseError("Syntax error in TCX file") from e
40
+ elif file_type in [".kml", ".kmz"]:
41
+ df = read_kml_activity(path, opener)
42
+ else:
43
+ raise ActivityParseError(f"Unsupported file format: {file_type}")
44
+
45
+ if len(df):
46
+ try:
47
+ if df.time.dt.tz is not None:
48
+ df.time = df.time.dt.tz_localize(None)
49
+ except AttributeError as e:
50
+ print(df)
51
+ print(df.dtypes)
52
+ types = {}
53
+ for elem in df["time"]:
54
+ t = str(type(elem))
55
+ if t not in types:
56
+ types[t] = elem
57
+ print(types)
58
+ raise ActivityParseError(
59
+ "It looks like the date parsing has gone wrong."
60
+ ) from e
61
+ df.name = path.stem.split(".")[0]
62
+ return df
63
+
64
+
18
65
  def read_fit_activity(path: pathlib.Path, open) -> pd.DataFrame:
19
66
  """
20
67
  {'timestamp': datetime.datetime(2023, 11, 11, 16, 29, 49, tzinfo=datetime.timezone.utc),
@@ -45,8 +92,11 @@ def read_fit_activity(path: pathlib.Path, open) -> pd.DataFrame:
45
92
  and fields.get("position_lat", None)
46
93
  and fields.get("position_long", None)
47
94
  ):
95
+ time = fields["timestamp"]
96
+ assert isinstance(time, datetime.datetime)
97
+ time = time.astimezone(datetime.timezone.utc)
48
98
  row = {
49
- "time": fields["timestamp"],
99
+ "time": time,
50
100
  "latitude": fields["position_lat"] / ((2**32) / 360),
51
101
  "longitude": fields["position_long"] / ((2**32) / 360),
52
102
  }
@@ -82,6 +132,8 @@ def read_gpx_activity(path: pathlib.Path, open) -> pd.DataFrame:
82
132
  time = point.time
83
133
  else:
84
134
  time = dateutil.parser.parse(str(point.time))
135
+ assert isinstance(time, datetime.datetime)
136
+ time = time.astimezone(datetime.timezone.utc)
85
137
  points.append((time, point.latitude, point.longitude))
86
138
 
87
139
  return pd.DataFrame(points, columns=["time", "latitude", "longitude"])
@@ -110,8 +162,11 @@ def read_tcx_activity(path: pathlib.Path, open) -> pd.DataFrame:
110
162
 
111
163
  for trackpoint in data.trackpoints:
112
164
  if trackpoint.latitude and trackpoint.longitude:
165
+ time = trackpoint.time
166
+ assert isinstance(time, datetime.datetime)
167
+ time = time.astimezone(datetime.timezone.utc)
113
168
  row = {
114
- "time": trackpoint.time,
169
+ "time": time,
115
170
  "latitude": trackpoint.latitude,
116
171
  "longitude": trackpoint.longitude,
117
172
  }
@@ -128,45 +183,24 @@ def read_tcx_activity(path: pathlib.Path, open) -> pd.DataFrame:
128
183
  return df
129
184
 
130
185
 
131
- def read_activity(path: pathlib.Path) -> pd.DataFrame:
132
- suffixes = path.suffixes
133
- if suffixes[-1] == ".gz":
134
- opener = gzip.open
135
- file_type = suffixes[-2]
136
- else:
137
- opener = open
138
- file_type = suffixes[-1]
139
-
140
- if file_type == ".gpx":
141
- try:
142
- df = read_gpx_activity(path, opener)
143
- except gpxpy.gpx.GPXXMLSyntaxException as e:
144
- raise ActivityParseError("Syntax error while parsing GPX file") from e
145
- elif file_type == ".fit":
146
- df = read_fit_activity(path, opener)
147
- elif file_type == ".tcx":
148
- try:
149
- df = read_tcx_activity(path, opener)
150
- except xml.etree.ElementTree.ParseError as e:
151
- raise ActivityParseError("Syntax error in TCX file") from e
152
- else:
153
- raise ActivityParseError(f"Unsupported file format: {file_type}")
154
-
155
- if len(df):
156
- try:
157
- if df.time.dt.tz is not None:
158
- df.time = df.time.dt.tz_localize(None)
159
- except AttributeError as e:
160
- print(df)
161
- print(df.dtypes)
162
- types = {}
163
- for elem in df["time"]:
164
- t = str(type(elem))
165
- if t not in types:
166
- types[t] = elem
167
- print(types)
168
- raise ActivityParseError(
169
- "It looks like the date parsing has gone wrong."
170
- ) from e
171
- df.name = path.stem.split(".")[0]
172
- return df
186
+ def read_kml_activity(path: pathlib.Path, opener) -> pd.DataFrame:
187
+ with opener(path, "rb") as f:
188
+ kml_dict = xmltodict.parse(f)
189
+ doc = kml_dict["kml"]["Document"]
190
+ keypoint_folder = doc["Folder"]
191
+ placemark = keypoint_folder["Placemark"]
192
+ track = placemark["gx:Track"]
193
+ rows = []
194
+ for when, where in zip(track["when"], track["gx:coord"]):
195
+ time = dateutil.parser.parse(when).astimezone(datetime.timezone.utc)
196
+ parts = where.split(" ")
197
+ if len(parts) == 2:
198
+ lon, lat = parts
199
+ alt = None
200
+ if len(parts) == 3:
201
+ lon, lat, alt = parts
202
+ row = {"time": time, "latitude": float(lat), "longitude": float(lon)}
203
+ if alt is not None:
204
+ row["altitude"] = float(alt)
205
+ rows.append(row)
206
+ return pd.DataFrame(rows)
@@ -5,7 +5,6 @@ import dataclasses
5
5
  import functools
6
6
  import logging
7
7
  import pathlib
8
- import pickle
9
8
 
10
9
  import matplotlib.pyplot as pl
11
10
  import numpy as np
@@ -16,46 +15,11 @@ from geo_activity_playground.core.tasks import work_tracker
16
15
  from geo_activity_playground.core.tiles import compute_tile_float
17
16
  from geo_activity_playground.core.tiles import get_tile
18
17
  from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
19
- from geo_activity_playground.explorer.converters import get_first_tiles
20
18
 
21
19
 
22
20
  logger = logging.getLogger(__name__)
23
21
 
24
22
 
25
- @functools.cache
26
- def compute_activities_per_tile(
27
- repository: ActivityRepository,
28
- ) -> dict[int, dict[tuple[int, int], set[int]]]:
29
- logger.info("Extracting activities per tile …")
30
- cache_path = pathlib.Path("Cache/activities-per-tile.pickle")
31
- if cache_path.exists():
32
- with open(cache_path, "rb") as f:
33
- data = pickle.load(f)
34
- else:
35
- data: dict[int, dict[tuple[int, int], set[int]]] = {}
36
- with work_tracker(pathlib.Path("Cache/activities-per-tile-task.json")) as tracker:
37
- for activity in repository.iter_activities():
38
- if activity.id in tracker:
39
- continue
40
- tracker.add(activity.id)
41
-
42
- logger.info(f"Add activity {activity.id} to all zoom levels …")
43
- for zoom in range(1, 20):
44
- if zoom not in data:
45
- data[zoom] = {}
46
- tiles_this_activity = get_first_tiles(activity.id, repository, zoom)
47
- for _, row in tiles_this_activity.iterrows():
48
- tile = (row["tile_x"], row["tile_y"])
49
- if tile not in data[zoom]:
50
- data[zoom][tile] = set()
51
- data[zoom][tile].add(activity.id)
52
-
53
- with open(cache_path, "wb") as f:
54
- pickle.dump(data, f)
55
-
56
- return data
57
-
58
-
59
23
  @functools.cache
60
24
  def get_all_points(repository: ActivityRepository) -> pd.DataFrame:
61
25
  logger.info("Gathering all points …")
@@ -77,12 +41,6 @@ def get_all_points(repository: ActivityRepository) -> pd.DataFrame:
77
41
  continue
78
42
  shard = time_series[["latitude", "longitude"]].copy()
79
43
  shard["activity_id"] = activity.id
80
- x, y = compute_tile_float(
81
- shard["latitude"],
82
- shard["longitude"],
83
- )
84
- shard["x"] = x
85
- shard["y"] = y
86
44
  new_shards.append(shard)
87
45
  logger.info("Concatenating shards …")
88
46
  all_points = pd.concat([all_points] + new_shards)
@@ -0,0 +1,49 @@
1
+ import contextlib
2
+ import json
3
+ import pathlib
4
+ import pickle
5
+ from typing import Any
6
+
7
+
8
+ @contextlib.contextmanager
9
+ def work_tracker(path: pathlib.Path):
10
+ if path.exists():
11
+ with open(path) as f:
12
+ s = set(json.load(f))
13
+ else:
14
+ s = set()
15
+
16
+ yield s
17
+
18
+ with open(path, "w") as f:
19
+ json.dump(list(s), f)
20
+
21
+
22
+ class WorkTracker:
23
+ def __init__(self, name: str) -> None:
24
+ self._path = pathlib.Path(f"Cache/work-tracker-{name}.pickle")
25
+
26
+ if self._path.exists():
27
+ with open(self._path, "rb") as f:
28
+ self._done = pickle.load(f)
29
+ else:
30
+ self._done = set()
31
+
32
+ def filter(self, ids: list[int]) -> set[int]:
33
+ return set(ids) - self._done
34
+
35
+ def mark_done(self, id: int) -> None:
36
+ self._done.add(id)
37
+
38
+ def close(self) -> None:
39
+ with open(self._path, "wb") as f:
40
+ pickle.dump(self._done, f)
41
+
42
+
43
+ def try_load_pickle(path: pathlib.Path) -> Any:
44
+ if path.exists():
45
+ try:
46
+ with open(path, "rb") as f:
47
+ return pickle.load(f)
48
+ except ModuleNotFoundError:
49
+ pass
@@ -3,6 +3,7 @@ import logging
3
3
  import math
4
4
  import pathlib
5
5
  import time
6
+ from typing import Iterator
6
7
  from typing import Optional
7
8
 
8
9
  import numpy as np
@@ -21,7 +22,7 @@ def compute_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
21
22
  return int(x * n), int(y * n)
22
23
 
23
24
 
24
- def compute_tile_float(lat: float, lon: float, zoom: int) -> tuple[int, int]:
25
+ def compute_tile_float(lat: float, lon: float, zoom: int) -> tuple[float, float]:
25
26
  x = np.radians(lon)
26
27
  y = np.arcsinh(np.tan(np.radians(lat)))
27
28
  x = (1 + x / np.pi) / 2
@@ -98,3 +99,11 @@ def interpolate_missing_tile(
98
99
  return (int(x2), y_hat)
99
100
  else:
100
101
  return (int(x1), y_hat)
102
+
103
+
104
+ def adjacent_to(tile: tuple[int, int]) -> Iterator[tuple[int, int]]:
105
+ x, y = tile
106
+ yield (x + 1, y)
107
+ yield (x - 1, y)
108
+ yield (x, y + 1)
109
+ yield (x, y - 1)
@@ -0,0 +1,102 @@
1
+ import logging
2
+ from typing import Iterator
3
+ from typing import Optional
4
+
5
+ import geojson
6
+ import gpxpy
7
+ import pandas as pd
8
+
9
+ from geo_activity_playground.core.coordinates import Bounds
10
+ from geo_activity_playground.core.tiles import adjacent_to
11
+ from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_border_tiles(
18
+ tiles: pd.DataFrame, zoom: int, tile_bounds: Bounds
19
+ ) -> list[list[list[float]]]:
20
+ logger.info("Generate border tiles …")
21
+ tile_set = set(zip(tiles["tile_x"], tiles["tile_y"]))
22
+ border_tiles = set()
23
+ for tile in tile_set:
24
+ for neighbor in adjacent_to(tile):
25
+ if neighbor not in tile_set:
26
+ for neighbor2 in adjacent_to(neighbor):
27
+ if neighbor2 not in tile_set and tile_bounds.contains(*neighbor):
28
+ border_tiles.add(neighbor2)
29
+ return make_grid_points(border_tiles, zoom)
30
+
31
+
32
+ def get_explored_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]]:
33
+ return make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]), zoom)
34
+
35
+
36
+ def make_explorer_tile(
37
+ tile_x: int, tile_y: int, properties: dict, zoom: int
38
+ ) -> geojson.Feature:
39
+ return make_explorer_rectangle(
40
+ tile_x, tile_y, tile_x + 1, tile_y + 1, zoom, properties
41
+ )
42
+
43
+
44
+ def make_explorer_rectangle(
45
+ x1: int, y1: int, x2: int, y2: int, zoom: int, properties: Optional[dict] = None
46
+ ) -> geojson.Feature:
47
+ corners = [
48
+ get_tile_upper_left_lat_lon(*args)
49
+ for args in [
50
+ (x1, y1, zoom),
51
+ (x2, y1, zoom),
52
+ (x2, y2, zoom),
53
+ (x1, y2, zoom),
54
+ (x1, y1, zoom),
55
+ ]
56
+ ]
57
+ return geojson.Feature(
58
+ geometry=geojson.Polygon([[(coord[1], coord[0]) for coord in corners]]),
59
+ properties=properties,
60
+ )
61
+
62
+
63
+ def make_grid_points(
64
+ tile_iterator: Iterator[tuple[int, int]], zoom: int
65
+ ) -> list[list[list[float]]]:
66
+ result = []
67
+ for tile_x, tile_y in tile_iterator:
68
+ tile = [
69
+ get_tile_upper_left_lat_lon(tile_x, tile_y, zoom),
70
+ get_tile_upper_left_lat_lon(tile_x + 1, tile_y, zoom),
71
+ get_tile_upper_left_lat_lon(tile_x + 1, tile_y + 1, zoom),
72
+ get_tile_upper_left_lat_lon(tile_x, tile_y + 1, zoom),
73
+ get_tile_upper_left_lat_lon(tile_x, tile_y, zoom),
74
+ ]
75
+ result.append(tile)
76
+ return result
77
+
78
+
79
+ def make_grid_file_gpx(grid_points: list[list[list[float]]]) -> str:
80
+ gpx = gpxpy.gpx.GPX()
81
+ gpx_track = gpxpy.gpx.GPXTrack()
82
+ gpx.tracks.append(gpx_track)
83
+
84
+ for points in grid_points:
85
+ gpx_segment = gpxpy.gpx.GPXTrackSegment()
86
+ gpx_track.segments.append(gpx_segment)
87
+ for point in points:
88
+ gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(*point))
89
+ return gpx.to_xml()
90
+
91
+
92
+ def make_grid_file_geojson(grid_points: list[list[list[float]]]) -> str:
93
+ fc = geojson.FeatureCollection(
94
+ [
95
+ geojson.Feature(
96
+ geometry=geojson.Polygon([[[lon, lat] for lat, lon in points]])
97
+ )
98
+ for points in grid_points
99
+ ]
100
+ )
101
+ result = geojson.dumps(fc, sort_keys=True, indent=4, ensure_ascii=False)
102
+ return result