geo-activity-playground 0.21.2__py3-none-any.whl → 0.23.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.
- geo_activity_playground/__main__.py +7 -27
- geo_activity_playground/core/activities.py +9 -3
- geo_activity_playground/core/activity_parsers.py +14 -5
- geo_activity_playground/core/heatmap.py +2 -2
- geo_activity_playground/explorer/grid_file.py +7 -5
- geo_activity_playground/explorer/tile_visits.py +51 -31
- geo_activity_playground/explorer/video.py +3 -2
- geo_activity_playground/importers/directory.py +4 -4
- geo_activity_playground/importers/strava_api.py +6 -6
- geo_activity_playground/importers/strava_checkout.py +8 -0
- geo_activity_playground/webui/app.py +60 -10
- geo_activity_playground/webui/calendar_controller.py +0 -3
- geo_activity_playground/webui/eddington_controller.py +0 -3
- geo_activity_playground/webui/entry_controller.py +3 -3
- geo_activity_playground/webui/equipment_controller.py +0 -3
- geo_activity_playground/webui/explorer_controller.py +16 -18
- geo_activity_playground/webui/heatmap_controller.py +16 -23
- geo_activity_playground/webui/square_planner_controller.py +6 -7
- geo_activity_playground/webui/templates/calendar.html.j2 +2 -2
- geo_activity_playground/webui/templates/page.html.j2 +13 -0
- geo_activity_playground/webui/templates/upload.html.j2 +37 -0
- geo_activity_playground/webui/upload_controller.py +114 -0
- {geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/RECORD +27 -25
- {geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/entry_points.txt +0 -0
@@ -7,17 +7,13 @@ import sys
|
|
7
7
|
import coloredlogs
|
8
8
|
|
9
9
|
from .importers.strava_checkout import convert_strava_checkout
|
10
|
-
from .importers.strava_checkout import import_from_strava_checkout
|
11
10
|
from geo_activity_playground.core.activities import ActivityRepository
|
12
|
-
from geo_activity_playground.core.activities import embellish_time_series
|
13
11
|
from geo_activity_playground.core.cache_migrations import apply_cache_migrations
|
14
12
|
from geo_activity_playground.core.config import get_config
|
15
|
-
from geo_activity_playground.explorer.tile_visits import
|
16
|
-
from geo_activity_playground.explorer.tile_visits import compute_tile_visits
|
13
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
17
14
|
from geo_activity_playground.explorer.video import explorer_video_main
|
18
|
-
from geo_activity_playground.importers.directory import import_from_directory
|
19
|
-
from geo_activity_playground.importers.strava_api import import_from_strava_api
|
20
15
|
from geo_activity_playground.webui.app import webui_main
|
16
|
+
from geo_activity_playground.webui.upload_controller import scan_for_activities
|
21
17
|
|
22
18
|
logger = logging.getLogger(__name__)
|
23
19
|
|
@@ -68,7 +64,7 @@ def main() -> None:
|
|
68
64
|
subparser = subparsers.add_parser("serve", help="Launch webserver")
|
69
65
|
subparser.set_defaults(
|
70
66
|
func=lambda options: webui_main(
|
71
|
-
make_activity_repository(options.basedir, options.skip_strava),
|
67
|
+
*make_activity_repository(options.basedir, options.skip_strava),
|
72
68
|
host=options.host,
|
73
69
|
port=options.port,
|
74
70
|
)
|
@@ -99,7 +95,7 @@ def main() -> None:
|
|
99
95
|
|
100
96
|
def make_activity_repository(
|
101
97
|
basedir: pathlib.Path, skip_strava: bool
|
102
|
-
) -> ActivityRepository:
|
98
|
+
) -> tuple[ActivityRepository, TileVisitAccessor, dict]:
|
103
99
|
os.chdir(basedir)
|
104
100
|
apply_cache_migrations()
|
105
101
|
config = get_config()
|
@@ -111,27 +107,11 @@ def make_activity_repository(
|
|
111
107
|
sys.exit(1)
|
112
108
|
|
113
109
|
repository = ActivityRepository()
|
110
|
+
tile_visit_accessor = TileVisitAccessor()
|
114
111
|
|
115
|
-
|
116
|
-
import_from_directory(
|
117
|
-
repository,
|
118
|
-
config.get("metadata_extraction_regexes", []),
|
119
|
-
)
|
120
|
-
if pathlib.Path("Strava Export").exists():
|
121
|
-
import_from_strava_checkout(repository)
|
122
|
-
if "strava" in config and not skip_strava:
|
123
|
-
import_from_strava_api(repository)
|
124
|
-
|
125
|
-
if len(repository) == 0:
|
126
|
-
logger.error(
|
127
|
-
f"No activities found. You need to either add activity files (GPX, FIT, …) to {basedir/'Activities'} or set up the Strava API. Starting without any activities is unfortunately not supported."
|
128
|
-
)
|
129
|
-
sys.exit(1)
|
112
|
+
scan_for_activities(repository, tile_visit_accessor, config, skip_strava)
|
130
113
|
|
131
|
-
|
132
|
-
compute_tile_visits(repository)
|
133
|
-
compute_tile_evolution()
|
134
|
-
return repository
|
114
|
+
return repository, tile_visit_accessor, config
|
135
115
|
|
136
116
|
|
137
117
|
if __name__ == "__main__":
|
@@ -78,6 +78,11 @@ class ActivityRepository:
|
|
78
78
|
f"Adding {len(self._loose_activities)} activities to the repository …"
|
79
79
|
)
|
80
80
|
new_df = pd.DataFrame(self._loose_activities)
|
81
|
+
if not pd.api.types.is_dtype_equal(
|
82
|
+
new_df["start"].dtype, "datetime64[ns, UTC]"
|
83
|
+
):
|
84
|
+
new_df["start"] = new_df["start"].dt.tz_localize("UTC")
|
85
|
+
new_df["start"] = new_df["start"].dt.tz_convert("UTC")
|
81
86
|
if len(self.meta):
|
82
87
|
new_ids_set = set(new_df["id"])
|
83
88
|
is_kept = [
|
@@ -89,7 +94,7 @@ class ActivityRepository:
|
|
89
94
|
self.meta = pd.concat([old_df, new_df])
|
90
95
|
assert pd.api.types.is_dtype_equal(
|
91
96
|
self.meta["start"].dtype, "datetime64[ns, UTC]"
|
92
|
-
), self.meta["start"].dtype
|
97
|
+
), (self.meta["start"].dtype, self.meta["start"].iloc[0])
|
93
98
|
self.save()
|
94
99
|
self._loose_activities = []
|
95
100
|
|
@@ -120,10 +125,11 @@ class ActivityRepository:
|
|
120
125
|
def activity_ids(self) -> set[int]:
|
121
126
|
return set(self.meta.index)
|
122
127
|
|
123
|
-
def iter_activities(self, new_to_old=True) -> Iterator[ActivityMeta]:
|
128
|
+
def iter_activities(self, new_to_old=True, dropna=False) -> Iterator[ActivityMeta]:
|
124
129
|
direction = -1 if new_to_old else 1
|
125
130
|
for index, row in self.meta[::direction].iterrows():
|
126
|
-
|
131
|
+
if not dropna or not pd.isna(row["start"]):
|
132
|
+
yield row
|
127
133
|
|
128
134
|
@functools.lru_cache()
|
129
135
|
def get_activity_by_id(self, id: int) -> ActivityMeta:
|
@@ -12,6 +12,7 @@ import numpy as np
|
|
12
12
|
import pandas as pd
|
13
13
|
import tcxreader.tcxreader
|
14
14
|
import xmltodict
|
15
|
+
from pandas._libs import NaTType
|
15
16
|
|
16
17
|
from geo_activity_playground.core.activities import ActivityMeta
|
17
18
|
from geo_activity_playground.core.activities import embellish_single_time_series
|
@@ -126,8 +127,12 @@ def read_fit_activity(path: pathlib.Path, open) -> tuple[ActivityMeta, pd.DataFr
|
|
126
127
|
and values.get("position_long", None)
|
127
128
|
):
|
128
129
|
time = values["timestamp"]
|
129
|
-
|
130
|
-
|
130
|
+
if isinstance(time, datetime.datetime):
|
131
|
+
time = time.astimezone(datetime.timezone.utc)
|
132
|
+
elif time is None or isinstance(time, int):
|
133
|
+
time = pd.NaT
|
134
|
+
else:
|
135
|
+
raise RuntimeError(f"Cannot parse time: {time} in {path}.")
|
131
136
|
row = {
|
132
137
|
"time": time,
|
133
138
|
"latitude": values["position_lat"] / ((2**32) / 360),
|
@@ -202,10 +207,13 @@ def read_gpx_activity(path: pathlib.Path, open) -> pd.DataFrame:
|
|
202
207
|
for point in segment.points:
|
203
208
|
if isinstance(point.time, datetime.datetime):
|
204
209
|
time = point.time
|
205
|
-
|
210
|
+
time = time.astimezone(datetime.timezone.utc)
|
211
|
+
elif isinstance(point.time, str):
|
206
212
|
time = dateutil.parser.parse(str(point.time))
|
207
|
-
|
208
|
-
|
213
|
+
time = time.astimezone(datetime.timezone.utc)
|
214
|
+
else:
|
215
|
+
time = pd.NaT
|
216
|
+
time.tz_localize("UTC")
|
209
217
|
points.append((time, point.latitude, point.longitude, point.elevation))
|
210
218
|
|
211
219
|
df = pd.DataFrame(points, columns=["time", "latitude", "longitude", "altitude"])
|
@@ -233,6 +241,7 @@ def read_tcx_activity(path: pathlib.Path, opener) -> pd.DataFrame:
|
|
233
241
|
content = f.read().strip()
|
234
242
|
|
235
243
|
stripped_file = pathlib.Path("Cache/temp.tcx")
|
244
|
+
stripped_file.parent.mkdir(exist_ok=True)
|
236
245
|
with open(stripped_file, "wb") as f:
|
237
246
|
f.write(content)
|
238
247
|
data = tcx_reader.read(str(stripped_file))
|
@@ -22,7 +22,7 @@ class GeoBounds:
|
|
22
22
|
lon_max: float
|
23
23
|
|
24
24
|
|
25
|
-
def get_bounds(lat_lon_data: np.
|
25
|
+
def get_bounds(lat_lon_data: np.ndarray) -> GeoBounds:
|
26
26
|
return GeoBounds(*np.min(lat_lon_data, axis=0), *np.max(lat_lon_data, axis=0))
|
27
27
|
|
28
28
|
|
@@ -106,7 +106,7 @@ def get_sensible_zoom_level(
|
|
106
106
|
)
|
107
107
|
|
108
108
|
|
109
|
-
def build_map_from_tiles(tile_bounds: TileBounds) -> np.
|
109
|
+
def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
|
110
110
|
background = np.zeros((*tile_bounds.shape, 3))
|
111
111
|
|
112
112
|
for x in range(tile_bounds.x_tile_min, tile_bounds.x_tile_max):
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
def get_border_tiles(
|
18
18
|
tiles: pd.DataFrame, zoom: int, tile_bounds: Bounds
|
19
|
-
) -> list[list[
|
19
|
+
) -> list[list[tuple[float, float]]]:
|
20
20
|
logger.info("Generate border tiles …")
|
21
21
|
tile_set = set(zip(tiles["tile_x"], tiles["tile_y"]))
|
22
22
|
border_tiles = set()
|
@@ -28,7 +28,9 @@ def get_border_tiles(
|
|
28
28
|
return make_grid_points(border_tiles, zoom)
|
29
29
|
|
30
30
|
|
31
|
-
def get_explored_tiles(
|
31
|
+
def get_explored_tiles(
|
32
|
+
tiles: pd.DataFrame, zoom: int
|
33
|
+
) -> list[list[tuple[float, float]]]:
|
32
34
|
return make_grid_points(zip(tiles["tile_x"], tiles["tile_y"]), zoom)
|
33
35
|
|
34
36
|
|
@@ -66,7 +68,7 @@ def make_explorer_rectangle(
|
|
66
68
|
|
67
69
|
def make_grid_points(
|
68
70
|
tiles: Iterable[tuple[int, int]], zoom: int
|
69
|
-
) -> list[list[
|
71
|
+
) -> list[list[tuple[float, float]]]:
|
70
72
|
result = []
|
71
73
|
for tile_x, tile_y in tiles:
|
72
74
|
tile = [
|
@@ -80,7 +82,7 @@ def make_grid_points(
|
|
80
82
|
return result
|
81
83
|
|
82
84
|
|
83
|
-
def make_grid_file_gpx(grid_points: list[list[
|
85
|
+
def make_grid_file_gpx(grid_points: list[list[tuple[float, float]]]) -> str:
|
84
86
|
gpx = gpxpy.gpx.GPX()
|
85
87
|
gpx_track = gpxpy.gpx.GPXTrack()
|
86
88
|
gpx.tracks.append(gpx_track)
|
@@ -93,7 +95,7 @@ def make_grid_file_gpx(grid_points: list[list[list[float]]]) -> str:
|
|
93
95
|
return gpx.to_xml()
|
94
96
|
|
95
97
|
|
96
|
-
def make_grid_file_geojson(grid_points: list[list[
|
98
|
+
def make_grid_file_geojson(grid_points: list[list[tuple[float, float]]]) -> str:
|
97
99
|
fc = geojson.FeatureCollection(
|
98
100
|
[
|
99
101
|
geojson.Feature(
|
@@ -20,18 +20,39 @@ from geo_activity_playground.core.tiles import interpolate_missing_tile
|
|
20
20
|
|
21
21
|
logger = logging.getLogger(__name__)
|
22
22
|
|
23
|
-
TILE_EVOLUTION_STATES_PATH = pathlib.Path("Cache/tile-evolution-state.pickle")
|
24
|
-
TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
|
25
|
-
TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
|
26
23
|
|
24
|
+
class TileVisitAccessor:
|
25
|
+
TILE_EVOLUTION_STATES_PATH = pathlib.Path("Cache/tile-evolution-state.pickle")
|
26
|
+
TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
|
27
|
+
TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
|
27
28
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
def __init__(self) -> None:
|
30
|
+
self.visits: dict[int, dict[tuple[int, int], dict[str, Any]]] = try_load_pickle(
|
31
|
+
self.TILE_VISITS_PATH
|
32
|
+
) or collections.defaultdict(dict)
|
33
|
+
|
34
|
+
self.histories: dict[int, pd.DataFrame] = try_load_pickle(
|
35
|
+
self.TILE_HISTORIES_PATH
|
36
|
+
) or collections.defaultdict(pd.DataFrame)
|
37
|
+
|
38
|
+
self.states = try_load_pickle(
|
39
|
+
self.TILE_EVOLUTION_STATES_PATH
|
40
|
+
) or collections.defaultdict(TileEvolutionState)
|
41
|
+
|
42
|
+
def save(self) -> None:
|
43
|
+
with open(self.TILE_VISITS_PATH, "wb") as f:
|
44
|
+
pickle.dump(self.visits, f)
|
45
|
+
|
46
|
+
with open(self.TILE_HISTORIES_PATH, "wb") as f:
|
47
|
+
pickle.dump(self.histories, f)
|
48
|
+
|
49
|
+
with open(self.TILE_EVOLUTION_STATES_PATH, "wb") as f:
|
50
|
+
pickle.dump(self.states, f)
|
51
|
+
|
52
|
+
|
53
|
+
def compute_tile_visits(
|
54
|
+
repository: ActivityRepository, tile_visits_accessor: TileVisitAccessor
|
55
|
+
) -> None:
|
35
56
|
|
36
57
|
work_tracker = WorkTracker("tile-visits")
|
37
58
|
activity_ids_to_process = work_tracker.filter(repository.activity_ids)
|
@@ -43,8 +64,8 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
|
|
43
64
|
for zoom in range(20):
|
44
65
|
for time, tile_x, tile_y in _tiles_from_points(time_series, zoom):
|
45
66
|
tile = (tile_x, tile_y)
|
46
|
-
if tile in
|
47
|
-
d =
|
67
|
+
if tile in tile_visits_accessor.visits[zoom]:
|
68
|
+
d = tile_visits_accessor.visits[zoom][tile]
|
48
69
|
if d["first_time"] > time:
|
49
70
|
d["first_time"] = time
|
50
71
|
d["first_id"] = activity_id
|
@@ -53,7 +74,7 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
|
|
53
74
|
d["last_id"] = activity_id
|
54
75
|
d["activity_ids"].add(activity_id)
|
55
76
|
else:
|
56
|
-
|
77
|
+
tile_visits_accessor.visits[zoom][tile] = {
|
57
78
|
"first_time": time,
|
58
79
|
"first_id": activity_id,
|
59
80
|
"last_time": time,
|
@@ -71,16 +92,19 @@ def compute_tile_visits(repository: ActivityRepository) -> None:
|
|
71
92
|
work_tracker.mark_done(activity_id)
|
72
93
|
|
73
94
|
if activity_ids_to_process:
|
74
|
-
with open(TILE_VISITS_PATH, "wb") as f:
|
75
|
-
pickle.dump(tile_visits, f)
|
76
|
-
|
77
95
|
for zoom, new_rows in new_tile_history_rows.items():
|
78
96
|
new_df = pd.DataFrame(new_rows)
|
97
|
+
if not pd.api.types.is_dtype_equal(
|
98
|
+
new_df["time"].dtype, "datetime64[ns, UTC]"
|
99
|
+
):
|
100
|
+
new_df["time"] = new_df["time"].dt.tz_localize("UTC")
|
101
|
+
new_df["time"] = new_df["time"].dt.tz_convert("UTC")
|
79
102
|
new_df.sort_values("time", inplace=True)
|
80
|
-
|
103
|
+
tile_visits_accessor.histories[zoom] = pd.concat(
|
104
|
+
[tile_visits_accessor.histories[zoom], new_df]
|
105
|
+
)
|
81
106
|
|
82
|
-
|
83
|
-
pickle.dump(tile_history, f)
|
107
|
+
tile_visits_accessor.save()
|
84
108
|
|
85
109
|
work_tracker.close()
|
86
110
|
|
@@ -123,23 +147,19 @@ class TileEvolutionState:
|
|
123
147
|
self.square_y: Optional[int] = None
|
124
148
|
|
125
149
|
|
126
|
-
def compute_tile_evolution() -> None:
|
127
|
-
with open(TILE_HISTORIES_PATH, "rb") as f:
|
128
|
-
tile_histories = pickle.load(f)
|
129
|
-
|
130
|
-
states = try_load_pickle(TILE_EVOLUTION_STATES_PATH) or collections.defaultdict(
|
131
|
-
TileEvolutionState
|
132
|
-
)
|
133
|
-
|
150
|
+
def compute_tile_evolution(tile_visits_accessor: TileVisitAccessor) -> None:
|
134
151
|
zoom_levels = list(reversed(list(range(20))))
|
135
152
|
|
136
153
|
for zoom in tqdm(zoom_levels, desc="Compute explorer cluster evolution"):
|
137
|
-
_compute_cluster_evolution(
|
154
|
+
_compute_cluster_evolution(
|
155
|
+
tile_visits_accessor.histories[zoom], tile_visits_accessor.states[zoom]
|
156
|
+
)
|
138
157
|
for zoom in tqdm(zoom_levels, desc="Compute explorer square evolution"):
|
139
|
-
_compute_square_history(
|
158
|
+
_compute_square_history(
|
159
|
+
tile_visits_accessor.histories[zoom], tile_visits_accessor.states[zoom]
|
160
|
+
)
|
140
161
|
|
141
|
-
|
142
|
-
pickle.dump(states, f)
|
162
|
+
tile_visits_accessor.save()
|
143
163
|
|
144
164
|
|
145
165
|
def _compute_cluster_evolution(tiles: pd.DataFrame, s: TileEvolutionState) -> None:
|
@@ -3,6 +3,7 @@ import math
|
|
3
3
|
import pathlib
|
4
4
|
from typing import Generator
|
5
5
|
from typing import List
|
6
|
+
from typing import Optional
|
6
7
|
from typing import Set
|
7
8
|
from typing import Tuple
|
8
9
|
|
@@ -23,8 +24,8 @@ def build_image(
|
|
23
24
|
brightness: float = 1.0,
|
24
25
|
width: int = 1920,
|
25
26
|
height: int = 1080,
|
26
|
-
frame_counter: int =
|
27
|
-
) -> Image.Image:
|
27
|
+
frame_counter: int = 0,
|
28
|
+
) -> Optional[Image.Image]:
|
28
29
|
path = pathlib.Path(f"video/{frame_counter:06d}.png")
|
29
30
|
if path.exists():
|
30
31
|
return None
|
@@ -49,9 +49,9 @@ def import_from_directory(
|
|
49
49
|
paths_with_errors = [error for error in paths_with_errors if error]
|
50
50
|
|
51
51
|
for path in tqdm(new_activity_paths, desc="Collate activity metadata"):
|
52
|
-
activity_id =
|
52
|
+
activity_id = get_file_hash(path)
|
53
53
|
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
54
|
-
work_tracker.mark_done(
|
54
|
+
work_tracker.mark_done(path)
|
55
55
|
|
56
56
|
if not file_metadata_path.exists():
|
57
57
|
continue
|
@@ -87,7 +87,7 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
|
|
87
87
|
activity_stream_dir = pathlib.Path("Cache/Activity Timeseries")
|
88
88
|
file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
|
89
89
|
|
90
|
-
activity_id =
|
90
|
+
activity_id = get_file_hash(path)
|
91
91
|
timeseries_path = activity_stream_dir / f"{activity_id}.parquet"
|
92
92
|
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
93
93
|
|
@@ -110,7 +110,7 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
|
|
110
110
|
pickle.dump(activity_meta_from_file, f)
|
111
111
|
|
112
112
|
|
113
|
-
def
|
113
|
+
def get_file_hash(path: pathlib.Path) -> int:
|
114
114
|
file_hash = hashlib.blake2s()
|
115
115
|
with open(path, "rb") as f:
|
116
116
|
while chunk := f.read(8192):
|
@@ -103,12 +103,12 @@ def import_from_strava_api(repository: ActivityRepository) -> None:
|
|
103
103
|
time.sleep(seconds_to_wait)
|
104
104
|
|
105
105
|
|
106
|
-
def try_import_strava(repository: ActivityRepository) ->
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
106
|
+
def try_import_strava(repository: ActivityRepository) -> bool:
|
107
|
+
last = repository.last_activity_date()
|
108
|
+
if last is None:
|
109
|
+
get_after = "2000-01-01T00:00:00Z"
|
110
|
+
else:
|
111
|
+
get_after = last.isoformat().replace("+00:00", "Z")
|
112
112
|
|
113
113
|
gear_names = {None: "None"}
|
114
114
|
|
@@ -2,6 +2,7 @@ import datetime
|
|
2
2
|
import logging
|
3
3
|
import pathlib
|
4
4
|
import shutil
|
5
|
+
import sys
|
5
6
|
import traceback
|
6
7
|
from typing import Optional
|
7
8
|
from typing import Union
|
@@ -133,6 +134,13 @@ def import_from_strava_checkout(repository: ActivityRepository) -> None:
|
|
133
134
|
dayfirst = False
|
134
135
|
if activities.columns[0] == "Aktivitäts-ID":
|
135
136
|
activities = pd.read_csv(checkout_path / "activities.csv", decimal=",")
|
137
|
+
if len(activities.columns) != len(EXPECTED_COLUMNS):
|
138
|
+
logger.error(
|
139
|
+
f"You are trying to import a Strava checkout where the `activities.csv` contains German column headers. In order to import this, we need to map these to the English ones. Unfortunately Strava has changed the number of columns. Your file has {len(activities.columns)} but we expect {len(EXPECTED_COLUMNS)}. This means that the program needs to be updated to match the new Strava export format. Please go to https://github.com/martin-ueding/geo-activity-playground/issues and open a new issue and share the following output in the ticket:"
|
140
|
+
)
|
141
|
+
print(activities.columns)
|
142
|
+
print(activities.dtypes)
|
143
|
+
sys.exit(1)
|
136
144
|
activities.columns = EXPECTED_COLUMNS
|
137
145
|
dayfirst = True
|
138
146
|
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import json
|
2
|
+
import pathlib
|
3
|
+
import secrets
|
1
4
|
import urllib
|
2
5
|
|
3
6
|
from flask import Flask
|
@@ -9,6 +12,7 @@ from flask import Response
|
|
9
12
|
from .locations_controller import LocationsController
|
10
13
|
from .search_controller import SearchController
|
11
14
|
from geo_activity_playground.core.activities import ActivityRepository
|
15
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
12
16
|
from geo_activity_playground.webui.activity_controller import ActivityController
|
13
17
|
from geo_activity_playground.webui.calendar_controller import CalendarController
|
14
18
|
from geo_activity_playground.webui.config_controller import ConfigController
|
@@ -26,6 +30,7 @@ from geo_activity_playground.webui.summary_controller import SummaryController
|
|
26
30
|
from geo_activity_playground.webui.tile_controller import (
|
27
31
|
TileController,
|
28
32
|
)
|
33
|
+
from geo_activity_playground.webui.upload_controller import UploadController
|
29
34
|
|
30
35
|
|
31
36
|
def route_activity(app: Flask, repository: ActivityRepository) -> None:
|
@@ -113,8 +118,10 @@ def route_equipment(app: Flask, repository: ActivityRepository) -> None:
|
|
113
118
|
return render_template("equipment.html.j2", **equipment_controller.render())
|
114
119
|
|
115
120
|
|
116
|
-
def route_explorer(
|
117
|
-
|
121
|
+
def route_explorer(
|
122
|
+
app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
123
|
+
) -> None:
|
124
|
+
explorer_controller = ExplorerController(repository, tile_visit_accessor)
|
118
125
|
|
119
126
|
@app.route("/explorer/<zoom>")
|
120
127
|
def explorer(zoom: str):
|
@@ -159,8 +166,10 @@ def route_explorer(app: Flask, repository: ActivityRepository) -> None:
|
|
159
166
|
)
|
160
167
|
|
161
168
|
|
162
|
-
def route_heatmap(
|
163
|
-
|
169
|
+
def route_heatmap(
|
170
|
+
app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
171
|
+
) -> None:
|
172
|
+
heatmap_controller = HeatmapController(repository, tile_visit_accessor)
|
164
173
|
|
165
174
|
@app.route("/heatmap")
|
166
175
|
def heatmap():
|
@@ -207,8 +216,10 @@ def route_search(app: Flask, repository: ActivityRepository) -> None:
|
|
207
216
|
)
|
208
217
|
|
209
218
|
|
210
|
-
def route_square_planner(
|
211
|
-
|
219
|
+
def route_square_planner(
|
220
|
+
app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
221
|
+
) -> None:
|
222
|
+
controller = SquarePlannerController(repository, tile_visit_accessor)
|
212
223
|
|
213
224
|
@app.route("/square-planner/<zoom>/<x>/<y>/<size>")
|
214
225
|
def square_planner_planner(zoom, x, y, size):
|
@@ -303,7 +314,42 @@ def route_tiles(app: Flask, repository: ActivityRepository) -> None:
|
|
303
314
|
)
|
304
315
|
|
305
316
|
|
306
|
-
def
|
317
|
+
def route_upload(
|
318
|
+
app: Flask,
|
319
|
+
repository: ActivityRepository,
|
320
|
+
tile_visit_accessor: TileVisitAccessor,
|
321
|
+
config: dict,
|
322
|
+
):
|
323
|
+
upload_controller = UploadController(repository, tile_visit_accessor, config)
|
324
|
+
|
325
|
+
@app.route("/upload")
|
326
|
+
def form():
|
327
|
+
return render_template("upload.html.j2", **upload_controller.render_form())
|
328
|
+
|
329
|
+
@app.route("/upload/receive", methods=["POST"])
|
330
|
+
def receive():
|
331
|
+
return upload_controller.receive()
|
332
|
+
|
333
|
+
|
334
|
+
def get_secret_key():
|
335
|
+
secret_file = pathlib.Path("Cache/flask-secret.json")
|
336
|
+
if secret_file.exists():
|
337
|
+
with open(secret_file) as f:
|
338
|
+
secret = json.load(f)
|
339
|
+
else:
|
340
|
+
secret = secrets.token_hex()
|
341
|
+
with open(secret_file, "w") as f:
|
342
|
+
json.dump(secret, f)
|
343
|
+
return secret
|
344
|
+
|
345
|
+
|
346
|
+
def webui_main(
|
347
|
+
repository: ActivityRepository,
|
348
|
+
tile_visit_accessor: TileVisitAccessor,
|
349
|
+
config: dict,
|
350
|
+
host: str,
|
351
|
+
port: int,
|
352
|
+
) -> None:
|
307
353
|
app = Flask(__name__)
|
308
354
|
|
309
355
|
route_activity(app, repository)
|
@@ -311,14 +357,18 @@ def webui_main(repository: ActivityRepository, host: str, port: int) -> None:
|
|
311
357
|
route_config(app, repository)
|
312
358
|
route_eddington(app, repository)
|
313
359
|
route_equipment(app, repository)
|
314
|
-
route_explorer(app, repository)
|
315
|
-
route_heatmap(app, repository)
|
360
|
+
route_explorer(app, repository, tile_visit_accessor)
|
361
|
+
route_heatmap(app, repository, tile_visit_accessor)
|
316
362
|
route_locations(app, repository)
|
317
363
|
route_search(app, repository)
|
318
|
-
route_square_planner(app, repository)
|
364
|
+
route_square_planner(app, repository, tile_visit_accessor)
|
319
365
|
route_start(app, repository)
|
320
366
|
route_strava(app, host, port)
|
321
367
|
route_summary(app, repository)
|
322
368
|
route_tiles(app, repository)
|
369
|
+
route_upload(app, repository, tile_visit_accessor, config)
|
370
|
+
|
371
|
+
app.config["UPLOAD_FOLDER"] = "Activities"
|
372
|
+
app.secret_key = get_secret_key()
|
323
373
|
|
324
374
|
app.run(host=host, port=port)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import collections
|
2
2
|
import datetime
|
3
|
-
import functools
|
4
3
|
|
5
4
|
from geo_activity_playground.core.activities import ActivityRepository
|
6
5
|
|
@@ -9,7 +8,6 @@ class CalendarController:
|
|
9
8
|
def __init__(self, repository: ActivityRepository) -> None:
|
10
9
|
self._repository = repository
|
11
10
|
|
12
|
-
@functools.cache
|
13
11
|
def render_overview(self) -> dict:
|
14
12
|
meta = self._repository.meta.copy()
|
15
13
|
meta["date"] = meta["start"].dt.date
|
@@ -41,7 +39,6 @@ class CalendarController:
|
|
41
39
|
"yearly_distances": yearly_distances,
|
42
40
|
}
|
43
41
|
|
44
|
-
@functools.cache
|
45
42
|
def render_month(self, year: int, month: int) -> dict:
|
46
43
|
meta = self._repository.meta.copy()
|
47
44
|
meta["date"] = meta["start"].dt.date
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import functools
|
2
|
-
|
3
1
|
import altair as alt
|
4
2
|
import numpy as np
|
5
3
|
import pandas as pd
|
@@ -11,7 +9,6 @@ class EddingtonController:
|
|
11
9
|
def __init__(self, repository: ActivityRepository) -> None:
|
12
10
|
self._repository = repository
|
13
11
|
|
14
|
-
@functools.cache
|
15
12
|
def render(self) -> dict:
|
16
13
|
activities = self._repository.meta.copy()
|
17
14
|
activities["day"] = [start.date() for start in activities["start"]]
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import datetime
|
2
|
-
import functools
|
3
2
|
import itertools
|
4
3
|
|
5
4
|
import altair as alt
|
@@ -13,7 +12,6 @@ class EntryController:
|
|
13
12
|
def __init__(self, repository: ActivityRepository) -> None:
|
14
13
|
self._repository = repository
|
15
14
|
|
16
|
-
@functools.cache
|
17
15
|
def render(self) -> dict:
|
18
16
|
result = {
|
19
17
|
"distance_last_30_days_plot": distance_last_30_days_meta_plot(
|
@@ -22,7 +20,9 @@ class EntryController:
|
|
22
20
|
"latest_activities": [],
|
23
21
|
}
|
24
22
|
|
25
|
-
for activity in itertools.islice(
|
23
|
+
for activity in itertools.islice(
|
24
|
+
self._repository.iter_activities(dropna=True), 15
|
25
|
+
):
|
26
26
|
time_series = self._repository.get_time_series(activity["id"])
|
27
27
|
result["latest_activities"].append(
|
28
28
|
{
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import functools
|
2
|
-
|
3
1
|
import altair as alt
|
4
2
|
import pandas as pd
|
5
3
|
|
@@ -11,7 +9,6 @@ class EquipmentController:
|
|
11
9
|
def __init__(self, repository: ActivityRepository) -> None:
|
12
10
|
self._repository = repository
|
13
11
|
|
14
|
-
@functools.cache
|
15
12
|
def render(self) -> dict:
|
16
13
|
total_distances = (
|
17
14
|
self._repository.meta.groupby("equipment")
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import datetime
|
2
|
-
import functools
|
3
2
|
import itertools
|
4
3
|
import pickle
|
5
4
|
|
@@ -20,27 +19,24 @@ from geo_activity_playground.explorer.grid_file import make_explorer_tile
|
|
20
19
|
from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
|
21
20
|
from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
|
22
21
|
from geo_activity_playground.explorer.grid_file import make_grid_points
|
23
|
-
from geo_activity_playground.explorer.tile_visits import TILE_EVOLUTION_STATES_PATH
|
24
|
-
from geo_activity_playground.explorer.tile_visits import TILE_HISTORIES_PATH
|
25
|
-
from geo_activity_playground.explorer.tile_visits import TILE_VISITS_PATH
|
26
22
|
from geo_activity_playground.explorer.tile_visits import TileEvolutionState
|
23
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
27
24
|
|
28
25
|
|
29
26
|
alt.data_transformers.enable("vegafusion")
|
30
27
|
|
31
28
|
|
32
29
|
class ExplorerController:
|
33
|
-
def __init__(
|
30
|
+
def __init__(
|
31
|
+
self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
32
|
+
) -> None:
|
34
33
|
self._repository = repository
|
34
|
+
self._tile_visit_accessor = tile_visit_accessor
|
35
35
|
|
36
|
-
@functools.cache
|
37
36
|
def render(self, zoom: int) -> dict:
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
tile_visits = pickle.load(f)
|
42
|
-
with open(TILE_HISTORIES_PATH, "rb") as f:
|
43
|
-
tile_histories = pickle.load(f)
|
37
|
+
tile_evolution_states = self._tile_visit_accessor.states
|
38
|
+
tile_visits = self._tile_visit_accessor.visits
|
39
|
+
tile_histories = self._tile_visit_accessor.histories
|
44
40
|
|
45
41
|
medians = tile_histories[zoom].median()
|
46
42
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
@@ -79,8 +75,7 @@ class ExplorerController:
|
|
79
75
|
x2, y2 = compute_tile(south, east, zoom)
|
80
76
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
81
77
|
|
82
|
-
|
83
|
-
tile_histories = pickle.load(f)
|
78
|
+
tile_histories = self._tile_visit_accessor.histories
|
84
79
|
tiles = tile_histories[zoom]
|
85
80
|
points = get_border_tiles(tiles, zoom, tile_bounds)
|
86
81
|
if suffix == "geojson":
|
@@ -93,8 +88,7 @@ class ExplorerController:
|
|
93
88
|
x2, y2 = compute_tile(south, east, zoom)
|
94
89
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
95
90
|
|
96
|
-
|
97
|
-
tile_visits = pickle.load(f)
|
91
|
+
tile_visits = self._tile_visit_accessor.visits
|
98
92
|
tiles = tile_visits[zoom]
|
99
93
|
points = make_grid_points(
|
100
94
|
(tile for tile in tiles.keys() if tile_bounds.contains(*tile)), zoom
|
@@ -118,8 +112,12 @@ def get_three_color_tiles(
|
|
118
112
|
cmap_last = matplotlib.colormaps["plasma"]
|
119
113
|
tile_dict = {}
|
120
114
|
for tile, tile_data in tile_visits.items():
|
121
|
-
|
122
|
-
|
115
|
+
if not pd.isna(tile_data["first_time"]):
|
116
|
+
first_age_days = (today - tile_data["first_time"].date()).days
|
117
|
+
last_age_days = (today - tile_data["last_time"].date()).days
|
118
|
+
else:
|
119
|
+
first_age_days = 10000
|
120
|
+
last_age_days = 10000
|
123
121
|
tile_dict[tile] = {
|
124
122
|
"first_activity_id": str(tile_data["first_id"]),
|
125
123
|
"first_activity_name": repository.get_activity_by_id(tile_data["first_id"])[
|
@@ -1,14 +1,9 @@
|
|
1
|
-
import functools
|
2
1
|
import io
|
3
2
|
import logging
|
4
3
|
import pathlib
|
5
|
-
import pickle
|
6
|
-
import threading
|
7
4
|
|
8
|
-
import matplotlib
|
9
5
|
import matplotlib.pylab as pl
|
10
6
|
import numpy as np
|
11
|
-
import pandas as pd
|
12
7
|
from PIL import Image
|
13
8
|
from PIL import ImageDraw
|
14
9
|
|
@@ -19,9 +14,7 @@ from geo_activity_playground.core.heatmap import get_sensible_zoom_level
|
|
19
14
|
from geo_activity_playground.core.tasks import work_tracker
|
20
15
|
from geo_activity_playground.core.tiles import get_tile
|
21
16
|
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
22
|
-
from geo_activity_playground.explorer.tile_visits import
|
23
|
-
from geo_activity_playground.explorer.tile_visits import TILE_HISTORIES_PATH
|
24
|
-
from geo_activity_playground.explorer.tile_visits import TILE_VISITS_PATH
|
17
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
25
18
|
from geo_activity_playground.webui.explorer_controller import (
|
26
19
|
bounding_box_for_biggest_cluster,
|
27
20
|
)
|
@@ -34,22 +27,20 @@ OSM_TILE_SIZE = 256 # OSM tile size in pixel
|
|
34
27
|
|
35
28
|
|
36
29
|
class HeatmapController:
|
37
|
-
def __init__(
|
30
|
+
def __init__(
|
31
|
+
self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
32
|
+
) -> None:
|
38
33
|
self._repository = repository
|
39
|
-
self.
|
34
|
+
self._tile_visit_accessor = tile_visit_accessor
|
40
35
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
self.tile_evolution_states = pickle.load(f)
|
45
|
-
with open(TILE_VISITS_PATH, "rb") as f:
|
46
|
-
self.tile_visits = pickle.load(f)
|
36
|
+
self.tile_histories = self._tile_visit_accessor.histories
|
37
|
+
self.tile_evolution_states = self._tile_visit_accessor.states
|
38
|
+
self.tile_visits = self._tile_visit_accessor.visits
|
47
39
|
|
48
|
-
@functools.cache
|
49
40
|
def render(self) -> dict:
|
50
41
|
zoom = 14
|
51
42
|
tiles = self.tile_histories[zoom]
|
52
|
-
medians = tiles.median()
|
43
|
+
medians = tiles.median(skipna=True)
|
53
44
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
54
45
|
medians["tile_x"], medians["tile_y"], zoom
|
55
46
|
)
|
@@ -58,11 +49,13 @@ class HeatmapController:
|
|
58
49
|
"center": {
|
59
50
|
"latitude": median_lat,
|
60
51
|
"longitude": median_lon,
|
61
|
-
"bbox":
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
52
|
+
"bbox": (
|
53
|
+
bounding_box_for_biggest_cluster(
|
54
|
+
cluster_state.clusters.values(), zoom
|
55
|
+
)
|
56
|
+
if len(cluster_state.memberships) > 0
|
57
|
+
else {}
|
58
|
+
),
|
66
59
|
}
|
67
60
|
}
|
68
61
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import functools
|
2
1
|
import pickle
|
3
2
|
|
4
3
|
import geojson
|
@@ -9,15 +8,17 @@ from geo_activity_playground.explorer.grid_file import make_explorer_tile
|
|
9
8
|
from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
|
10
9
|
from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
|
11
10
|
from geo_activity_playground.explorer.grid_file import make_grid_points
|
12
|
-
from geo_activity_playground.explorer.tile_visits import
|
11
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
13
12
|
|
14
13
|
|
15
14
|
class SquarePlannerController:
|
16
|
-
def __init__(
|
15
|
+
def __init__(
|
16
|
+
self, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
17
|
+
) -> None:
|
17
18
|
self._repository = repository
|
19
|
+
self._tile_visit_accessor = tile_visit_accessor
|
18
20
|
|
19
|
-
|
20
|
-
self._tile_visits = pickle.load(f)
|
21
|
+
self._tile_visits = self._tile_visit_accessor.visits
|
21
22
|
|
22
23
|
def action_planner(
|
23
24
|
self, zoom: int, square_x: int, square_y: int, square_size: int
|
@@ -79,11 +80,9 @@ class SquarePlannerController:
|
|
79
80
|
elif suffix == "gpx":
|
80
81
|
return make_grid_file_gpx(points)
|
81
82
|
|
82
|
-
@functools.cache
|
83
83
|
def _get_explored_tiles(self, zoom: int) -> set[tuple[int, int]]:
|
84
84
|
return set(self._tile_visits[zoom].keys())
|
85
85
|
|
86
|
-
@functools.cache
|
87
86
|
def _get_explored_geojson(self, zoom: int) -> str:
|
88
87
|
return geojson.dumps(
|
89
88
|
geojson.FeatureCollection(
|
@@ -22,12 +22,12 @@
|
|
22
22
|
<tbody>
|
23
23
|
{% for year, month_data in monthly_distances.items() %}
|
24
24
|
<tr>
|
25
|
-
<td>{{ year }}</td>
|
25
|
+
<td>{{ year|round(0)|int }}</td>
|
26
26
|
{% for month in range(1, 13) %}
|
27
27
|
<td align="right">
|
28
28
|
{% set distance = month_data[month] %}
|
29
29
|
{% if distance %}
|
30
|
-
<a href="/calendar/{{ year }}/{{ month }}">{{ distance|int() }} km</a>
|
30
|
+
<a href="/calendar/{{ year|round(0)|int }}/{{ month }}">{{ distance|int() }} km</a>
|
31
31
|
{% else %}
|
32
32
|
0 km
|
33
33
|
{% endif %}
|
@@ -84,6 +84,9 @@
|
|
84
84
|
<li class="nav-item">
|
85
85
|
<a class="nav-link active" aria-current="page" href="/equipment">Equipment</a>
|
86
86
|
</li>
|
87
|
+
<li class="nav-item">
|
88
|
+
<a class="nav-link active" aria-current="page" href="/upload">Upload</a>
|
89
|
+
</li>
|
87
90
|
</ul>
|
88
91
|
</div>
|
89
92
|
</div>
|
@@ -103,6 +106,16 @@
|
|
103
106
|
</script>
|
104
107
|
{% endmacro %}
|
105
108
|
|
109
|
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
110
|
+
{% if messages %}
|
111
|
+
{% for category, message in messages %}
|
112
|
+
<div class="alert alert-{{ category }}" role="alert">
|
113
|
+
{{ message }}
|
114
|
+
</div>
|
115
|
+
{% endfor %}
|
116
|
+
{% endif %}
|
117
|
+
{% endwith %}
|
118
|
+
|
106
119
|
{% block container %}
|
107
120
|
{% endblock %}
|
108
121
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<div class="row mb-1">
|
5
|
+
<div class="col">
|
6
|
+
<h1>Upload Activity</h1>
|
7
|
+
|
8
|
+
{% if has_upload %}
|
9
|
+
<form method="post" enctype="multipart/form-data" action="/upload/receive">
|
10
|
+
<div class="mb-3">
|
11
|
+
<label for="file1" class="form-label">Activity file</label>
|
12
|
+
<input type="file" name="file" id="file1" class="form-control">
|
13
|
+
</div>
|
14
|
+
<div class="mb-3">
|
15
|
+
<label for="directory" class="form-label">Target directory</label>
|
16
|
+
<select name="directory" id="directory" class="form-select" aria-label="Default select example">
|
17
|
+
{% for directory in directories %}
|
18
|
+
<option>{{ directory }}</option>
|
19
|
+
{% endfor %}
|
20
|
+
</select>
|
21
|
+
</div>
|
22
|
+
<div class="mb-3">
|
23
|
+
<label for="password" class="form-label">Password</label>
|
24
|
+
<input type="password" name="password" id="password" class="form-control">
|
25
|
+
</div>
|
26
|
+
<button type="submit" class="btn btn-primary">Upload</button>
|
27
|
+
</form>
|
28
|
+
{% else %}
|
29
|
+
<p>You don't have an upload password set. In order to use this feature, add the following to your configuration
|
30
|
+
file:</p>
|
31
|
+
<code><pre>[upload]
|
32
|
+
password = "your unique password here"</pre></code>
|
33
|
+
{% endif %}
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
{% endblock %}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import pathlib
|
4
|
+
import sys
|
5
|
+
|
6
|
+
from flask import flash
|
7
|
+
from flask import redirect
|
8
|
+
from flask import request
|
9
|
+
from flask import Response
|
10
|
+
from werkzeug.utils import secure_filename
|
11
|
+
|
12
|
+
from geo_activity_playground.core.activities import ActivityRepository
|
13
|
+
from geo_activity_playground.core.activities import embellish_time_series
|
14
|
+
from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
|
15
|
+
from geo_activity_playground.explorer.tile_visits import compute_tile_visits
|
16
|
+
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
17
|
+
from geo_activity_playground.importers.directory import get_file_hash
|
18
|
+
from geo_activity_playground.importers.directory import import_from_directory
|
19
|
+
from geo_activity_playground.importers.strava_api import import_from_strava_api
|
20
|
+
from geo_activity_playground.importers.strava_checkout import (
|
21
|
+
import_from_strava_checkout,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
class UploadController:
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
repository: ActivityRepository,
|
32
|
+
tile_visit_accessor: TileVisitAccessor,
|
33
|
+
config: dict,
|
34
|
+
) -> None:
|
35
|
+
self._repository = repository
|
36
|
+
self._tile_visit_accessor = tile_visit_accessor
|
37
|
+
self._config = config
|
38
|
+
|
39
|
+
def render_form(self) -> dict:
|
40
|
+
directories = []
|
41
|
+
for root, dirs, files in os.walk("Activities"):
|
42
|
+
directories.append(root)
|
43
|
+
directories.sort()
|
44
|
+
return {
|
45
|
+
"directories": directories,
|
46
|
+
"has_upload": "password" in self._config.get("upload", {}),
|
47
|
+
}
|
48
|
+
|
49
|
+
def receive(self) -> Response:
|
50
|
+
# check if the post request has the file part
|
51
|
+
if "file" not in request.files:
|
52
|
+
flash("No file could be found. Did you select a file?", "warning")
|
53
|
+
return redirect("/upload")
|
54
|
+
|
55
|
+
if request.form["password"] != self._config["upload"]["password"]:
|
56
|
+
flash("Incorrect upload password!", "danger")
|
57
|
+
return redirect("/upload")
|
58
|
+
|
59
|
+
file = request.files["file"]
|
60
|
+
# If the user does not select a file, the browser submits an
|
61
|
+
# empty file without a filename.
|
62
|
+
if file.filename == "":
|
63
|
+
flash("No selected file", "warning")
|
64
|
+
return redirect("/upload")
|
65
|
+
if file:
|
66
|
+
filename = secure_filename(file.filename)
|
67
|
+
target_path = pathlib.Path(request.form["directory"]) / filename
|
68
|
+
assert target_path.suffix in [
|
69
|
+
".csv",
|
70
|
+
".fit",
|
71
|
+
".gpx",
|
72
|
+
".gz",
|
73
|
+
".kml",
|
74
|
+
".kmz",
|
75
|
+
".tcx",
|
76
|
+
]
|
77
|
+
assert target_path.is_relative_to("Activities")
|
78
|
+
file.save(target_path)
|
79
|
+
scan_for_activities(
|
80
|
+
self._repository,
|
81
|
+
self._tile_visit_accessor,
|
82
|
+
self._config,
|
83
|
+
skip_strava=True,
|
84
|
+
)
|
85
|
+
activity_id = get_file_hash(target_path)
|
86
|
+
flash(f"Activity was saved with ID {activity_id}.", "success")
|
87
|
+
return redirect(f"/activity/{activity_id}")
|
88
|
+
|
89
|
+
|
90
|
+
def scan_for_activities(
|
91
|
+
repository: ActivityRepository,
|
92
|
+
tile_visit_accessor: TileVisitAccessor,
|
93
|
+
config: dict,
|
94
|
+
skip_strava: bool = False,
|
95
|
+
) -> None:
|
96
|
+
if pathlib.Path("Activities").exists():
|
97
|
+
import_from_directory(
|
98
|
+
repository,
|
99
|
+
config.get("metadata_extraction_regexes", []),
|
100
|
+
)
|
101
|
+
if pathlib.Path("Strava Export").exists():
|
102
|
+
import_from_strava_checkout(repository)
|
103
|
+
if "strava" in config and not skip_strava:
|
104
|
+
import_from_strava_api(repository)
|
105
|
+
|
106
|
+
if len(repository) == 0:
|
107
|
+
logger.error(
|
108
|
+
f"No activities found. You need to either add activity files (GPX, FIT, …) to {pathlib.Path('Activities')} or set up the Strava API. Starting without any activities is unfortunately not supported."
|
109
|
+
)
|
110
|
+
sys.exit(1)
|
111
|
+
|
112
|
+
embellish_time_series(repository)
|
113
|
+
compute_tile_visits(repository, tile_visit_accessor)
|
114
|
+
compute_tile_evolution(tile_visit_accessor)
|
{geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/RECORD
RENAMED
@@ -1,38 +1,38 @@
|
|
1
1
|
geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
geo_activity_playground/__main__.py,sha256=
|
2
|
+
geo_activity_playground/__main__.py,sha256=ssK4XW5RsyV_guZEVslRgOr-eYW3mKp29rOc8ZWwfqg,4131
|
3
3
|
geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
geo_activity_playground/core/activities.py,sha256
|
5
|
-
geo_activity_playground/core/activity_parsers.py,sha256=
|
4
|
+
geo_activity_playground/core/activities.py,sha256=gxn0-qU4HwuAFDChAUBylc_zf5GcyTwP2DrktgCW8ME,11357
|
5
|
+
geo_activity_playground/core/activity_parsers.py,sha256=2j6QZHjDYJR_w0baYUHHlzSVI1v1fOcCqdRuAkRHfhA,11887
|
6
6
|
geo_activity_playground/core/cache_migrations.py,sha256=cz7zwoYtjAcFbUQee1UqeyHT0K2oiyfpPVh5tXkzk0U,3479
|
7
7
|
geo_activity_playground/core/config.py,sha256=YjqCiEmIAa-GM1-JfBctMEsl8-I56pZyyDdTyPduOzw,477
|
8
8
|
geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
|
9
|
-
geo_activity_playground/core/heatmap.py,sha256=
|
9
|
+
geo_activity_playground/core/heatmap.py,sha256=EW_k_ob7NyQr2Yebe-S0miBRaWmQRsqTTrbCQB9kNrQ,4423
|
10
10
|
geo_activity_playground/core/paths.py,sha256=EX4yQQyAmWRxTYS0QvFCbGz9v1J0z_1QYfd5hjTmaRw,808
|
11
11
|
geo_activity_playground/core/similarity.py,sha256=GUBYIpQEe8AgVRNwas0UfmxoK0r0oWg_VEzLeEwxMO0,3153
|
12
12
|
geo_activity_playground/core/tasks.py,sha256=lcfLeYpla819v2BTf53zvT0xEB4I6OpSfqfF_xlsLwM,1817
|
13
13
|
geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
|
14
14
|
geo_activity_playground/core/tiles.py,sha256=VxPu9vdfKnxDxaYo5JSYmka9Dt3TDxg0zo3cYVJXVHc,3359
|
15
15
|
geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
geo_activity_playground/explorer/grid_file.py,sha256=
|
17
|
-
geo_activity_playground/explorer/tile_visits.py,sha256=
|
18
|
-
geo_activity_playground/explorer/video.py,sha256=
|
19
|
-
geo_activity_playground/importers/directory.py,sha256=
|
20
|
-
geo_activity_playground/importers/strava_api.py,sha256=
|
21
|
-
geo_activity_playground/importers/strava_checkout.py,sha256=
|
16
|
+
geo_activity_playground/explorer/grid_file.py,sha256=k6j6KBEk2a2BY-onE8SV5TJsERGGyOrlY4as__meWpA,3304
|
17
|
+
geo_activity_playground/explorer/tile_visits.py,sha256=698V43KsmSx_VZPThHqQ9XkvVwbULQu_qGydu0FVo0U,11495
|
18
|
+
geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVpEWenIq4hMCRM,4389
|
19
|
+
geo_activity_playground/importers/directory.py,sha256=9jaryMpxUYA_mUdcHs79CvSsAATkQ7ZwnZpVuOXgesE,4321
|
20
|
+
geo_activity_playground/importers/strava_api.py,sha256=iCxhDykJIazNdxm7EyNNZXuheddUZTYN6BSvAd9k-38,7602
|
21
|
+
geo_activity_playground/importers/strava_checkout.py,sha256=4C5yvo3-f9kQF1y_8rSq4dxWnReRXBLYuCz6L4_pWMs,8008
|
22
22
|
geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQYEmZJUDVv3OOF5opklxdT4,724
|
23
23
|
geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
|
24
24
|
geo_activity_playground/webui/activity_controller.py,sha256=yM7-EPcoKen-e9mt4D7UWo-E7Zk7Y39_-kr2IV7Km6U,12515
|
25
|
-
geo_activity_playground/webui/app.py,sha256=
|
26
|
-
geo_activity_playground/webui/calendar_controller.py,sha256=
|
25
|
+
geo_activity_playground/webui/app.py,sha256=nimykpmtEeiQh3-wJ-QNbmute1e3J1c3DFmDRW45Qkk,12556
|
26
|
+
geo_activity_playground/webui/calendar_controller.py,sha256=gRc0KjJ5r7dEZeiU1JHnJ-f_MBNlhRev2EZyqlHybko,2754
|
27
27
|
geo_activity_playground/webui/config_controller.py,sha256=4M8mQc58Hkm-ssfYF1gKRepiAXFIzkZdIMRSbX-aI1U,320
|
28
|
-
geo_activity_playground/webui/eddington_controller.py,sha256=
|
29
|
-
geo_activity_playground/webui/entry_controller.py,sha256=
|
30
|
-
geo_activity_playground/webui/equipment_controller.py,sha256=
|
31
|
-
geo_activity_playground/webui/explorer_controller.py,sha256=
|
32
|
-
geo_activity_playground/webui/heatmap_controller.py,sha256=
|
28
|
+
geo_activity_playground/webui/eddington_controller.py,sha256=86HISbRxmnkiCxclVacYzUe-PLgQ9Db7YuVN6F0a33M,2607
|
29
|
+
geo_activity_playground/webui/entry_controller.py,sha256=AbF17UA7ySKV5WXsF6GlGCLpQiH97TmG2RPHM0XdW4U,1899
|
30
|
+
geo_activity_playground/webui/equipment_controller.py,sha256=mDFQutTLE8H4w29rcfv-TDMXb448fRGEuVuKLR4ZQlM,2290
|
31
|
+
geo_activity_playground/webui/explorer_controller.py,sha256=8d0FFrG55ZlPQ5seQC2DegxIkPGIW7pvw8Jm4eJt3fg,10634
|
32
|
+
geo_activity_playground/webui/heatmap_controller.py,sha256=BDH8J3-RbQY0MPwsgkkYpS1o4RgfJVMGgKASAjowt88,5428
|
33
33
|
geo_activity_playground/webui/locations_controller.py,sha256=xTIm-MN0B_0TDN6_J13HCkCnKLhipjYvlNWynGEpWuE,950
|
34
34
|
geo_activity_playground/webui/search_controller.py,sha256=WG5TTVDFBqamrWKHbiEoZXo-73WqO5SynLAoDeU8o2A,859
|
35
|
-
geo_activity_playground/webui/square_planner_controller.py,sha256=
|
35
|
+
geo_activity_playground/webui/square_planner_controller.py,sha256=wYcNEviDgqyYxSrnwMD_5LnYXIazVH9plGX8RxG6oco,3464
|
36
36
|
geo_activity_playground/webui/static/android-chrome-192x192.png,sha256=yxZgo8Jw4hrgOgrn3tvi9G0AXWGFD29kjCuxC07WoT4,17610
|
37
37
|
geo_activity_playground/webui/static/android-chrome-384x384.png,sha256=bgeqAdyvDZBMch7rVi3qSawf0Zr4Go0EG8Ws_B8NApY,49297
|
38
38
|
geo_activity_playground/webui/static/android-chrome-512x512.png,sha256=Uiv62gQpUjMOdp9d6exzd6IyOi5zgQdgjIVVWYw5m98,38891
|
@@ -51,7 +51,7 @@ geo_activity_playground/webui/templates/activity-lines.html.j2,sha256=5gB1aDjRgi
|
|
51
51
|
geo_activity_playground/webui/templates/activity-name.html.j2,sha256=opHCj_zY3Xz1l3jIXUQvVdxBNi_D9C-Mdnbx2nQqTTQ,2381
|
52
52
|
geo_activity_playground/webui/templates/activity.html.j2,sha256=ncj0K1471nRHtbBL9fqhGZ7a9DoJLyqt6ckX8rnhS28,4946
|
53
53
|
geo_activity_playground/webui/templates/calendar-month.html.j2,sha256=rV96gOXS0eZU3Dokg8Wb7AJVXJvTPsw61OJoj8lRvt4,1767
|
54
|
-
geo_activity_playground/webui/templates/calendar.html.j2,sha256=
|
54
|
+
geo_activity_playground/webui/templates/calendar.html.j2,sha256=kJjJ0s0dSYV2DjtiUrcbfjTlR02qiHO7VoQbLgIx7Bo,1375
|
55
55
|
geo_activity_playground/webui/templates/config.html.j2,sha256=pmec-TqSl5CVznQlyHuC91o18qa0ZQWHXxSBrlV4au4,796
|
56
56
|
geo_activity_playground/webui/templates/eddington.html.j2,sha256=yl75IzWeIkFpwPj8FjTrzJsz_f-qdETPmNnAGLPJuL8,487
|
57
57
|
geo_activity_playground/webui/templates/equipment.html.j2,sha256=dNuezVKJNJZbQ0y2-AYlXNHpncTbtOSDNLqNswRTxaI,1320
|
@@ -59,14 +59,16 @@ geo_activity_playground/webui/templates/explorer.html.j2,sha256=ORaBlDxqg9YTxnOC
|
|
59
59
|
geo_activity_playground/webui/templates/heatmap.html.j2,sha256=VQZBLv5Rw6QAGosA50darHdpOjSUsYfx7HkWhqt0YnQ,1208
|
60
60
|
geo_activity_playground/webui/templates/index.html.j2,sha256=5eDy9F9HGGAZRZB9siFeqgkrGS86ZoV4JvM8ahU739M,2163
|
61
61
|
geo_activity_playground/webui/templates/locations.html.j2,sha256=uexZpLjd9bm3gt3YZQA2WTPlwln7TuOc6OkIOOfJ6ik,1060
|
62
|
-
geo_activity_playground/webui/templates/page.html.j2,sha256=
|
62
|
+
geo_activity_playground/webui/templates/page.html.j2,sha256=SVlT0gs6CvDTMTjMlGquxjivROf5DOaIT_3-pwpRtv8,7118
|
63
63
|
geo_activity_playground/webui/templates/search.html.j2,sha256=1UGCQU49oQgSJUB1OJ2kgAOuFMPVp0XXAQtmUnffzBI,1066
|
64
64
|
geo_activity_playground/webui/templates/square-planner.html.j2,sha256=aIB0ql5qW4HXfp0ENksYYOk9vTgBitwyHJX5W7bqkeY,6512
|
65
65
|
geo_activity_playground/webui/templates/strava-connect.html.j2,sha256=vLMqTnTV-DZJ1FHRjpm4OMgbABMwZQvbs8Ru9baKeBg,1111
|
66
66
|
geo_activity_playground/webui/templates/summary.html.j2,sha256=eEwcPOURJ-uT89jeJGZHq_5pSq56_fTC7z-j_m5nQiA,471
|
67
|
+
geo_activity_playground/webui/templates/upload.html.j2,sha256=BodcWv4PaiooAyyNRuLeNQEzclk_eRETZIBQhMO6ACw,1461
|
67
68
|
geo_activity_playground/webui/tile_controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
|
68
|
-
geo_activity_playground
|
69
|
-
geo_activity_playground-0.
|
70
|
-
geo_activity_playground-0.
|
71
|
-
geo_activity_playground-0.
|
72
|
-
geo_activity_playground-0.
|
69
|
+
geo_activity_playground/webui/upload_controller.py,sha256=V8Lksi7pHhLXg3TinzxyBTt8fwoG5DO5y7dZi0fY9iI,4056
|
70
|
+
geo_activity_playground-0.23.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
|
71
|
+
geo_activity_playground-0.23.0.dist-info/METADATA,sha256=wWjwOEawy_aLmKLALINY0vhszfp1c0WIe56YMmkwET8,1625
|
72
|
+
geo_activity_playground-0.23.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
73
|
+
geo_activity_playground-0.23.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
|
74
|
+
geo_activity_playground-0.23.0.dist-info/RECORD,,
|
{geo_activity_playground-0.21.2.dist-info → geo_activity_playground-0.23.0.dist-info}/LICENSE
RENAMED
File without changes
|
File without changes
|
File without changes
|