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.
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/PKG-INFO +2 -1
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/__main__.py +23 -2
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/activities.py +53 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/activity_parsers.py +78 -44
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/heatmap.py +0 -42
- geo_activity_playground-0.14.1/geo_activity_playground/core/tasks.py +49 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/tiles.py +10 -1
- geo_activity_playground-0.14.1/geo_activity_playground/explorer/grid_file.py +102 -0
- geo_activity_playground-0.14.1/geo_activity_playground/explorer/tile_visits.py +266 -0
- geo_activity_playground-0.14.1/geo_activity_playground/importers/directory.py +109 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/importers/strava_api.py +51 -51
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/app.py +25 -22
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/entry_controller.py +22 -20
- geo_activity_playground-0.14.1/geo_activity_playground/webui/explorer_controller.py +286 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/heatmap_controller.py +19 -12
- geo_activity_playground-0.14.1/geo_activity_playground/webui/summary_controller.py +58 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/explorer.html.j2 +5 -3
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/summary-statistics.html.j2 +2 -2
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/pyproject.toml +5 -4
- geo_activity_playground-0.13.0/geo_activity_playground/core/plots.py +0 -81
- geo_activity_playground-0.13.0/geo_activity_playground/core/sources.py +0 -10
- geo_activity_playground-0.13.0/geo_activity_playground/core/tasks.py +0 -17
- geo_activity_playground-0.13.0/geo_activity_playground/explorer/clusters.py +0 -272
- geo_activity_playground-0.13.0/geo_activity_playground/explorer/converters.py +0 -135
- geo_activity_playground-0.13.0/geo_activity_playground/explorer/grid_file.py +0 -234
- geo_activity_playground-0.13.0/geo_activity_playground/importers/directory.py +0 -107
- geo_activity_playground-0.13.0/geo_activity_playground/webui/explorer_controller.py +0 -121
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/LICENSE +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/cache_migrations.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/config.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/heatmap.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/activity_controller.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/calendar_controller.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/eddington_controller.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/equipment_controller.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/grayscale_tile_controller.py +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/favicon.ico +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/static/site.webmanifest +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/activity.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/calendar-month.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/heatmap.html.j2 +0 -0
- {geo_activity_playground-0.13.0 → geo_activity_playground-0.14.1}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
- {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.
|
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(
|
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
|
-
|
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":
|
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":
|
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
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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[
|
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
|