geo-activity-playground 0.29.1__tar.gz → 0.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/PKG-INFO +1 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/activities.py +4 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/config.py +2 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/enrichment.py +18 -7
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/paths.py +0 -2
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_api.py +2 -2
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/controller.py +19 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +1 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +14 -2
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/templates/equipment/index.html.j2 +1 -1
- geo_activity_playground-0.30.0/geo_activity_playground/webui/search/blueprint.py +101 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/search/templates/search/index.html.j2 +91 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/blueprint.py +44 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/index.html.j2 +18 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +25 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +10 -11
- geo_activity_playground-0.30.0/geo_activity_playground/webui/settings/templates/settings/segmentation.html.j2 +27 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon-48x48.png +0 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.ico +0 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/favicon.svg +3 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/site.webmanifest +21 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-192x192.png +0 -0
- geo_activity_playground-0.30.0/geo_activity_playground/webui/static/web-app-manifest-512x512.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/home.html.j2 +1 -8
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -3
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/controller.py +1 -1
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/pyproject.toml +1 -1
- geo_activity_playground-0.29.1/geo_activity_playground/webui/search/blueprint.py +0 -20
- geo_activity_playground-0.29.1/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -38
- geo_activity_playground-0.29.1/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- geo_activity_playground-0.29.1/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- geo_activity_playground-0.29.1/geo_activity_playground/webui/static/favicon.ico +0 -0
- geo_activity_playground-0.29.1/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -121
- geo_activity_playground-0.29.1/geo_activity_playground/webui/static/site.webmanifest +0 -19
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/LICENSE +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/__main__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heart_rate.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heatmap.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/privacy_zones.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tasks.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tiles.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/core/time_conversion.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/csv_parser.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/directory.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_directory.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/app.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/authenticator.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/entry_controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/plot_util.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/controller.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
- {geo_activity_playground-0.29.1 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +0 -0
@@ -89,7 +89,10 @@ def build_activity_meta() -> None:
|
|
89
89
|
new_shard = pd.DataFrame(rows)
|
90
90
|
new_shard.index = new_shard["id"]
|
91
91
|
new_shard.index.name = "index"
|
92
|
-
|
92
|
+
if len(meta):
|
93
|
+
meta = pd.concat([meta, new_shard])
|
94
|
+
else:
|
95
|
+
meta = new_shard
|
93
96
|
|
94
97
|
if len(meta):
|
95
98
|
assert pd.api.types.is_dtype_equal(meta["start"].dtype, "datetime64[ns]"), (
|
@@ -29,6 +29,7 @@ class Config:
|
|
29
29
|
)
|
30
30
|
heart_rate_resting: int = 0
|
31
31
|
heart_rate_maximum: Optional[int] = None
|
32
|
+
kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
|
32
33
|
kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
|
33
34
|
metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
|
34
35
|
num_processes: Optional[int] = 1
|
@@ -39,6 +40,7 @@ class Config:
|
|
39
40
|
strava_client_id: int = 131693
|
40
41
|
strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
|
41
42
|
strava_client_code: Optional[str] = None
|
43
|
+
time_diff_threshold_seconds: Optional[int] = 30
|
42
44
|
upload_password: Optional[str] = None
|
43
45
|
|
44
46
|
|
@@ -82,11 +82,15 @@ def enrich_activities(config: Config) -> None:
|
|
82
82
|
)
|
83
83
|
continue
|
84
84
|
|
85
|
+
# Rename kinds if needed.
|
86
|
+
if metadata["kind"] in config.kind_renames:
|
87
|
+
metadata["kind"] = config.kind_renames[metadata["kind"]]
|
88
|
+
|
85
89
|
# Enrich time series.
|
86
90
|
if metadata["kind"] in config.kinds_without_achievements:
|
87
91
|
metadata["consider_for_achievements"] = False
|
88
92
|
time_series = _embellish_single_time_series(
|
89
|
-
time_series, metadata.get("start", None)
|
93
|
+
time_series, metadata.get("start", None), config.time_diff_threshold_seconds
|
90
94
|
)
|
91
95
|
metadata.update(_get_metadata_from_timeseries(time_series))
|
92
96
|
|
@@ -131,7 +135,9 @@ def _compute_moving_time(time_series: pd.DataFrame) -> datetime.timedelta:
|
|
131
135
|
|
132
136
|
|
133
137
|
def _embellish_single_time_series(
|
134
|
-
timeseries: pd.DataFrame,
|
138
|
+
timeseries: pd.DataFrame,
|
139
|
+
start: Optional[datetime.datetime],
|
140
|
+
time_diff_threshold_seconds: int,
|
135
141
|
) -> pd.DataFrame:
|
136
142
|
if start is not None and pd.api.types.is_dtype_equal(
|
137
143
|
timeseries["time"].dtype, "int64"
|
@@ -153,10 +159,12 @@ def _embellish_single_time_series(
|
|
153
159
|
timeseries["latitude"],
|
154
160
|
timeseries["longitude"],
|
155
161
|
).fillna(0.0)
|
156
|
-
time_diff_threshold_seconds
|
157
|
-
|
158
|
-
|
159
|
-
|
162
|
+
if time_diff_threshold_seconds:
|
163
|
+
time_diff = (
|
164
|
+
timeseries["time"] - timeseries["time"].shift(1)
|
165
|
+
).dt.total_seconds()
|
166
|
+
jump_indices = time_diff >= time_diff_threshold_seconds
|
167
|
+
distances.loc[jump_indices] = 0.0
|
160
168
|
|
161
169
|
if "distance_km" not in timeseries.columns:
|
162
170
|
timeseries["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
|
@@ -173,7 +181,10 @@ def _embellish_single_time_series(
|
|
173
181
|
timeseries = timeseries.loc[~potential_jumps].copy()
|
174
182
|
|
175
183
|
if "segment_id" not in timeseries.columns:
|
176
|
-
|
184
|
+
if time_diff_threshold_seconds:
|
185
|
+
timeseries["segment_id"] = np.cumsum(jump_indices)
|
186
|
+
else:
|
187
|
+
timeseries["segment_id"] = 0
|
177
188
|
|
178
189
|
if "x" not in timeseries.columns:
|
179
190
|
x, y = compute_tile_float(timeseries["latitude"], timeseries["longitude"], 0)
|
@@ -8,7 +8,6 @@ import typing
|
|
8
8
|
|
9
9
|
|
10
10
|
def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
11
|
-
@functools.cache
|
12
11
|
def wrapper() -> pathlib.Path:
|
13
12
|
path.mkdir(exist_ok=True, parents=True)
|
14
13
|
return path
|
@@ -17,7 +16,6 @@ def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
|
17
16
|
|
18
17
|
|
19
18
|
def file_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
20
|
-
@functools.cache
|
21
19
|
def wrapper() -> pathlib.Path:
|
22
20
|
path.parent.mkdir(exist_ok=True, parents=True)
|
23
21
|
return path
|
@@ -137,9 +137,9 @@ def try_import_strava(config: Config) -> bool:
|
|
137
137
|
**{
|
138
138
|
"id": activity.id,
|
139
139
|
"commute": activity.commute,
|
140
|
-
"distance_km": activity.distance
|
140
|
+
"distance_km": activity.distance / 1000,
|
141
141
|
"name": activity.name,
|
142
|
-
"kind": str(activity.type),
|
142
|
+
"kind": str(activity.type.root),
|
143
143
|
"start": convert_to_datetime_ns(activity.start_date),
|
144
144
|
"elapsed_time": activity.elapsed_time,
|
145
145
|
"equipment": gear_names[activity.gear_id],
|
@@ -72,6 +72,7 @@ class ActivityController:
|
|
72
72
|
}
|
73
73
|
|
74
74
|
new_tiles_geojson = {}
|
75
|
+
new_tiles_per_zoom = {}
|
75
76
|
for zoom in sorted(self._config.explorer_zoom_levels):
|
76
77
|
new_tiles = self._tile_visit_accessor.tile_state["tile_history"][zoom].loc[
|
77
78
|
self._tile_visit_accessor.tile_state["tile_history"][zoom][
|
@@ -88,6 +89,7 @@ class ActivityController:
|
|
88
89
|
zoom,
|
89
90
|
)
|
90
91
|
new_tiles_geojson[zoom] = make_grid_file_geojson(points)
|
92
|
+
new_tiles_per_zoom[zoom] = len(new_tiles)
|
91
93
|
|
92
94
|
result = {
|
93
95
|
"activity": activity,
|
@@ -100,7 +102,7 @@ class ActivityController:
|
|
100
102
|
"speed_color_bar": make_speed_color_bar(time_series),
|
101
103
|
"date": activity["start"].date(),
|
102
104
|
"time": activity["start"].time(),
|
103
|
-
"new_tiles":
|
105
|
+
"new_tiles": new_tiles_per_zoom,
|
104
106
|
"new_tiles_geojson": new_tiles_geojson,
|
105
107
|
}
|
106
108
|
if (
|
@@ -113,6 +115,8 @@ class ActivityController:
|
|
113
115
|
result["altitude_time_plot"] = altitude_time_plot(time_series)
|
114
116
|
if "heartrate" in time_series.columns:
|
115
117
|
result["heartrate_time_plot"] = heart_rate_time_plot(time_series)
|
118
|
+
if "cadence" in time_series.columns:
|
119
|
+
result["cadence_time_plot"] = cadence_time_plot(time_series)
|
116
120
|
return result
|
117
121
|
|
118
122
|
def render_sharepic(self, id: int) -> bytes:
|
@@ -322,6 +326,20 @@ def heart_rate_time_plot(time_series: pd.DataFrame) -> str:
|
|
322
326
|
)
|
323
327
|
|
324
328
|
|
329
|
+
def cadence_time_plot(time_series: pd.DataFrame) -> str:
|
330
|
+
return (
|
331
|
+
alt.Chart(time_series, title="Cadence")
|
332
|
+
.mark_line()
|
333
|
+
.encode(
|
334
|
+
alt.X("time", title="Time"),
|
335
|
+
alt.Y("cadence", title="Cadence"),
|
336
|
+
alt.Color("segment_id:N", title="Segment"),
|
337
|
+
)
|
338
|
+
.interactive(bind_y=False)
|
339
|
+
.to_json(format="vega")
|
340
|
+
)
|
341
|
+
|
342
|
+
|
325
343
|
def heart_rate_zone_plot(heart_zones: pd.DataFrame) -> str:
|
326
344
|
return (
|
327
345
|
alt.Chart(heart_zones, title="Heart Rate Zones")
|
@@ -50,12 +50,12 @@
|
|
50
50
|
<div class="col">
|
51
51
|
<h2>Activities</h2>
|
52
52
|
|
53
|
-
<table class="table">
|
53
|
+
<table class="table table-sort table-arrows">
|
54
54
|
<thead>
|
55
55
|
<tr>
|
56
56
|
<th>Name</th>
|
57
57
|
<th>Date</th>
|
58
|
-
<th>Distance / km</th>
|
58
|
+
<th class="numeric-sort">Distance / km</th>
|
59
59
|
<th>Elapsed time</th>
|
60
60
|
<th>Equipment</th>
|
61
61
|
<th>Kind</th>
|
@@ -129,6 +129,17 @@
|
|
129
129
|
</div>
|
130
130
|
{% endif %}
|
131
131
|
|
132
|
+
{% if cadence_time_plot is defined %}
|
133
|
+
<h2>Cadence</h2>
|
134
|
+
|
135
|
+
<div class="row mb-3">
|
136
|
+
<div class="col-md-4">
|
137
|
+
{{ vega_direct("cadence_time_plot", cadence_time_plot) }}
|
138
|
+
</div>
|
139
|
+
</div>
|
140
|
+
{% endif %}
|
141
|
+
|
142
|
+
|
132
143
|
<h2>Share picture</h2>
|
133
144
|
|
134
145
|
<p><img src="{{ url_for('.sharepic', id=activity.id) }}" /></p>
|
@@ -160,6 +171,7 @@
|
|
160
171
|
{% for zoom, geojson in new_tiles_geojson.items() %}
|
161
172
|
<div class="col-md-6">
|
162
173
|
<h3>Zoom {{ zoom }}</h3>
|
174
|
+
<p>There are {{ new_tiles[zoom] }} new tiles:</p>
|
163
175
|
<div id="map-{{ zoom }}" style="height: 300px; width: 100%;"></div>
|
164
176
|
<script>
|
165
177
|
let map{{ zoom }} = add_map("{{ zoom }}", {{ geojson | safe }})
|
@@ -176,11 +188,11 @@
|
|
176
188
|
|
177
189
|
<p><a href="{{ url_for('.name', name=activity['name']) }}">Overview over these activities</a></p>
|
178
190
|
|
179
|
-
<table class="table">
|
191
|
+
<table class="table table-sort table-arrows">
|
180
192
|
<thead>
|
181
193
|
<tr>
|
182
194
|
<th>Date</th>
|
183
|
-
<th>Distance / km</th>
|
195
|
+
<th class="numeric-sort">Distance / km</th>
|
184
196
|
<th>Elapsed time</th>
|
185
197
|
<th>Equipment</th>
|
186
198
|
<th>Kind</th>
|
@@ -0,0 +1,101 @@
|
|
1
|
+
from functools import reduce
|
2
|
+
|
3
|
+
import dateutil.parser
|
4
|
+
from flask import Blueprint
|
5
|
+
from flask import flash
|
6
|
+
from flask import render_template
|
7
|
+
from flask import request
|
8
|
+
from flask import Response
|
9
|
+
|
10
|
+
from ...core.activities import ActivityRepository
|
11
|
+
|
12
|
+
|
13
|
+
def reduce_or(selections):
|
14
|
+
return reduce(lambda a, b: a | b, selections)
|
15
|
+
|
16
|
+
|
17
|
+
def reduce_and(selections):
|
18
|
+
return reduce(lambda a, b: a & b, selections)
|
19
|
+
|
20
|
+
|
21
|
+
def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
|
22
|
+
blueprint = Blueprint("search", __name__, template_folder="templates")
|
23
|
+
|
24
|
+
@blueprint.route("/")
|
25
|
+
def index():
|
26
|
+
kinds_avail = repository.meta["kind"].unique()
|
27
|
+
equipments_avail = repository.meta["equipment"].unique()
|
28
|
+
|
29
|
+
print(request.args)
|
30
|
+
|
31
|
+
activities = repository.meta
|
32
|
+
|
33
|
+
if equipments := request.args.getlist("equipment"):
|
34
|
+
selection = reduce_or(
|
35
|
+
activities["equipment"] == equipment for equipment in equipments
|
36
|
+
)
|
37
|
+
activities = activities.loc[selection]
|
38
|
+
|
39
|
+
if kinds := request.args.getlist("kind"):
|
40
|
+
selection = reduce_or(activities["kind"] == kind for kind in kinds)
|
41
|
+
activities = activities.loc[selection]
|
42
|
+
|
43
|
+
name_exact = bool(request.args.get("name_exact", False))
|
44
|
+
name_casing = bool(request.args.get("name_casing", False))
|
45
|
+
if name := request.args.get("name", ""):
|
46
|
+
if name_casing:
|
47
|
+
haystack = activities["name"]
|
48
|
+
needle = name
|
49
|
+
else:
|
50
|
+
haystack = activities["name"].str.lower()
|
51
|
+
needle = name.lower()
|
52
|
+
if name_exact:
|
53
|
+
selection = haystack == needle
|
54
|
+
else:
|
55
|
+
selection = [needle in an for an in haystack]
|
56
|
+
activities = activities.loc[selection]
|
57
|
+
|
58
|
+
begin = request.args.get("begin", "")
|
59
|
+
end = request.args.get("end", "")
|
60
|
+
|
61
|
+
if begin:
|
62
|
+
try:
|
63
|
+
begin_dt = dateutil.parser.parse(begin)
|
64
|
+
except ValueError:
|
65
|
+
flash(
|
66
|
+
f"Cannot parse date `{begin}`, please use a different format.",
|
67
|
+
category="danger",
|
68
|
+
)
|
69
|
+
else:
|
70
|
+
selection = begin_dt <= activities["start"]
|
71
|
+
activities = activities.loc[selection]
|
72
|
+
|
73
|
+
if end:
|
74
|
+
try:
|
75
|
+
end_dt = dateutil.parser.parse(end)
|
76
|
+
except ValueError:
|
77
|
+
flash(
|
78
|
+
f"Cannot parse date `{end}`, please use a different format.",
|
79
|
+
category="danger",
|
80
|
+
)
|
81
|
+
else:
|
82
|
+
selection = activities["start"] < end_dt
|
83
|
+
activities = activities.loc[selection]
|
84
|
+
|
85
|
+
activities = activities.sort_values("start", ascending=False)
|
86
|
+
|
87
|
+
return render_template(
|
88
|
+
"search/index.html.j2",
|
89
|
+
activities=list(activities.iterrows()),
|
90
|
+
equipments=request.args.getlist("equipment"),
|
91
|
+
equipments_avail=sorted(equipments_avail),
|
92
|
+
kinds=request.args.getlist("kind"),
|
93
|
+
kinds_avail=sorted(kinds_avail),
|
94
|
+
name=name,
|
95
|
+
name_exact=name_exact,
|
96
|
+
name_casing=name_casing,
|
97
|
+
begin=begin,
|
98
|
+
end=end,
|
99
|
+
)
|
100
|
+
|
101
|
+
return blueprint
|
geo_activity_playground-0.30.0/geo_activity_playground/webui/search/templates/search/index.html.j2
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="row mb-3">Activities Overview & Search</h1>
|
6
|
+
|
7
|
+
<div class="row mb-3">
|
8
|
+
<div class="col-md-2">
|
9
|
+
<form>
|
10
|
+
<div class="mb-3">
|
11
|
+
<label for="name" class="form-label">Name</label>
|
12
|
+
<input type="text" class="form-control" id="name" name="name" value="{{ name }}">
|
13
|
+
<div class="form-check">
|
14
|
+
<input class="form-check-input" type="checkbox" name="name_exact" value="true" id="name_exact" {% if
|
15
|
+
name_exact %} checked {% endif %}>
|
16
|
+
<label class="form-check-label" for="name_exact">
|
17
|
+
Exact match
|
18
|
+
</label>
|
19
|
+
</div>
|
20
|
+
<div class="form-check">
|
21
|
+
<input class="form-check-input" type="checkbox" name="name_casing" value="true" id="name_casing" {%
|
22
|
+
if name_casing %} checked {% endif %}>
|
23
|
+
<label class="form-check-label" for="name_casing">
|
24
|
+
Case sensitive
|
25
|
+
</label>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div class="mb-3">
|
30
|
+
<label for="begin" class="form-label">After</label>
|
31
|
+
<input type="text" class="form-control" id="begin" name="begin" value="{{ begin }}">
|
32
|
+
<label for="end" class="form-label">Until</label>
|
33
|
+
<input type="text" class="form-control" id="end" name="end" value="{{ end }}">
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div class="mb-3">
|
37
|
+
<label for="" class="form-label">Kind</label>
|
38
|
+
{% for kind in kinds_avail %}
|
39
|
+
<div class="form-check">
|
40
|
+
<input class="form-check-input" type="checkbox" name="kind" value="{{ kind }}" id="kind_{{ kind }}"
|
41
|
+
{% if kind in kinds %} checked {% endif %}>
|
42
|
+
<label class="form-check-label" for="kind_{{ kind }}">
|
43
|
+
{{ kind }}
|
44
|
+
</label>
|
45
|
+
</div>
|
46
|
+
{% endfor %}
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class="mb-3">
|
50
|
+
<label for="" class="form-label">Equipment</label>
|
51
|
+
{% for equipment in equipments_avail %}
|
52
|
+
<div class="form-check">
|
53
|
+
<input class="form-check-input" type="checkbox" name="equipment" value="{{ equipment }}"
|
54
|
+
id="equipment_{{ equipment }}" {% if equipment in equipments %} checked {% endif %}>
|
55
|
+
<label class="form-check-label" for="equipment_{{ equipment }}">
|
56
|
+
{{ equipment }}
|
57
|
+
</label>
|
58
|
+
</div>
|
59
|
+
{% endfor %}
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<button type="submit" class="btn btn-primary">Search</button>
|
63
|
+
</form>
|
64
|
+
</div>
|
65
|
+
|
66
|
+
<div class="col-md-10">
|
67
|
+
<table class="table table-sort table-arrows">
|
68
|
+
<thead>
|
69
|
+
<tr>
|
70
|
+
<th>Name</th>
|
71
|
+
<th>Start</th>
|
72
|
+
<th>Kind</th>
|
73
|
+
<th class="numeric-sort">Distance</th>
|
74
|
+
<th>Elapsed time</th>
|
75
|
+
</tr>
|
76
|
+
</thead>
|
77
|
+
<tbody>
|
78
|
+
{% for index, activity in activities %}
|
79
|
+
<tr>
|
80
|
+
<td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
|
81
|
+
<td>{{ activity['start'] }}</td>
|
82
|
+
<td>{{ activity['kind'] }}</td>
|
83
|
+
<td>{{ '%.1f' % activity["distance_km"] }} km</td>
|
84
|
+
<td>{{ activity.elapsed_time }}</td>
|
85
|
+
</tr>
|
86
|
+
{% endfor %}
|
87
|
+
</tbody>
|
88
|
+
</table>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
{% endblock %}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import shutil
|
1
2
|
from typing import Optional
|
2
3
|
|
3
4
|
from flask import Blueprint
|
@@ -8,6 +9,7 @@ from flask import request
|
|
8
9
|
from flask import url_for
|
9
10
|
|
10
11
|
from geo_activity_playground.core.config import ConfigAccessor
|
12
|
+
from geo_activity_playground.core.paths import _activity_enriched_dir
|
11
13
|
from geo_activity_playground.webui.authenticator import Authenticator
|
12
14
|
from geo_activity_playground.webui.authenticator import needs_authentication
|
13
15
|
from geo_activity_playground.webui.settings.controller import SettingsController
|
@@ -139,6 +141,33 @@ def make_settings_blueprint(
|
|
139
141
|
"settings/heart-rate.html.j2", **settings_controller.render_heart_rate()
|
140
142
|
)
|
141
143
|
|
144
|
+
@blueprint.route("/kind-renames", methods=["GET", "POST"])
|
145
|
+
@needs_authentication(authenticator)
|
146
|
+
def kind_renames():
|
147
|
+
if request.method == "POST":
|
148
|
+
rules_str = request.form["rules_str"]
|
149
|
+
rules = {}
|
150
|
+
try:
|
151
|
+
for line in rules_str.strip().split("\n"):
|
152
|
+
first, second = line.split(" => ")
|
153
|
+
rules[first.strip()] = second.strip()
|
154
|
+
config_accessor().kind_renames = rules
|
155
|
+
config_accessor.save()
|
156
|
+
flash(f"Kind renames updated.", category="success")
|
157
|
+
shutil.rmtree(_activity_enriched_dir)
|
158
|
+
return redirect(url_for("upload.reload"))
|
159
|
+
except ValueError as e:
|
160
|
+
flash(f"Cannot parse this. Please try again.", category="danger")
|
161
|
+
else:
|
162
|
+
rules_str = "\n".join(
|
163
|
+
f"{key} => {value}"
|
164
|
+
for key, value in config_accessor().kind_renames.items()
|
165
|
+
)
|
166
|
+
return render_template(
|
167
|
+
"settings/kind-renames.html.j2",
|
168
|
+
rules_str=rules_str,
|
169
|
+
)
|
170
|
+
|
142
171
|
@blueprint.route("/kinds-without-achievements", methods=["GET", "POST"])
|
143
172
|
@needs_authentication(authenticator)
|
144
173
|
def kinds_without_achievements():
|
@@ -173,6 +202,21 @@ def make_settings_blueprint(
|
|
173
202
|
**settings_controller.render_privacy_zones(),
|
174
203
|
)
|
175
204
|
|
205
|
+
@blueprint.route("/segmentation", methods=["GET", "POST"])
|
206
|
+
@needs_authentication(authenticator)
|
207
|
+
def segmentation():
|
208
|
+
if request.method == "POST":
|
209
|
+
threshold = int(request.form.get("threshold", 0))
|
210
|
+
config_accessor().time_diff_threshold_seconds = threshold
|
211
|
+
config_accessor.save()
|
212
|
+
flash(f"Threshold set to {threshold}.", category="success")
|
213
|
+
shutil.rmtree(_activity_enriched_dir)
|
214
|
+
return redirect(url_for("upload.reload"))
|
215
|
+
return render_template(
|
216
|
+
"settings/segmentation.html.j2",
|
217
|
+
threshold=config_accessor().time_diff_threshold_seconds,
|
218
|
+
)
|
219
|
+
|
176
220
|
@blueprint.route("/sharepic", methods=["GET", "POST"])
|
177
221
|
@needs_authentication(authenticator)
|
178
222
|
def sharepic():
|
@@ -47,6 +47,15 @@
|
|
47
47
|
</div>
|
48
48
|
</div>
|
49
49
|
</div>
|
50
|
+
<div class="col">
|
51
|
+
<div class="card">
|
52
|
+
<div class="card-body">
|
53
|
+
<h5 class="card-title">Kind renames</h5>
|
54
|
+
<p class="card-text">Bulk rename activity kinds</p>
|
55
|
+
<a href="{{ url_for('.kind_renames') }}" class="btn btn-primary">Set up kind renames</a>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
50
59
|
<div class="col">
|
51
60
|
<div class="card">
|
52
61
|
<div class="card-body">
|
@@ -96,6 +105,15 @@
|
|
96
105
|
</div>
|
97
106
|
</div>
|
98
107
|
</div>
|
108
|
+
<div class="col">
|
109
|
+
<div class="card">
|
110
|
+
<div class="card-body">
|
111
|
+
<h5 class="card-title">Track segmentation</h5>
|
112
|
+
<p class="card-text">Split tracks into multiple segments if there are breaks or jumps.</p>
|
113
|
+
<a href="{{ url_for('.segmentation') }}" class="btn btn-primary">Set up track segmentations</a>
|
114
|
+
</div>
|
115
|
+
</div>
|
116
|
+
</div>
|
99
117
|
</div>
|
100
118
|
</div>
|
101
119
|
{% endblock %}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Kind renaming</h1>
|
6
|
+
|
7
|
+
<p>If you have used different apps for tracking, you might have that your bike rides have <i>kind</i> "ride", "Ride",
|
8
|
+
"Radfahrt" and so on. In order to unify these, you can specify mappings from old to new names.</p>
|
9
|
+
|
10
|
+
<p>If you want to unify these to "Ride", enter the following:</p>
|
11
|
+
|
12
|
+
<pre><code>
|
13
|
+
ride => Ride
|
14
|
+
Radfahrt => Ride
|
15
|
+
</code></pre>
|
16
|
+
|
17
|
+
<form method="POST">
|
18
|
+
<div class="mb-3">
|
19
|
+
<label for="rules" class="form-label">Rules</label>
|
20
|
+
<textarea class="form-control" id=rules" cols="80" rows="10" name="rules_str">{{ rules_str }}</textarea>
|
21
|
+
</div>
|
22
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
23
|
+
</form>
|
24
|
+
|
25
|
+
{% endblock %}
|
@@ -4,8 +4,7 @@
|
|
4
4
|
|
5
5
|
<h1 class="mb-3">Metadata Extraction</h1>
|
6
6
|
|
7
|
-
<p>
|
8
|
-
default value. These are:</p>
|
7
|
+
<p>If the current activity metadata is not to your liking, you can adjust how these fields are populated:</p>
|
9
8
|
|
10
9
|
<ul>
|
11
10
|
<li><tt>kind</tt>: The kind of the activity, like "Ride" or "Run".</li>
|
@@ -13,8 +12,9 @@
|
|
13
12
|
<li><tt>name</tt>: Name for the activity, like "Ride with Friends".</li>
|
14
13
|
</ul>
|
15
14
|
|
16
|
-
<p>By default these fields are
|
17
|
-
|
15
|
+
<p>By default these fields are extracted from files that contain this data. Otherwise the filename is set as <tt>name</tt>.
|
16
|
+
You can overwrite this by setting up a directory structure with corresponding regular expressions using named capture groups.</p>
|
17
|
+
|
18
18
|
|
19
19
|
<form method="POST">
|
20
20
|
<div class="row">
|
@@ -32,19 +32,18 @@
|
|
32
32
|
</div>
|
33
33
|
|
34
34
|
<div class="col-md-6">
|
35
|
-
<p>
|
36
|
-
<tt>
|
37
|
-
|
38
|
-
following regular expression:
|
39
|
-
</p>
|
35
|
+
<p>Consider a directory structure <tt>Ride/Red Roadbike/2024-08-10 11-45-00 Ride with Friends.fit</tt> under Activities.
|
36
|
+
You can extract <tt>kind</tt>: "Ride", <tt>equipment</tt>: "Red Roadbike", <tt>name</tt>: "Ride with Friends"
|
37
|
+
with the following regular expression:</p>
|
40
38
|
|
41
39
|
<div class="code">
|
42
40
|
<pre
|
43
41
|
class="code literal-block">(?P<kind>[^/]+)/(?P<equipment>[^/]+)/[-\d_ .]+(?P<name>[^/\.]+)</pre>
|
44
42
|
</div>
|
45
43
|
|
46
|
-
<p>
|
47
|
-
|
44
|
+
<p>Have a look at the documentation
|
45
|
+
<a href="https://martin-ueding.github.io/geo-activity-playground/getting-started/advanced-metadata-extraction">Advanced Metadata Extraction</a>
|
46
|
+
for explanations and examples.</p>
|
48
47
|
</div>
|
49
48
|
</div>
|
50
49
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Track segmentation</h1>
|
6
|
+
|
7
|
+
<p>Some activity tracking apps or devices automatically pause the recording when there is no movement for a while. Other
|
8
|
+
trackers do not record new points when one goes in a straight line. And some users manually pause the activity,
|
9
|
+
forget to resume and resume after having moved for a while.</p>
|
10
|
+
|
11
|
+
<p>Depending on the usage patterns one wants to segment the tracks into segments or really keep them as one long track.
|
12
|
+
In order to cater for different use cases, this can be changed with a setting.</p>
|
13
|
+
|
14
|
+
<p>In the following you can enter a threshold. If these many seconds elapse between two subsequent points in the track,
|
15
|
+
these will be considered different segments and not be connected with a straight line. Entering "0" disabled
|
16
|
+
segmentation.</p>
|
17
|
+
|
18
|
+
<form method="POST">
|
19
|
+
<div class="mb-3">
|
20
|
+
<label for="threshold" class="form-label">Threshold / s</label>
|
21
|
+
<input type="text" class="form-control" id="threshold" name="threshold" value="{{ threshold }}" />
|
22
|
+
</div>
|
23
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
24
|
+
</form>
|
25
|
+
|
26
|
+
|
27
|
+
{% endblock %}
|
Binary file
|