geo-activity-playground 0.29.2__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.2 → geo_activity_playground-0.30.0}/PKG-INFO +1 -1
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/config.py +2 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/enrichment.py +18 -7
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/paths.py +0 -2
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_api.py +1 -1
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/controller.py +16 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/day.html.j2 +1 -1
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/show.html.j2 +13 -2
- {geo_activity_playground-0.29.2 → 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.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/blueprint.py +44 -0
- {geo_activity_playground-0.29.2 → 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.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.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/home.html.j2 +1 -8
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/templates/page.html.j2 +3 -3
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/pyproject.toml +1 -1
- geo_activity_playground-0.29.2/geo_activity_playground/webui/search/blueprint.py +0 -20
- geo_activity_playground-0.29.2/geo_activity_playground/webui/search/templates/search/index.html.j2 +0 -38
- geo_activity_playground-0.29.2/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
- geo_activity_playground-0.29.2/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
- geo_activity_playground-0.29.2/geo_activity_playground/webui/static/favicon.ico +0 -0
- geo_activity_playground-0.29.2/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -121
- geo_activity_playground-0.29.2/geo_activity_playground/webui/static/site.webmanifest +0 -19
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/LICENSE +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/__main__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/activities.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/coordinates.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heart_rate.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/heatmap.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/privacy_zones.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tasks.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/test_time_conversion.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/tiles.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/core/time_conversion.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/tile_visits.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/activity_parsers.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/csv_parser.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/directory.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/strava_checkout.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_directory.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/app.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/auth/templates/auth/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/authenticator.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/entry_controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/equipment/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/plot_util.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → 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.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/settings/templates/settings/strava.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/square_planner/controller.py +0 -0
- {geo_activity_playground-0.29.2 → 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.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/android-chrome-512x512.png +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/bootstrap-dark-mode.js +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/summary/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/tile/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/__init__.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/blueprint.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/controller.py +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/index.html.j2 +0 -0
- {geo_activity_playground-0.29.2 → geo_activity_playground-0.30.0}/geo_activity_playground/webui/upload/templates/upload/reload.html.j2 +0 -0
@@ -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
|
@@ -139,7 +139,7 @@ def try_import_strava(config: Config) -> bool:
|
|
139
139
|
"commute": activity.commute,
|
140
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],
|
@@ -115,6 +115,8 @@ class ActivityController:
|
|
115
115
|
result["altitude_time_plot"] = altitude_time_plot(time_series)
|
116
116
|
if "heartrate" in time_series.columns:
|
117
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)
|
118
120
|
return result
|
119
121
|
|
120
122
|
def render_sharepic(self, id: int) -> bytes:
|
@@ -324,6 +326,20 @@ def heart_rate_time_plot(time_series: pd.DataFrame) -> str:
|
|
324
326
|
)
|
325
327
|
|
326
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
|
+
|
327
343
|
def heart_rate_zone_plot(heart_zones: pd.DataFrame) -> str:
|
328
344
|
return (
|
329
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>
|
@@ -177,11 +188,11 @@
|
|
177
188
|
|
178
189
|
<p><a href="{{ url_for('.name', name=activity['name']) }}">Overview over these activities</a></p>
|
179
190
|
|
180
|
-
<table class="table">
|
191
|
+
<table class="table table-sort table-arrows">
|
181
192
|
<thead>
|
182
193
|
<tr>
|
183
194
|
<th>Date</th>
|
184
|
-
<th>Distance / km</th>
|
195
|
+
<th class="numeric-sort">Distance / km</th>
|
185
196
|
<th>Elapsed time</th>
|
186
197
|
<th>Equipment</th>
|
187
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 %}
|
@@ -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
|
Binary file
|