geo-activity-playground 0.20.0__py3-none-any.whl → 0.21.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 +16 -3
- geo_activity_playground/core/activities.py +47 -29
- geo_activity_playground/core/activity_parsers.py +4 -0
- geo_activity_playground/importers/directory.py +61 -37
- geo_activity_playground/importers/strava_checkout.py +10 -1
- geo_activity_playground/importers/test_directory.py +22 -0
- geo_activity_playground/webui/activity_controller.py +180 -12
- geo_activity_playground/webui/app.py +30 -3
- geo_activity_playground/webui/calendar_controller.py +8 -4
- geo_activity_playground/webui/templates/activity-day.html.j2 +71 -0
- geo_activity_playground/webui/templates/activity-lines.html.j2 +36 -0
- geo_activity_playground/webui/templates/activity-name.html.j2 +81 -0
- geo_activity_playground/webui/templates/activity.html.j2 +36 -12
- geo_activity_playground/webui/templates/calendar-month.html.j2 +7 -0
- geo_activity_playground/webui/tile_controller.py +10 -0
- {geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/METADATA +1 -2
- {geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/RECORD +20 -16
- {geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/WHEEL +1 -1
- {geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,6 @@ import sys
|
|
6
6
|
|
7
7
|
import coloredlogs
|
8
8
|
|
9
|
-
from .core.similarity import precompute_activity_distances
|
10
9
|
from .importers.strava_checkout import convert_strava_checkout
|
11
10
|
from .importers.strava_checkout import import_from_strava_checkout
|
12
11
|
from geo_activity_playground.core.activities import ActivityRepository
|
@@ -105,17 +104,31 @@ def make_activity_repository(
|
|
105
104
|
apply_cache_migrations()
|
106
105
|
config = get_config()
|
107
106
|
|
107
|
+
if not config.get("prefer_metadata_from_file", True):
|
108
|
+
logger.error(
|
109
|
+
"The config option `prefer_metadata_from_file` is deprecated. If you want to prefer extract metadata from the activity file paths, please use the new `metadata_extraction_regexes` as explained at https://martin-ueding.github.io/geo-activity-playground/getting-started/using-activity-files/#directory-structure."
|
110
|
+
)
|
111
|
+
sys.exit(1)
|
112
|
+
|
108
113
|
repository = ActivityRepository()
|
109
114
|
|
110
115
|
if pathlib.Path("Activities").exists():
|
111
|
-
import_from_directory(
|
116
|
+
import_from_directory(
|
117
|
+
repository,
|
118
|
+
config.get("metadata_extraction_regexes", []),
|
119
|
+
)
|
112
120
|
if pathlib.Path("Strava Export").exists():
|
113
121
|
import_from_strava_checkout(repository)
|
114
122
|
if "strava" in config and not skip_strava:
|
115
123
|
import_from_strava_api(repository)
|
116
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)
|
130
|
+
|
117
131
|
embellish_time_series(repository)
|
118
|
-
precompute_activity_distances(repository)
|
119
132
|
compute_tile_visits(repository)
|
120
133
|
compute_tile_evolution()
|
121
134
|
return repository
|
@@ -37,6 +37,7 @@ class ActivityMeta(TypedDict):
|
|
37
37
|
start_latitude: float
|
38
38
|
start_longitude: float
|
39
39
|
start: datetime.datetime
|
40
|
+
steps: int
|
40
41
|
|
41
42
|
|
42
43
|
class ActivityRepository:
|
@@ -163,9 +164,6 @@ def embellish_single_time_series(
|
|
163
164
|
timeseries: pd.DataFrame, start: Optional[datetime.datetime] = None
|
164
165
|
) -> bool:
|
165
166
|
changed = False
|
166
|
-
time_diff_threshold_seconds = 30
|
167
|
-
time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
|
168
|
-
jump_indices = time_diff >= time_diff_threshold_seconds
|
169
167
|
|
170
168
|
if start is not None and pd.api.types.is_dtype_equal(
|
171
169
|
timeseries["time"].dtype, "int64"
|
@@ -176,31 +174,37 @@ def embellish_single_time_series(
|
|
176
174
|
changed = True
|
177
175
|
assert pd.api.types.is_dtype_equal(timeseries["time"].dtype, "datetime64[ns, UTC]")
|
178
176
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
177
|
+
distances = get_distance(
|
178
|
+
timeseries["latitude"].shift(1),
|
179
|
+
timeseries["longitude"].shift(1),
|
180
|
+
timeseries["latitude"],
|
181
|
+
timeseries["longitude"],
|
182
|
+
).fillna(0.0)
|
183
|
+
time_diff_threshold_seconds = 30
|
184
|
+
time_diff = (timeseries["time"] - timeseries["time"].shift(1)).dt.total_seconds()
|
185
|
+
jump_indices = (time_diff >= time_diff_threshold_seconds) & (distances > 100)
|
186
|
+
distances.loc[jump_indices] = 0.0
|
187
|
+
|
188
|
+
if not "distance_km" in timeseries.columns:
|
188
189
|
timeseries["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
|
189
190
|
changed = True
|
190
191
|
|
191
|
-
if "
|
192
|
-
|
193
|
-
timeseries["
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
changed = True
|
192
|
+
if "speed" not in timeseries.columns:
|
193
|
+
timeseries["speed"] = (
|
194
|
+
timeseries["distance_km"].diff()
|
195
|
+
/ (timeseries["time"].diff().dt.total_seconds() + 1e-3)
|
196
|
+
* 3600
|
197
|
+
)
|
198
|
+
changed = True
|
199
199
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
200
|
+
potential_jumps = (timeseries["speed"] > 40) & (timeseries["speed"].diff() > 10)
|
201
|
+
if np.any(potential_jumps):
|
202
|
+
timeseries = timeseries.loc[~potential_jumps].copy()
|
203
|
+
changed = True
|
204
|
+
|
205
|
+
if "segment_id" not in timeseries.columns:
|
206
|
+
timeseries["segment_id"] = np.cumsum(jump_indices)
|
207
|
+
changed = True
|
204
208
|
|
205
209
|
if "x" not in timeseries.columns:
|
206
210
|
x, y = compute_tile_float(timeseries["latitude"], timeseries["longitude"], 0)
|
@@ -208,10 +212,6 @@ def embellish_single_time_series(
|
|
208
212
|
timeseries["y"] = y
|
209
213
|
changed = True
|
210
214
|
|
211
|
-
if "segment_id" not in timeseries.columns:
|
212
|
-
timeseries["segment_id"] = np.cumsum(jump_indices)
|
213
|
-
changed = True
|
214
|
-
|
215
215
|
return timeseries, changed
|
216
216
|
|
217
217
|
|
@@ -228,6 +228,11 @@ def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
|
|
228
228
|
|
229
229
|
|
230
230
|
def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
231
|
+
speed_without_na = time_series["speed"].dropna()
|
232
|
+
low = min(speed_without_na)
|
233
|
+
high = max(speed_without_na)
|
234
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
235
|
+
|
231
236
|
cmap = matplotlib.colormaps["viridis"]
|
232
237
|
features = [
|
233
238
|
geojson.Feature(
|
@@ -239,7 +244,7 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
|
239
244
|
),
|
240
245
|
properties={
|
241
246
|
"speed": next["speed"] if np.isfinite(next["speed"]) else 0.0,
|
242
|
-
"color": matplotlib.colors.to_hex(cmap(
|
247
|
+
"color": matplotlib.colors.to_hex(cmap(clamp_speed(next["speed"]))),
|
243
248
|
},
|
244
249
|
)
|
245
250
|
for _, group in time_series.groupby("segment_id")
|
@@ -249,6 +254,19 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
|
|
249
254
|
return geojson.dumps(feature_collection)
|
250
255
|
|
251
256
|
|
257
|
+
def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
|
258
|
+
speed_without_na = time_series["speed"].dropna()
|
259
|
+
low = min(speed_without_na)
|
260
|
+
high = max(speed_without_na)
|
261
|
+
cmap = matplotlib.colormaps["viridis"]
|
262
|
+
clamp_speed = lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
|
263
|
+
colors = [
|
264
|
+
(f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
|
265
|
+
for speed in np.linspace(low, high, 10)
|
266
|
+
]
|
267
|
+
return {"low": low, "high": high, "colors": colors}
|
268
|
+
|
269
|
+
|
252
270
|
def extract_heart_rate_zones(time_series: pd.DataFrame) -> Optional[pd.DataFrame]:
|
253
271
|
if "heartrate" not in time_series:
|
254
272
|
return None
|
@@ -168,6 +168,10 @@ def read_fit_activity(path: pathlib.Path, open) -> tuple[ActivityMeta, pd.DataFr
|
|
168
168
|
metadata["kind"] = str(values["sport"])
|
169
169
|
if "sub_sport" in values:
|
170
170
|
metadata["kind"] += " " + str(values["sub_sport"])
|
171
|
+
if "total_calories" in fields:
|
172
|
+
metadata["calories"] = values["total_calories"]
|
173
|
+
if "total_strides" in fields:
|
174
|
+
metadata["steps"] = 2 * int(values["total_strides"])
|
171
175
|
|
172
176
|
return metadata, pd.DataFrame(rows)
|
173
177
|
|
@@ -1,9 +1,12 @@
|
|
1
1
|
import hashlib
|
2
2
|
import logging
|
3
|
+
import multiprocessing
|
3
4
|
import pathlib
|
4
5
|
import pickle
|
6
|
+
import re
|
5
7
|
import sys
|
6
8
|
import traceback
|
9
|
+
from typing import Optional
|
7
10
|
|
8
11
|
import pandas as pd
|
9
12
|
from tqdm import tqdm
|
@@ -16,16 +19,18 @@ from geo_activity_playground.core.tasks import WorkTracker
|
|
16
19
|
|
17
20
|
logger = logging.getLogger(__name__)
|
18
21
|
|
22
|
+
ACTIVITY_DIR = pathlib.Path("Activities")
|
23
|
+
|
19
24
|
|
20
25
|
def import_from_directory(
|
21
|
-
repository: ActivityRepository,
|
26
|
+
repository: ActivityRepository, metadata_extraction_regexes: list[str] = []
|
22
27
|
) -> None:
|
23
28
|
paths_with_errors = []
|
24
29
|
work_tracker = WorkTracker("parse-activity-files")
|
25
30
|
|
26
31
|
activity_paths = [
|
27
32
|
path
|
28
|
-
for path in
|
33
|
+
for path in ACTIVITY_DIR.rglob("*.*")
|
29
34
|
if path.is_file() and path.suffixes and not path.stem.startswith(".")
|
30
35
|
]
|
31
36
|
new_activity_paths = work_tracker.filter(activity_paths)
|
@@ -35,36 +40,26 @@ def import_from_directory(
|
|
35
40
|
file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
|
36
41
|
file_metadata_dir.mkdir(exist_ok=True, parents=True)
|
37
42
|
|
38
|
-
|
43
|
+
with multiprocessing.Pool() as pool:
|
44
|
+
paths_with_errors = tqdm(
|
45
|
+
pool.imap(_cache_single_file, new_activity_paths),
|
46
|
+
desc="Parse activity metadata",
|
47
|
+
total=len(new_activity_paths),
|
48
|
+
)
|
49
|
+
paths_with_errors = [error for error in paths_with_errors if error]
|
50
|
+
|
51
|
+
for path in tqdm(new_activity_paths, desc="Collate activity metadata"):
|
39
52
|
activity_id = _get_file_hash(path)
|
40
|
-
timeseries_path = activity_stream_dir / f"{activity_id}.parquet"
|
41
53
|
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
42
|
-
work_tracker.mark_done(
|
43
|
-
|
44
|
-
if not
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
traceback.print_exc()
|
50
|
-
paths_with_errors.append((path, str(e)))
|
51
|
-
continue
|
52
|
-
except:
|
53
|
-
logger.error(f"Encountered a problem with {path=}, see details below.")
|
54
|
-
raise
|
55
|
-
|
56
|
-
if len(timeseries) == 0:
|
57
|
-
continue
|
58
|
-
|
59
|
-
timeseries.to_parquet(timeseries_path)
|
60
|
-
with open(file_metadata_path, "wb") as f:
|
61
|
-
pickle.dump(activity_meta_from_file, f)
|
62
|
-
else:
|
63
|
-
with open(file_metadata_path, "rb") as f:
|
64
|
-
activity_meta_from_file = pickle.load(f)
|
54
|
+
work_tracker.mark_done(activity_id)
|
55
|
+
|
56
|
+
if not file_metadata_path.exists():
|
57
|
+
continue
|
58
|
+
|
59
|
+
with open(file_metadata_path, "rb") as f:
|
60
|
+
activity_meta_from_file = pickle.load(f)
|
65
61
|
|
66
62
|
activity_meta = ActivityMeta(
|
67
|
-
commute=path.parts[-2] == "Commute",
|
68
63
|
id=activity_id,
|
69
64
|
# https://stackoverflow.com/a/74718395/653152
|
70
65
|
name=path.name.removesuffix("".join(path.suffixes)),
|
@@ -72,15 +67,8 @@ def import_from_directory(
|
|
72
67
|
kind="Unknown",
|
73
68
|
equipment="Unknown",
|
74
69
|
)
|
75
|
-
|
76
|
-
|
77
|
-
if len(path.parts) >= 4 and path.parts[2] != "Commute":
|
78
|
-
activity_meta["equipment"] = path.parts[2]
|
79
|
-
|
80
|
-
if prefer_metadata_from_file:
|
81
|
-
activity_meta = {**activity_meta, **activity_meta_from_file}
|
82
|
-
else:
|
83
|
-
activity_meta = {**activity_meta_from_file, **activity_meta}
|
70
|
+
activity_meta.update(activity_meta_from_file)
|
71
|
+
activity_meta.update(_get_metadata_from_path(path, metadata_extraction_regexes))
|
84
72
|
repository.add_activity(activity_meta)
|
85
73
|
|
86
74
|
if paths_with_errors:
|
@@ -95,9 +83,45 @@ def import_from_directory(
|
|
95
83
|
work_tracker.close()
|
96
84
|
|
97
85
|
|
86
|
+
def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]:
|
87
|
+
activity_stream_dir = pathlib.Path("Cache/Activity Timeseries")
|
88
|
+
file_metadata_dir = pathlib.Path("Cache/Activity Metadata")
|
89
|
+
|
90
|
+
activity_id = _get_file_hash(path)
|
91
|
+
timeseries_path = activity_stream_dir / f"{activity_id}.parquet"
|
92
|
+
file_metadata_path = file_metadata_dir / f"{activity_id}.pickle"
|
93
|
+
|
94
|
+
if not timeseries_path.exists():
|
95
|
+
try:
|
96
|
+
activity_meta_from_file, timeseries = read_activity(path)
|
97
|
+
except ActivityParseError as e:
|
98
|
+
logger.error(f"Error while parsing file {path}:")
|
99
|
+
traceback.print_exc()
|
100
|
+
return (path, str(e))
|
101
|
+
except:
|
102
|
+
logger.error(f"Encountered a problem with {path=}, see details below.")
|
103
|
+
raise
|
104
|
+
|
105
|
+
if len(timeseries) == 0:
|
106
|
+
return
|
107
|
+
|
108
|
+
timeseries.to_parquet(timeseries_path)
|
109
|
+
with open(file_metadata_path, "wb") as f:
|
110
|
+
pickle.dump(activity_meta_from_file, f)
|
111
|
+
|
112
|
+
|
98
113
|
def _get_file_hash(path: pathlib.Path) -> int:
|
99
114
|
file_hash = hashlib.blake2s()
|
100
115
|
with open(path, "rb") as f:
|
101
116
|
while chunk := f.read(8192):
|
102
117
|
file_hash.update(chunk)
|
103
118
|
return int(file_hash.hexdigest(), 16) % 2**62
|
119
|
+
|
120
|
+
|
121
|
+
def _get_metadata_from_path(
|
122
|
+
path: pathlib.Path, metadata_extraction_regexes: list[str]
|
123
|
+
) -> dict[str, str]:
|
124
|
+
for regex in metadata_extraction_regexes:
|
125
|
+
if m := re.search(regex, str(path.relative_to(ACTIVITY_DIR))):
|
126
|
+
return m.groupdict()
|
127
|
+
return {}
|
@@ -3,6 +3,8 @@ import logging
|
|
3
3
|
import pathlib
|
4
4
|
import shutil
|
5
5
|
import traceback
|
6
|
+
from typing import Optional
|
7
|
+
from typing import Union
|
6
8
|
|
7
9
|
import dateutil.parser
|
8
10
|
import numpy as np
|
@@ -116,6 +118,13 @@ EXPECTED_COLUMNS = [
|
|
116
118
|
]
|
117
119
|
|
118
120
|
|
121
|
+
def float_or_none(x: Union[float, str]) -> Optional[float]:
|
122
|
+
try:
|
123
|
+
return float(x)
|
124
|
+
except ValueError:
|
125
|
+
return None
|
126
|
+
|
127
|
+
|
119
128
|
def import_from_strava_checkout(repository: ActivityRepository) -> None:
|
120
129
|
checkout_path = pathlib.Path("Strava Export")
|
121
130
|
activities = pd.read_csv(checkout_path / "activities.csv")
|
@@ -143,7 +152,7 @@ def import_from_strava_checkout(repository: ActivityRepository) -> None:
|
|
143
152
|
row = activities.loc[activity_id]
|
144
153
|
activity_file = checkout_path / row["Filename"]
|
145
154
|
table_activity_meta = {
|
146
|
-
"calories": row["Calories"],
|
155
|
+
"calories": float_or_none(row["Calories"]),
|
147
156
|
"commute": row["Commute"] == "true",
|
148
157
|
"distance_km": row["Distance"],
|
149
158
|
"elapsed_time": datetime.timedelta(seconds=int(row["Elapsed Time"])),
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import pathlib
|
2
|
+
|
3
|
+
from geo_activity_playground.importers.directory import _get_metadata_from_path
|
4
|
+
|
5
|
+
|
6
|
+
def test_get_metadata_from_path() -> None:
|
7
|
+
expected = {
|
8
|
+
"kind": "Radfahrt",
|
9
|
+
"equipment": "Bike 2019",
|
10
|
+
"name": "Foo-Bar to Baz24",
|
11
|
+
}
|
12
|
+
actual = _get_metadata_from_path(
|
13
|
+
pathlib.Path(
|
14
|
+
"Activities/Radfahrt/Bike 2019/Foo-Bar to Baz24/2024-03-03-17-42-10 Something something.fit"
|
15
|
+
),
|
16
|
+
[
|
17
|
+
r"(?P<kind>[^/]+)/(?P<equipment>[^/]+)/(?P<name>[^/]+)/",
|
18
|
+
r"(?P<kind>[^/]+)/(?P<equipment>[^/]+)/[-\d_ ]+(?P<name>[^/\.]+)(?:\.\w+)+$",
|
19
|
+
r"(?P<kind>[^/]+)/[-\d_ ]+(?P<name>[^/\.]+)(?:\.\w+)+$",
|
20
|
+
],
|
21
|
+
)
|
22
|
+
assert actual == expected
|
@@ -1,8 +1,12 @@
|
|
1
|
+
import datetime
|
1
2
|
import functools
|
2
3
|
import io
|
4
|
+
import logging
|
3
5
|
import pickle
|
4
6
|
|
5
7
|
import altair as alt
|
8
|
+
import geojson
|
9
|
+
import matplotlib
|
6
10
|
import matplotlib.pyplot as pl
|
7
11
|
import numpy as np
|
8
12
|
import pandas as pd
|
@@ -13,15 +17,17 @@ from geo_activity_playground.core.activities import ActivityRepository
|
|
13
17
|
from geo_activity_playground.core.activities import extract_heart_rate_zones
|
14
18
|
from geo_activity_playground.core.activities import make_geojson_color_line
|
15
19
|
from geo_activity_playground.core.activities import make_geojson_from_time_series
|
20
|
+
from geo_activity_playground.core.activities import make_speed_color_bar
|
16
21
|
from geo_activity_playground.core.heatmap import add_margin_to_geo_bounds
|
17
22
|
from geo_activity_playground.core.heatmap import build_map_from_tiles
|
18
23
|
from geo_activity_playground.core.heatmap import crop_image_to_bounds
|
19
24
|
from geo_activity_playground.core.heatmap import get_bounds
|
20
25
|
from geo_activity_playground.core.heatmap import get_sensible_zoom_level
|
21
26
|
from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
|
22
|
-
from geo_activity_playground.core.similarity import distances_path
|
23
27
|
from geo_activity_playground.core.tiles import compute_tile_float
|
24
28
|
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
25
31
|
|
26
32
|
class ActivityController:
|
27
33
|
def __init__(self, repository: ActivityRepository) -> None:
|
@@ -34,16 +40,12 @@ class ActivityController:
|
|
34
40
|
time_series = self._repository.get_time_series(id)
|
35
41
|
line_json = make_geojson_from_time_series(time_series)
|
36
42
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
for activity_id in distances[activity.id].get(distance, set())
|
44
|
-
]
|
45
|
-
for distance in range(10)
|
46
|
-
}
|
43
|
+
meta = self._repository.meta
|
44
|
+
similar_activities = meta.loc[
|
45
|
+
(meta.name == activity["name"]) & (meta.id != activity["id"])
|
46
|
+
]
|
47
|
+
similar_activities = [row for _, row in similar_activities.iterrows()]
|
48
|
+
similar_activities.reverse()
|
47
49
|
|
48
50
|
result = {
|
49
51
|
"activity": activity,
|
@@ -52,7 +54,10 @@ class ActivityController:
|
|
52
54
|
"color_line_geojson": make_geojson_color_line(time_series),
|
53
55
|
"speed_time_plot": speed_time_plot(time_series),
|
54
56
|
"speed_distribution_plot": speed_distribution_plot(time_series),
|
55
|
-
"similar_activites":
|
57
|
+
"similar_activites": similar_activities,
|
58
|
+
"speed_color_bar": make_speed_color_bar(time_series),
|
59
|
+
"date": activity.start.date(),
|
60
|
+
"time": activity.start.time(),
|
56
61
|
}
|
57
62
|
if (heart_zones := extract_heart_rate_zones(time_series)) is not None:
|
58
63
|
result["heart_zones_plot"] = heartrate_zone_plot(heart_zones)
|
@@ -66,6 +71,124 @@ class ActivityController:
|
|
66
71
|
time_series = self._repository.get_time_series(id)
|
67
72
|
return make_sharepic(time_series)
|
68
73
|
|
74
|
+
def render_day(self, year: int, month: int, day: int) -> dict:
|
75
|
+
meta = self._repository.meta
|
76
|
+
selection = meta["start"].dt.date == datetime.date(year, month, day)
|
77
|
+
activities_that_day = meta.loc[selection]
|
78
|
+
|
79
|
+
time_series = [
|
80
|
+
self._repository.get_time_series(activity_id)
|
81
|
+
for activity_id in activities_that_day["id"]
|
82
|
+
]
|
83
|
+
|
84
|
+
cmap = matplotlib.colormaps["Dark2"]
|
85
|
+
fc = geojson.FeatureCollection(
|
86
|
+
features=[
|
87
|
+
geojson.Feature(
|
88
|
+
geometry=geojson.MultiLineString(
|
89
|
+
coordinates=[
|
90
|
+
[
|
91
|
+
[lon, lat]
|
92
|
+
for lat, lon in zip(
|
93
|
+
group["latitude"], group["longitude"]
|
94
|
+
)
|
95
|
+
]
|
96
|
+
for _, group in ts.groupby("segment_id")
|
97
|
+
]
|
98
|
+
),
|
99
|
+
properties={"color": matplotlib.colors.to_hex(cmap(i % 8))},
|
100
|
+
)
|
101
|
+
for i, ts in enumerate(time_series)
|
102
|
+
]
|
103
|
+
)
|
104
|
+
|
105
|
+
activities_list = activities_that_day.to_dict(orient="records")
|
106
|
+
for i, activity_record in enumerate(activities_list):
|
107
|
+
activity_record["color"] = matplotlib.colors.to_hex(cmap(i % 8))
|
108
|
+
|
109
|
+
return {
|
110
|
+
"activities": activities_list,
|
111
|
+
"geojson": geojson.dumps(fc),
|
112
|
+
"date": datetime.date(year, month, day).isoformat(),
|
113
|
+
}
|
114
|
+
|
115
|
+
def render_all(self) -> dict:
|
116
|
+
cmap = matplotlib.colormaps["Dark2"]
|
117
|
+
fc = geojson.FeatureCollection(
|
118
|
+
features=[
|
119
|
+
geojson.Feature(
|
120
|
+
geometry=geojson.MultiLineString(
|
121
|
+
coordinates=[
|
122
|
+
[
|
123
|
+
[lon, lat]
|
124
|
+
for lat, lon in zip(
|
125
|
+
group["latitude"], group["longitude"]
|
126
|
+
)
|
127
|
+
]
|
128
|
+
for _, group in self._repository.get_time_series(
|
129
|
+
activity["id"]
|
130
|
+
).groupby("segment_id")
|
131
|
+
]
|
132
|
+
),
|
133
|
+
properties={
|
134
|
+
"color": matplotlib.colors.to_hex(cmap(i % 8)),
|
135
|
+
"activity_name": activity["name"],
|
136
|
+
"activity_id": str(activity["id"]),
|
137
|
+
},
|
138
|
+
)
|
139
|
+
for i, activity in enumerate(self._repository.iter_activities())
|
140
|
+
]
|
141
|
+
)
|
142
|
+
|
143
|
+
return {
|
144
|
+
"geojson": geojson.dumps(fc),
|
145
|
+
}
|
146
|
+
|
147
|
+
def render_name(self, name: str) -> dict:
|
148
|
+
meta = self._repository.meta
|
149
|
+
selection = meta["name"] == name
|
150
|
+
activities_with_name = meta.loc[selection]
|
151
|
+
|
152
|
+
time_series = [
|
153
|
+
self._repository.get_time_series(activity_id)
|
154
|
+
for activity_id in activities_with_name["id"]
|
155
|
+
]
|
156
|
+
|
157
|
+
cmap = matplotlib.colormaps["Dark2"]
|
158
|
+
fc = geojson.FeatureCollection(
|
159
|
+
features=[
|
160
|
+
geojson.Feature(
|
161
|
+
geometry=geojson.MultiLineString(
|
162
|
+
coordinates=[
|
163
|
+
[
|
164
|
+
[lon, lat]
|
165
|
+
for lat, lon in zip(
|
166
|
+
group["latitude"], group["longitude"]
|
167
|
+
)
|
168
|
+
]
|
169
|
+
for _, group in ts.groupby("segment_id")
|
170
|
+
]
|
171
|
+
),
|
172
|
+
properties={"color": matplotlib.colors.to_hex(cmap(i % 8))},
|
173
|
+
)
|
174
|
+
for i, ts in enumerate(time_series)
|
175
|
+
]
|
176
|
+
)
|
177
|
+
|
178
|
+
activities_list = activities_with_name.to_dict(orient="records")
|
179
|
+
for i, activity_record in enumerate(activities_list):
|
180
|
+
activity_record["color"] = matplotlib.colors.to_hex(cmap(i % 8))
|
181
|
+
|
182
|
+
return {
|
183
|
+
"activities": activities_list,
|
184
|
+
"geojson": geojson.dumps(fc),
|
185
|
+
"name": name,
|
186
|
+
"tick_plot": name_tick_plot(activities_with_name),
|
187
|
+
"equipment_plot": name_equipment_plot(activities_with_name),
|
188
|
+
"distance_plot": name_distance_plot(activities_with_name),
|
189
|
+
"minutes_plot": name_minutes_plot(activities_with_name),
|
190
|
+
}
|
191
|
+
|
69
192
|
|
70
193
|
def speed_time_plot(time_series: pd.DataFrame) -> str:
|
71
194
|
return (
|
@@ -154,6 +277,51 @@ def heartrate_zone_plot(heart_zones: pd.DataFrame) -> str:
|
|
154
277
|
)
|
155
278
|
|
156
279
|
|
280
|
+
def name_tick_plot(meta: pd.DataFrame) -> str:
|
281
|
+
return (
|
282
|
+
alt.Chart(meta, title="Repetitions")
|
283
|
+
.mark_tick()
|
284
|
+
.encode(
|
285
|
+
alt.X("start", title="Date"),
|
286
|
+
)
|
287
|
+
.to_json(format="vega")
|
288
|
+
)
|
289
|
+
|
290
|
+
|
291
|
+
def name_equipment_plot(meta: pd.DataFrame) -> str:
|
292
|
+
return (
|
293
|
+
alt.Chart(meta, title="Equipment")
|
294
|
+
.mark_bar()
|
295
|
+
.encode(alt.X("count()", title="Count"), alt.Y("equipment", title="Equipment"))
|
296
|
+
.to_json(format="vega")
|
297
|
+
)
|
298
|
+
|
299
|
+
|
300
|
+
def name_distance_plot(meta: pd.DataFrame) -> str:
|
301
|
+
return (
|
302
|
+
alt.Chart(meta, title="Distance")
|
303
|
+
.mark_bar()
|
304
|
+
.encode(
|
305
|
+
alt.X("distance_km", bin=True, title="Distance / km"),
|
306
|
+
alt.Y("count()", title="Count"),
|
307
|
+
)
|
308
|
+
.to_json(format="vega")
|
309
|
+
)
|
310
|
+
|
311
|
+
|
312
|
+
def name_minutes_plot(meta: pd.DataFrame) -> str:
|
313
|
+
minutes = meta["elapsed_time"].dt.total_seconds() / 60
|
314
|
+
return (
|
315
|
+
alt.Chart(pd.DataFrame({"minutes": minutes}), title="Elapsed time")
|
316
|
+
.mark_bar()
|
317
|
+
.encode(
|
318
|
+
alt.X("minutes", bin=True, title="Time / min"),
|
319
|
+
alt.Y("count()", title="Count"),
|
320
|
+
)
|
321
|
+
.to_json(format="vega")
|
322
|
+
)
|
323
|
+
|
324
|
+
|
157
325
|
def make_sharepic(time_series: pd.DataFrame) -> bytes:
|
158
326
|
lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
|
159
327
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import urllib
|
2
|
+
|
1
3
|
from flask import Flask
|
2
4
|
from flask import redirect
|
3
5
|
from flask import render_template
|
@@ -29,6 +31,12 @@ from geo_activity_playground.webui.tile_controller import (
|
|
29
31
|
def route_activity(app: Flask, repository: ActivityRepository) -> None:
|
30
32
|
activity_controller = ActivityController(repository)
|
31
33
|
|
34
|
+
@app.route("/activity/all")
|
35
|
+
def activity_all():
|
36
|
+
return render_template(
|
37
|
+
"activity-lines.html.j2", **activity_controller.render_all()
|
38
|
+
)
|
39
|
+
|
32
40
|
@app.route("/activity/<id>")
|
33
41
|
def activity(id: str):
|
34
42
|
return render_template(
|
@@ -42,6 +50,20 @@ def route_activity(app: Flask, repository: ActivityRepository) -> None:
|
|
42
50
|
mimetype="image/png",
|
43
51
|
)
|
44
52
|
|
53
|
+
@app.route("/activity/day/<year>/<month>/<day>")
|
54
|
+
def activity_day(year: str, month: str, day: str):
|
55
|
+
return render_template(
|
56
|
+
"activity-day.html.j2",
|
57
|
+
**activity_controller.render_day(int(year), int(month), int(day))
|
58
|
+
)
|
59
|
+
|
60
|
+
@app.route("/activity/name/<name>")
|
61
|
+
def activity_name(name: str):
|
62
|
+
return render_template(
|
63
|
+
"activity-name.html.j2",
|
64
|
+
**activity_controller.render_name(urllib.parse.unquote(name))
|
65
|
+
)
|
66
|
+
|
45
67
|
|
46
68
|
def route_calendar(app: Flask, repository: ActivityRepository) -> None:
|
47
69
|
calendar_controller = CalendarController(repository)
|
@@ -53,7 +75,7 @@ def route_calendar(app: Flask, repository: ActivityRepository) -> None:
|
|
53
75
|
)
|
54
76
|
|
55
77
|
@app.route("/calendar/<year>/<month>")
|
56
|
-
def
|
78
|
+
def calendar_month(year: str, month: str):
|
57
79
|
return render_template(
|
58
80
|
"calendar-month.html.j2",
|
59
81
|
**calendar_controller.render_month(int(year), int(month))
|
@@ -264,8 +286,7 @@ def route_tiles(app: Flask, repository: ActivityRepository) -> None:
|
|
264
286
|
@app.route("/tile/color/<z>/<x>/<y>.png")
|
265
287
|
def tile_color(x: str, y: str, z: str):
|
266
288
|
return Response(
|
267
|
-
tile_controller.render_color(int(x), int(y), int(z)),
|
268
|
-
mimetype="image/png",
|
289
|
+
tile_controller.render_color(int(x), int(y), int(z)), mimetype="image/png"
|
269
290
|
)
|
270
291
|
|
271
292
|
@app.route("/tile/grayscale/<z>/<x>/<y>.png")
|
@@ -275,6 +296,12 @@ def route_tiles(app: Flask, repository: ActivityRepository) -> None:
|
|
275
296
|
mimetype="image/png",
|
276
297
|
)
|
277
298
|
|
299
|
+
@app.route("/tile/pastel/<z>/<x>/<y>.png")
|
300
|
+
def tile_pastel(x: str, y: str, z: str):
|
301
|
+
return Response(
|
302
|
+
tile_controller.render_pastel(int(x), int(y), int(z)), mimetype="image/png"
|
303
|
+
)
|
304
|
+
|
278
305
|
|
279
306
|
def webui_main(repository: ActivityRepository, host: str, port: int) -> None:
|
280
307
|
app = Flask(__name__)
|
@@ -2,8 +2,6 @@ import collections
|
|
2
2
|
import datetime
|
3
3
|
import functools
|
4
4
|
|
5
|
-
import pandas as pd
|
6
|
-
|
7
5
|
from geo_activity_playground.core.activities import ActivityRepository
|
8
6
|
|
9
7
|
|
@@ -58,11 +56,12 @@ class CalendarController:
|
|
58
56
|
].sort_values("start")
|
59
57
|
|
60
58
|
weeks = collections.defaultdict(dict)
|
61
|
-
|
59
|
+
day_of_month = collections.defaultdict(dict)
|
62
60
|
date = datetime.datetime(year, month, 1)
|
63
61
|
while date.month == month:
|
64
62
|
iso = date.isocalendar()
|
65
63
|
weeks[iso.week][iso.weekday] = []
|
64
|
+
day_of_month[iso.week][iso.weekday] = date.day
|
66
65
|
date += datetime.timedelta(days=1)
|
67
66
|
|
68
67
|
for index, row in filtered.iterrows():
|
@@ -76,4 +75,9 @@ class CalendarController:
|
|
76
75
|
}
|
77
76
|
)
|
78
77
|
|
79
|
-
return {
|
78
|
+
return {
|
79
|
+
"year": year,
|
80
|
+
"month": month,
|
81
|
+
"weeks": weeks,
|
82
|
+
"day_of_month": day_of_month,
|
83
|
+
}
|
@@ -0,0 +1,71 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<div class="row mb-3">
|
5
|
+
<div class="col">
|
6
|
+
<h1>{{ date }}</h1>
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
|
11
|
+
<div class="row mb-3">
|
12
|
+
<div class="col-md-9">
|
13
|
+
<div id="activity-map" style="height: 500px;"></div>
|
14
|
+
<script>
|
15
|
+
var map = L.map('activity-map', {
|
16
|
+
fullscreenControl: true
|
17
|
+
});
|
18
|
+
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
19
|
+
maxZoom: 19,
|
20
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
21
|
+
}).addTo(map);
|
22
|
+
|
23
|
+
let geojson = L.geoJSON({{ geojson| safe }}, {
|
24
|
+
style: function (feature) { return { color: feature.properties.color } }
|
25
|
+
}).addTo(map)
|
26
|
+
map.fitBounds(geojson.getBounds());
|
27
|
+
</script>
|
28
|
+
</div>
|
29
|
+
<div class="col-md-3">
|
30
|
+
<ol>
|
31
|
+
{% for activity in activities %}
|
32
|
+
<li><span style="color: {{ activity['color'] }};">█</span> <a href="/activity/{{ activity.id }}">{{
|
33
|
+
activity.name }}</a></li>
|
34
|
+
{% endfor %}
|
35
|
+
</ol>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="row mb-3">
|
40
|
+
<div class="col">
|
41
|
+
<h2>Activities</h2>
|
42
|
+
|
43
|
+
<table class="table">
|
44
|
+
<thead>
|
45
|
+
<tr>
|
46
|
+
<th>Name</th>
|
47
|
+
<th>Date</th>
|
48
|
+
<th>Distance / km</th>
|
49
|
+
<th>Elapsed time</th>
|
50
|
+
<th>Equipment</th>
|
51
|
+
<th>Kind</th>
|
52
|
+
</tr>
|
53
|
+
</thead>
|
54
|
+
<tbody>
|
55
|
+
{% for activity in activities %}
|
56
|
+
<tr>
|
57
|
+
<td><span style="color: {{ activity['color'] }};">█</span> <a href="/activity/{{ activity.id }}">{{
|
58
|
+
activity.name }}</a></td>
|
59
|
+
<td>{{ activity.start }}</td>
|
60
|
+
<td>{{ activity.distance_km | round(1) }}</td>
|
61
|
+
<td>{{ activity.elapsed_time }}</td>
|
62
|
+
<td>{{ activity["equipment"] }}</td>
|
63
|
+
<td>{{ activity["kind"] }}</td>
|
64
|
+
</tr>
|
65
|
+
{% endfor %}
|
66
|
+
</tbody>
|
67
|
+
</table>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
{% endblock %}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<div class="row mb-3">
|
6
|
+
<div class="col">
|
7
|
+
<h1>All Activity Lines</h1>
|
8
|
+
</div>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div class="row mb-3">
|
12
|
+
<div class="col-md-12">
|
13
|
+
<div id="activity-map" style="height: 500px;"></div>
|
14
|
+
<script>
|
15
|
+
function onEachFeature(feature, layer) {
|
16
|
+
layer.bindPopup(`<a href=/activity/${feature.properties.activity_id}>${feature.properties.activity_name}</a>`)
|
17
|
+
}
|
18
|
+
|
19
|
+
var map = L.map('activity-map', {
|
20
|
+
fullscreenControl: true
|
21
|
+
});
|
22
|
+
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
23
|
+
maxZoom: 19,
|
24
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
25
|
+
}).addTo(map);
|
26
|
+
|
27
|
+
let geojson = L.geoJSON({{ geojson| safe }}, {
|
28
|
+
style: function (feature) { return { color: feature.properties.color } },
|
29
|
+
onEachFeature: onEachFeature
|
30
|
+
}).addTo(map)
|
31
|
+
map.fitBounds(geojson.getBounds());
|
32
|
+
</script>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
{% endblock %}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<div class="row mb-3">
|
5
|
+
<div class="col">
|
6
|
+
<h1>{{ name }}</h1>
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
|
11
|
+
<div class="row mb-3">
|
12
|
+
<div class="col-md-12">
|
13
|
+
<div id="activity-map" style="height: 500px;"></div>
|
14
|
+
<script>
|
15
|
+
var map = L.map('activity-map', {
|
16
|
+
fullscreenControl: true
|
17
|
+
});
|
18
|
+
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
19
|
+
maxZoom: 19,
|
20
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
21
|
+
}).addTo(map);
|
22
|
+
|
23
|
+
let geojson = L.geoJSON({{ geojson| safe }}, {
|
24
|
+
style: function (feature) { return { color: feature.properties.color } }
|
25
|
+
}).addTo(map)
|
26
|
+
map.fitBounds(geojson.getBounds());
|
27
|
+
</script>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div class="row mb-3">
|
32
|
+
<div class="col-md-4">
|
33
|
+
{{ vega_direct("tick_plot", tick_plot) }}
|
34
|
+
</div>
|
35
|
+
<div class="col-md-4">
|
36
|
+
{{ vega_direct("equipment_plot", equipment_plot) }}
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div class="row mb-3">
|
41
|
+
<div class="col-md-4">
|
42
|
+
{{ vega_direct("distance_plot", distance_plot) }}
|
43
|
+
</div>
|
44
|
+
<div class="col-md-4">
|
45
|
+
{{ vega_direct("minutes_plot", minutes_plot) }}
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class="row mb-3">
|
50
|
+
<div class="col">
|
51
|
+
<h2>Activities</h2>
|
52
|
+
|
53
|
+
<table class="table">
|
54
|
+
<thead>
|
55
|
+
<tr>
|
56
|
+
<th>Name</th>
|
57
|
+
<th>Date</th>
|
58
|
+
<th>Distance / km</th>
|
59
|
+
<th>Elapsed time</th>
|
60
|
+
<th>Equipment</th>
|
61
|
+
<th>Kind</th>
|
62
|
+
</tr>
|
63
|
+
</thead>
|
64
|
+
<tbody>
|
65
|
+
{% for activity in activities %}
|
66
|
+
<tr>
|
67
|
+
<td><span style="color: {{ activity['color'] }};">█</span> <a href="/activity/{{ activity.id }}">{{
|
68
|
+
activity.name }}</a></td>
|
69
|
+
<td>{{ activity.start }}</td>
|
70
|
+
<td>{{ activity.distance_km | round(1) }}</td>
|
71
|
+
<td>{{ activity.elapsed_time }}</td>
|
72
|
+
<td>{{ activity["equipment"] }}</td>
|
73
|
+
<td>{{ activity["kind"] }}</td>
|
74
|
+
</tr>
|
75
|
+
{% endfor %}
|
76
|
+
</tbody>
|
77
|
+
</table>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
|
81
|
+
{% endblock %}
|
@@ -21,9 +21,12 @@
|
|
21
21
|
<dt>Elapsed time</dt>
|
22
22
|
<dd>{{ activity.elapsed_time }}</dd>
|
23
23
|
<dt>Start time</dt>
|
24
|
-
<dd
|
24
|
+
<dd><a href="/activity/day/{{ date.year }}/{{ date.month }}/{{ date.day }}">{{ date }}</a> {{ time }}
|
25
|
+
</dd>
|
25
26
|
<dt>Calories</dt>
|
26
27
|
<dd>{{ activity.calories }}</dd>
|
28
|
+
<dt>Steps</dt>
|
29
|
+
<dd>{{ activity.steps }}</dd>
|
27
30
|
<dt>Equipment</dt>
|
28
31
|
<dd>{{ activity.equipment }}</dd>
|
29
32
|
<dt>ID</dt>
|
@@ -33,12 +36,12 @@
|
|
33
36
|
</dl>
|
34
37
|
</div>
|
35
38
|
<div class="col-8">
|
36
|
-
<div id="activity-map" style="height: 500px;"></div>
|
39
|
+
<div id="activity-map" style="height: 500px;" class="mb-3"></div>
|
37
40
|
<script>
|
38
41
|
var map = L.map('activity-map', {
|
39
42
|
fullscreenControl: true
|
40
43
|
});
|
41
|
-
L.tileLayer('/tile/
|
44
|
+
L.tileLayer('/tile/pastel/{z}/{x}/{y}.png', {
|
42
45
|
maxZoom: 19,
|
43
46
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
44
47
|
}).addTo(map);
|
@@ -48,6 +51,24 @@
|
|
48
51
|
}).addTo(map)
|
49
52
|
map.fitBounds(geojson.getBounds());
|
50
53
|
</script>
|
54
|
+
|
55
|
+
|
56
|
+
<style>
|
57
|
+
span.colorbar {
|
58
|
+
text-shadow: -1px -1px 0 black, -1px 0px 0 black, -1px 1px 0 black, 0px -1px 0 black, 0px 0px 0 black, 0px 1px 0 black, 1px -1px 0 black, 1px 0px 0 black, 1px 1px 0 black;
|
59
|
+
padding-left: 5px;
|
60
|
+
padding-right: 5px;
|
61
|
+
line-height: 130%;
|
62
|
+
color: white;
|
63
|
+
}
|
64
|
+
</style>
|
65
|
+
|
66
|
+
<div>
|
67
|
+
{% for speed, color in speed_color_bar.colors %}
|
68
|
+
<span class="colorbar" style="width: 15px; background-color: {{ color }}">{{ speed }}</span>
|
69
|
+
{% endfor %}
|
70
|
+
km/h
|
71
|
+
</div>
|
51
72
|
</div>
|
52
73
|
</div>
|
53
74
|
|
@@ -109,35 +130,38 @@
|
|
109
130
|
</div>
|
110
131
|
</div>
|
111
132
|
|
133
|
+
{% if similar_activites|length > 0 %}
|
112
134
|
<div class="row mb-3">
|
113
135
|
<div class="col">
|
114
|
-
<h2>
|
136
|
+
<h2>Activities with the same name</h2>
|
137
|
+
|
138
|
+
<p><a href="/activity/name/{{ activity['name']|urlencode() }}">Overview over these activities</a></p>
|
115
139
|
|
116
140
|
<table class="table">
|
117
141
|
<thead>
|
118
142
|
<tr>
|
119
|
-
<th>Hamming distance</th>
|
120
|
-
<th>Name</th>
|
121
143
|
<th>Date</th>
|
122
144
|
<th>Distance / km</th>
|
123
145
|
<th>Elapsed time</th>
|
146
|
+
<th>Equipment</th>
|
147
|
+
<th>Kind</th>
|
124
148
|
</tr>
|
125
149
|
</thead>
|
126
150
|
<tbody>
|
127
|
-
{% for
|
128
|
-
{% for other_activity in other_activities %}
|
151
|
+
{% for other_activity in similar_activites %}
|
129
152
|
<tr>
|
130
|
-
<td
|
131
|
-
|
132
|
-
<td>{{ other_activity.start }}</td>
|
153
|
+
<td><a href="/activity/{{ other_activity.id }}">{{ other_activity.start
|
154
|
+
}}</a></td>
|
133
155
|
<td>{{ other_activity.distance_km | round(1) }}</td>
|
134
156
|
<td>{{ other_activity.elapsed_time }}</td>
|
157
|
+
<td>{{ other_activity["equipment"] }}</td>
|
158
|
+
<td>{{ other_activity["kind"] }}</td>
|
135
159
|
</tr>
|
136
160
|
{% endfor %}
|
137
|
-
{% endfor %}
|
138
161
|
</tbody>
|
139
162
|
</table>
|
140
163
|
</div>
|
141
164
|
</div>
|
165
|
+
{% endif %}
|
142
166
|
|
143
167
|
{% endblock %}
|
@@ -28,6 +28,13 @@
|
|
28
28
|
<td>{{ week }}</td>
|
29
29
|
{% for day in range(1, 8) %}
|
30
30
|
<td>
|
31
|
+
{% if weeks[week][day] %}
|
32
|
+
<a href="/activity/day/{{ year }}/{{ month }}/{{ day_of_month[week][day] }}"><b>{{
|
33
|
+
day_of_month[week][day] }}.</b></a>
|
34
|
+
{% elif day_of_month[week][day] %}
|
35
|
+
<b>{{ day_of_month[week][day] }}.</b>
|
36
|
+
{% endif %}
|
37
|
+
|
31
38
|
{% if weeks[week][day] %}
|
32
39
|
<ul>
|
33
40
|
{% for activity in weeks[week][day] %}
|
@@ -20,3 +20,13 @@ class TileController:
|
|
20
20
|
f = io.BytesIO()
|
21
21
|
pl.imsave(f, map_tile, format="png")
|
22
22
|
return bytes(f.getbuffer())
|
23
|
+
|
24
|
+
def render_pastel(self, x: int, y: int, z: int) -> bytes:
|
25
|
+
map_tile = np.array(get_tile(z, x, y)) / 255
|
26
|
+
averaged_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2)
|
27
|
+
grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
|
28
|
+
factor = 0.7
|
29
|
+
pastel_tile = factor * grayscale_tile + (1 - factor) * map_tile
|
30
|
+
f = io.BytesIO()
|
31
|
+
pl.imsave(f, pastel_tile, format="png")
|
32
|
+
return bytes(f.getbuffer())
|
{geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: geo-activity-playground
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.21.0
|
4
4
|
Summary: Analysis of geo data activities like rides, runs or hikes.
|
5
5
|
License: MIT
|
6
6
|
Author: Martin Ueding
|
@@ -20,7 +20,6 @@ Requires-Dist: fitdecode (>=0.10.0,<0.11.0)
|
|
20
20
|
Requires-Dist: flask (>=3.0.0,<4.0.0)
|
21
21
|
Requires-Dist: geojson (>=3.0.1,<4.0.0)
|
22
22
|
Requires-Dist: gpxpy (>=1.5.0,<2.0.0)
|
23
|
-
Requires-Dist: imagehash (>=4.3.1,<5.0.0)
|
24
23
|
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
25
24
|
Requires-Dist: matplotlib (>=3.6.3,<4.0.0)
|
26
25
|
Requires-Dist: numpy (>=1.22.4,<2.0.0)
|
{geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/RECORD
RENAMED
@@ -1,8 +1,8 @@
|
|
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=z8CaA2SgW7PQ4peK-D5Z6JXz79k3v3GMGQsYS28TjlA,4990
|
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=-2stf_xJ1C2j-KV08wj5Y-uuuV6w01RN3bh8Gvuti1I,10986
|
5
|
+
geo_activity_playground/core/activity_parsers.py,sha256=GYDaKi5lFo5YIThoRc0enmwwgWkTfJKCxiPsSkpHBxw,11440
|
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
|
@@ -16,13 +16,14 @@ geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
16
16
|
geo_activity_playground/explorer/grid_file.py,sha256=0PCFd6WE1uL3PXwZNeseURuFY7ImEVREoZpTObKiU1U,3258
|
17
17
|
geo_activity_playground/explorer/tile_visits.py,sha256=z6ExfywFNOQ6dh2NZ9wOR_I3XkAvByIaM8O27WM5hH4,10745
|
18
18
|
geo_activity_playground/explorer/video.py,sha256=RGZik93ghfZuRILXf8pfUbPh5VV37_QRAR4FgOMfZqQ,4354
|
19
|
-
geo_activity_playground/importers/directory.py,sha256=
|
19
|
+
geo_activity_playground/importers/directory.py,sha256=8UVgVr20RBf5_Pp7xJfvE-cSbFWWXVFNEzpcE7MdeEo,4331
|
20
20
|
geo_activity_playground/importers/strava_api.py,sha256=yg1SWt_kbyMYFD5uOp7I0uDUtmAOgQvD2Gx3CZZ_ME8,7615
|
21
|
-
geo_activity_playground/importers/strava_checkout.py,sha256=
|
21
|
+
geo_activity_playground/importers/strava_checkout.py,sha256=0lR4aV1yGjIOcmkt4Govrei-mRttbajfXl-yKEgSdLs,7243
|
22
|
+
geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQYEmZJUDVv3OOF5opklxdT4,724
|
22
23
|
geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
|
23
|
-
geo_activity_playground/webui/activity_controller.py,sha256=
|
24
|
-
geo_activity_playground/webui/app.py,sha256=
|
25
|
-
geo_activity_playground/webui/calendar_controller.py,sha256=
|
24
|
+
geo_activity_playground/webui/activity_controller.py,sha256=yM7-EPcoKen-e9mt4D7UWo-E7Zk7Y39_-kr2IV7Km6U,12515
|
25
|
+
geo_activity_playground/webui/app.py,sha256=759gC8M1vw2zjH4Zt4Pk9KqWd5RVjl6htu7f76vFrS4,11089
|
26
|
+
geo_activity_playground/webui/calendar_controller.py,sha256=vn9YNQglbBrQd3IVIU6d2vT3BxGaW0kcRsXLOjuZgqI,2813
|
26
27
|
geo_activity_playground/webui/config_controller.py,sha256=4M8mQc58Hkm-ssfYF1gKRepiAXFIzkZdIMRSbX-aI1U,320
|
27
28
|
geo_activity_playground/webui/eddington_controller.py,sha256=06YLPeXTrCMgI4eNwbhmXQEyVAK-yAk0OT5XGzjHY-Q,2646
|
28
29
|
geo_activity_playground/webui/entry_controller.py,sha256=4rgMvM_1N1p1IxkDIIf20XQnbsNK3h2jdnXgUg58VOU,1904
|
@@ -45,8 +46,11 @@ geo_activity_playground/webui/static/safari-pinned-tab.svg,sha256=OzoEVGY0igWRXM
|
|
45
46
|
geo_activity_playground/webui/static/site.webmanifest,sha256=4vYxdPMpwTdB8EmOvHkkYcjZ8Yrci3pOwwY3o_VwACA,440
|
46
47
|
geo_activity_playground/webui/strava_controller.py,sha256=-DZ1Ae-0cWx5tia2dJpGfsBBoIya0QO7IC2qa1-7Q_U,779
|
47
48
|
geo_activity_playground/webui/summary_controller.py,sha256=CN64yUQkhGFC11iiG8_83E3rYFdRixB5Dl_mUhqSo28,1806
|
48
|
-
geo_activity_playground/webui/templates/activity.html.j2,sha256=
|
49
|
-
geo_activity_playground/webui/templates/
|
49
|
+
geo_activity_playground/webui/templates/activity-day.html.j2,sha256=LiYe5e_b8t7k0ZS6Lk8E0bpm-I4-kc-SRS-P8UUQBYI,2237
|
50
|
+
geo_activity_playground/webui/templates/activity-lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
|
51
|
+
geo_activity_playground/webui/templates/activity-name.html.j2,sha256=opHCj_zY3Xz1l3jIXUQvVdxBNi_D9C-Mdnbx2nQqTTQ,2381
|
52
|
+
geo_activity_playground/webui/templates/activity.html.j2,sha256=ncj0K1471nRHtbBL9fqhGZ7a9DoJLyqt6ckX8rnhS28,4946
|
53
|
+
geo_activity_playground/webui/templates/calendar-month.html.j2,sha256=rV96gOXS0eZU3Dokg8Wb7AJVXJvTPsw61OJoj8lRvt4,1767
|
50
54
|
geo_activity_playground/webui/templates/calendar.html.j2,sha256=x3E1R6KoscVxfcndFePEA855tYz5UoHDSrDbjkhuOOs,1349
|
51
55
|
geo_activity_playground/webui/templates/config.html.j2,sha256=pmec-TqSl5CVznQlyHuC91o18qa0ZQWHXxSBrlV4au4,796
|
52
56
|
geo_activity_playground/webui/templates/eddington.html.j2,sha256=yl75IzWeIkFpwPj8FjTrzJsz_f-qdETPmNnAGLPJuL8,487
|
@@ -60,9 +64,9 @@ geo_activity_playground/webui/templates/search.html.j2,sha256=lYFe9PzP8gqTenhZuf
|
|
60
64
|
geo_activity_playground/webui/templates/square-planner.html.j2,sha256=aIB0ql5qW4HXfp0ENksYYOk9vTgBitwyHJX5W7bqkeY,6512
|
61
65
|
geo_activity_playground/webui/templates/strava-connect.html.j2,sha256=vLMqTnTV-DZJ1FHRjpm4OMgbABMwZQvbs8Ru9baKeBg,1111
|
62
66
|
geo_activity_playground/webui/templates/summary.html.j2,sha256=eEwcPOURJ-uT89jeJGZHq_5pSq56_fTC7z-j_m5nQiA,471
|
63
|
-
geo_activity_playground/webui/tile_controller.py,sha256=
|
64
|
-
geo_activity_playground-0.
|
65
|
-
geo_activity_playground-0.
|
66
|
-
geo_activity_playground-0.
|
67
|
-
geo_activity_playground-0.
|
68
|
-
geo_activity_playground-0.
|
67
|
+
geo_activity_playground/webui/tile_controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
|
68
|
+
geo_activity_playground-0.21.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
|
69
|
+
geo_activity_playground-0.21.0.dist-info/METADATA,sha256=lbVR-EIvkZ05oPkGHW3WgXI1Wn8io-ale9X9de6up0s,1573
|
70
|
+
geo_activity_playground-0.21.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
71
|
+
geo_activity_playground-0.21.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
|
72
|
+
geo_activity_playground-0.21.0.dist-info/RECORD,,
|
{geo_activity_playground-0.20.0.dist-info → geo_activity_playground-0.21.0.dist-info}/LICENSE
RENAMED
File without changes
|
File without changes
|