geo-activity-playground 1.2.0__py3-none-any.whl → 1.3.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/alembic/versions/85fe0348e8a2_add_time_series_uuid_field.py +28 -0
- geo_activity_playground/alembic/versions/f2f50843be2d_make_all_fields_in_activity_nullable.py +34 -0
- geo_activity_playground/core/coordinates.py +12 -1
- geo_activity_playground/core/copernicus_dem.py +95 -0
- geo_activity_playground/core/datamodel.py +43 -16
- geo_activity_playground/core/enrichment.py +226 -164
- geo_activity_playground/core/paths.py +8 -0
- geo_activity_playground/core/test_pandas_timezone.py +36 -0
- geo_activity_playground/core/test_time_zone_from_location.py +7 -0
- geo_activity_playground/core/test_time_zone_import.py +93 -0
- geo_activity_playground/core/test_timezone_sqlalchemy.py +44 -0
- geo_activity_playground/core/tiles.py +4 -1
- geo_activity_playground/core/time_conversion.py +42 -14
- geo_activity_playground/explorer/tile_visits.py +7 -4
- geo_activity_playground/importers/activity_parsers.py +21 -22
- geo_activity_playground/importers/directory.py +62 -108
- geo_activity_playground/importers/strava_api.py +53 -36
- geo_activity_playground/importers/strava_checkout.py +30 -56
- geo_activity_playground/webui/app.py +40 -2
- geo_activity_playground/webui/blueprints/activity_blueprint.py +13 -11
- geo_activity_playground/webui/blueprints/entry_views.py +1 -1
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +1 -7
- geo_activity_playground/webui/blueprints/heatmap_blueprint.py +2 -2
- geo_activity_playground/webui/blueprints/settings_blueprint.py +3 -14
- geo_activity_playground/webui/blueprints/summary_blueprint.py +6 -6
- geo_activity_playground/webui/blueprints/time_zone_fixer_blueprint.py +69 -0
- geo_activity_playground/webui/blueprints/upload_blueprint.py +3 -16
- geo_activity_playground/webui/columns.py +9 -1
- geo_activity_playground/webui/templates/activity/show.html.j2 +3 -1
- geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +1 -1
- geo_activity_playground/webui/templates/home.html.j2 +3 -2
- geo_activity_playground/webui/templates/page.html.j2 +2 -0
- geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2 +31 -0
- {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.0.dist-info}/METADATA +7 -3
- {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.0.dist-info}/RECORD +38 -30
- geo_activity_playground/core/test_time_conversion.py +0 -37
- {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,9 @@
|
|
1
1
|
import datetime
|
2
2
|
import logging
|
3
3
|
import pathlib
|
4
|
-
import pickle
|
5
4
|
import shutil
|
6
5
|
import sys
|
7
|
-
import
|
6
|
+
import zoneinfo
|
8
7
|
from typing import Optional
|
9
8
|
|
10
9
|
import dateutil.parser
|
@@ -12,17 +11,16 @@ import numpy as np
|
|
12
11
|
import pandas as pd
|
13
12
|
from tqdm import tqdm
|
14
13
|
|
15
|
-
from ..core.
|
14
|
+
from ..core.config import Config
|
16
15
|
from ..core.datamodel import DEFAULT_UNKNOWN_NAME
|
16
|
+
from ..core.datamodel import get_or_make_equipment
|
17
|
+
from ..core.datamodel import get_or_make_kind
|
18
|
+
from ..core.enrichment import update_and_commit
|
17
19
|
from ..core.paths import activity_extracted_meta_dir
|
18
|
-
from ..core.paths import activity_extracted_time_series_dir
|
19
|
-
from ..core.paths import strava_last_activity_date_path
|
20
20
|
from ..core.tasks import get_state
|
21
21
|
from ..core.tasks import set_state
|
22
22
|
from ..core.tasks import work_tracker_path
|
23
23
|
from ..core.tasks import WorkTracker
|
24
|
-
from ..core.time_conversion import convert_to_datetime_ns
|
25
|
-
from .activity_parsers import ActivityParseError
|
26
24
|
from .activity_parsers import read_activity
|
27
25
|
from .csv_parser import parse_csv
|
28
26
|
|
@@ -144,7 +142,7 @@ def float_with_comma_or_period(x: str) -> Optional[float]:
|
|
144
142
|
return float(x)
|
145
143
|
|
146
144
|
|
147
|
-
def import_from_strava_checkout() -> None:
|
145
|
+
def import_from_strava_checkout(config: Config) -> None:
|
148
146
|
checkout_path = pathlib.Path("Strava Export")
|
149
147
|
with open(checkout_path / "activities.csv", encoding="utf-8") as f:
|
150
148
|
rows = parse_csv(f.read())
|
@@ -191,47 +189,13 @@ def import_from_strava_checkout() -> None:
|
|
191
189
|
if not row["Filename"]:
|
192
190
|
continue
|
193
191
|
|
194
|
-
start_datetime = dateutil.parser.parse(
|
192
|
+
start_datetime = dateutil.parser.parse(
|
193
|
+
row["Activity Date"], dayfirst=dayfirst
|
194
|
+
).replace(tzinfo=zoneinfo.ZoneInfo("utc"))
|
195
195
|
|
196
196
|
activity_file = checkout_path / row["Filename"]
|
197
|
-
|
198
|
-
|
199
|
-
"commute": row["Commute"] == "true",
|
200
|
-
"distance_km": row["Distance"],
|
201
|
-
"elapsed_time": datetime.timedelta(
|
202
|
-
seconds=float_with_comma_or_period(row["Elapsed Time"])
|
203
|
-
),
|
204
|
-
"equipment": str(
|
205
|
-
nan_as_none(row["Activity Gear"])
|
206
|
-
or nan_as_none(row["Bike"])
|
207
|
-
or nan_as_none(row["Gear"])
|
208
|
-
or ""
|
209
|
-
),
|
210
|
-
"kind": row["Activity Type"],
|
211
|
-
"id": activity_id,
|
212
|
-
"name": row["Activity Name"],
|
213
|
-
"path": str(activity_file),
|
214
|
-
"start": convert_to_datetime_ns(start_datetime),
|
215
|
-
"steps": float_with_comma_or_period(row["Total Steps"]),
|
216
|
-
}
|
217
|
-
|
218
|
-
time_series_path = (
|
219
|
-
activity_extracted_time_series_dir() / f"{activity_id}.parquet"
|
220
|
-
)
|
221
|
-
if time_series_path.exists():
|
222
|
-
time_series = pd.read_parquet(time_series_path)
|
223
|
-
else:
|
224
|
-
try:
|
225
|
-
file_activity_meta, time_series = read_activity(activity_file)
|
226
|
-
except ActivityParseError as e:
|
227
|
-
logger.error(f"Error while parsing file {activity_file}:")
|
228
|
-
traceback.print_exc()
|
229
|
-
continue
|
230
|
-
except:
|
231
|
-
logger.error(
|
232
|
-
f"Encountered a problem with {activity_file=}, see details below."
|
233
|
-
)
|
234
|
-
raise
|
197
|
+
|
198
|
+
activity, time_series = read_activity(activity_file)
|
235
199
|
|
236
200
|
if not len(time_series):
|
237
201
|
continue
|
@@ -239,16 +203,26 @@ def import_from_strava_checkout() -> None:
|
|
239
203
|
if "latitude" not in time_series.columns:
|
240
204
|
continue
|
241
205
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
206
|
+
activity.upstream_id = activity_id
|
207
|
+
activity.calories = float_with_comma_or_period(row["Calories"])
|
208
|
+
activity.distance_km = row["Distance"]
|
209
|
+
activity.elapsed_time = datetime.timedelta(
|
210
|
+
seconds=float_with_comma_or_period(row["Elapsed Time"])
|
211
|
+
)
|
212
|
+
activity.equipment = get_or_make_equipment(
|
213
|
+
nan_as_none(row["Activity Gear"])
|
214
|
+
or nan_as_none(row["Bike"])
|
215
|
+
or nan_as_none(row["Gear"])
|
216
|
+
or DEFAULT_UNKNOWN_NAME,
|
217
|
+
config,
|
249
218
|
)
|
250
|
-
|
251
|
-
|
219
|
+
activity.kind = get_or_make_kind(row["Activity Type"])
|
220
|
+
activity.name = row["Activity Name"]
|
221
|
+
activity.path = str(activity_file)
|
222
|
+
activity.start = start_datetime
|
223
|
+
activity.steps = float_with_comma_or_period(row["Total Steps"])
|
224
|
+
|
225
|
+
update_and_commit(activity, time_series, config)
|
252
226
|
|
253
227
|
work_tracker.close()
|
254
228
|
|
@@ -6,11 +6,14 @@ import os
|
|
6
6
|
import pathlib
|
7
7
|
import secrets
|
8
8
|
import shutil
|
9
|
+
import threading
|
9
10
|
import urllib.parse
|
10
11
|
import uuid
|
12
|
+
import warnings
|
11
13
|
|
12
14
|
import pandas as pd
|
13
15
|
import sqlalchemy
|
16
|
+
from flask import Config
|
14
17
|
from flask import Flask
|
15
18
|
from flask import request
|
16
19
|
from flask_alembic import Alembic
|
@@ -19,12 +22,14 @@ from ..core.activities import ActivityRepository
|
|
19
22
|
from ..core.config import ConfigAccessor
|
20
23
|
from ..core.config import import_old_config
|
21
24
|
from ..core.config import import_old_strava_config
|
25
|
+
from ..core.datamodel import Activity
|
22
26
|
from ..core.datamodel import DB
|
23
27
|
from ..core.datamodel import Equipment
|
24
28
|
from ..core.datamodel import Kind
|
25
29
|
from ..core.datamodel import Photo
|
26
30
|
from ..core.datamodel import Tag
|
27
31
|
from ..core.heart_rate import HeartRateZoneComputer
|
32
|
+
from ..core.paths import TIME_SERIES_DIR
|
28
33
|
from ..core.raster_map import GrayscaleImageTransform
|
29
34
|
from ..core.raster_map import IdentityImageTransform
|
30
35
|
from ..core.raster_map import PastelImageTransform
|
@@ -49,6 +54,7 @@ from .blueprints.settings_blueprint import make_settings_blueprint
|
|
49
54
|
from .blueprints.square_planner_blueprint import make_square_planner_blueprint
|
50
55
|
from .blueprints.summary_blueprint import make_summary_blueprint
|
51
56
|
from .blueprints.tile_blueprint import make_tile_blueprint
|
57
|
+
from .blueprints.time_zone_fixer_blueprint import make_time_zone_fixer_blueprint
|
52
58
|
from .blueprints.upload_blueprint import make_upload_blueprint
|
53
59
|
from .blueprints.upload_blueprint import scan_for_activities
|
54
60
|
from .flasher import FlaskFlasher
|
@@ -65,11 +71,22 @@ def get_secret_key():
|
|
65
71
|
secret = json.load(f)
|
66
72
|
else:
|
67
73
|
secret = secrets.token_hex()
|
74
|
+
secret_file.parent.mkdir(exist_ok=True, parents=True)
|
68
75
|
with open(secret_file, "w") as f:
|
69
76
|
json.dump(secret, f)
|
70
77
|
return secret
|
71
78
|
|
72
79
|
|
80
|
+
def importer_thread(
|
81
|
+
app: Flask,
|
82
|
+
repository: ActivityRepository,
|
83
|
+
tile_visit_accessor: TileVisitAccessor,
|
84
|
+
config: Config,
|
85
|
+
) -> None:
|
86
|
+
with app.app_context():
|
87
|
+
scan_for_activities(repository, tile_visit_accessor, config)
|
88
|
+
|
89
|
+
|
73
90
|
def web_ui_main(
|
74
91
|
basedir: pathlib.Path,
|
75
92
|
skip_reload: bool,
|
@@ -78,6 +95,12 @@ def web_ui_main(
|
|
78
95
|
) -> None:
|
79
96
|
os.chdir(basedir)
|
80
97
|
|
98
|
+
warnings.filterwarnings("ignore", "__array__ implementation doesn't")
|
99
|
+
warnings.filterwarnings("ignore", '\'field "native_field_num"')
|
100
|
+
warnings.filterwarnings("ignore", '\'field "units"')
|
101
|
+
warnings.filterwarnings(
|
102
|
+
"ignore", r"datetime.datetime.utcfromtimestamp\(\) is deprecated"
|
103
|
+
)
|
81
104
|
app = Flask(__name__)
|
82
105
|
|
83
106
|
database_path = pathlib.Path("database.sqlite")
|
@@ -100,9 +123,21 @@ def web_ui_main(
|
|
100
123
|
import_old_config(config_accessor)
|
101
124
|
import_old_strava_config(config_accessor)
|
102
125
|
|
126
|
+
with app.app_context():
|
127
|
+
for activity in DB.session.scalars(sqlalchemy.select(Activity)).all():
|
128
|
+
if not activity.time_series_uuid:
|
129
|
+
activity.time_series_uuid = str(uuid.uuid4())
|
130
|
+
DB.session.commit()
|
131
|
+
old_path = TIME_SERIES_DIR() / f"{activity.id}.parquet"
|
132
|
+
if old_path.exists() and not activity.time_series_path.exists():
|
133
|
+
old_path.rename(activity.time_series_path)
|
134
|
+
|
103
135
|
if not skip_reload:
|
104
|
-
|
105
|
-
|
136
|
+
thread = threading.Thread(
|
137
|
+
target=importer_thread,
|
138
|
+
args=(app, repository, tile_visit_accessor, config_accessor()),
|
139
|
+
)
|
140
|
+
thread.start()
|
106
141
|
|
107
142
|
app.config["UPLOAD_FOLDER"] = "Activities"
|
108
143
|
app.secret_key = get_secret_key()
|
@@ -183,6 +218,9 @@ def web_ui_main(
|
|
183
218
|
),
|
184
219
|
"/summary": make_summary_blueprint(repository, config, search_query_history),
|
185
220
|
"/tile": make_tile_blueprint(image_transforms, tile_getter),
|
221
|
+
"/time-zone-fixer": make_time_zone_fixer_blueprint(
|
222
|
+
authenticator, config, tile_visit_accessor
|
223
|
+
),
|
186
224
|
"/upload": make_upload_blueprint(
|
187
225
|
repository, tile_visit_accessor, config_accessor(), authenticator, flasher
|
188
226
|
),
|
@@ -32,7 +32,7 @@ from ...core.datamodel import DB
|
|
32
32
|
from ...core.datamodel import Equipment
|
33
33
|
from ...core.datamodel import Kind
|
34
34
|
from ...core.datamodel import Tag
|
35
|
-
from ...core.enrichment import
|
35
|
+
from ...core.enrichment import update_and_commit
|
36
36
|
from ...core.heart_rate import HeartRateZoneComputer
|
37
37
|
from ...core.privacy_zones import PrivacyZone
|
38
38
|
from ...core.raster_map import map_image_from_tile_bounds
|
@@ -44,8 +44,7 @@ from ...explorer.grid_file import make_grid_points
|
|
44
44
|
from ...explorer.tile_visits import TileVisitAccessor
|
45
45
|
from ..authenticator import Authenticator
|
46
46
|
from ..authenticator import needs_authentication
|
47
|
-
from ..columns import
|
48
|
-
from ..columns import column_speed
|
47
|
+
from ..columns import TIME_SERIES_COLUMNS
|
49
48
|
|
50
49
|
logger = logging.getLogger(__name__)
|
51
50
|
|
@@ -137,7 +136,7 @@ def make_activity_blueprint(
|
|
137
136
|
new_tiles_per_zoom[zoom] = len(new_tiles)
|
138
137
|
|
139
138
|
line_color_columns_avail = dict(
|
140
|
-
[(column.name, column) for column in
|
139
|
+
[(column.name, column) for column in TIME_SERIES_COLUMNS]
|
141
140
|
)
|
142
141
|
line_color_column = (
|
143
142
|
request.args.get("line_color_column")
|
@@ -165,8 +164,8 @@ def make_activity_blueprint(
|
|
165
164
|
time_series[line_color_column],
|
166
165
|
line_color_columns_avail[line_color_column].format,
|
167
166
|
),
|
168
|
-
"date": activity.
|
169
|
-
"time": activity.
|
167
|
+
"date": activity.start_local_tz.date(),
|
168
|
+
"time": activity.start_local_tz.time(),
|
170
169
|
"line_color_column": line_color_column,
|
171
170
|
"line_color_columns_avail": line_color_columns_avail,
|
172
171
|
}
|
@@ -178,7 +177,7 @@ def make_activity_blueprint(
|
|
178
177
|
)
|
179
178
|
) is not None:
|
180
179
|
context["heart_zones_plot"] = heart_rate_zone_plot(heart_zones)
|
181
|
-
if "
|
180
|
+
if "copernicus_elevation" in time_series.columns:
|
182
181
|
context["elevation_time_plot"] = elevation_time_plot(time_series)
|
183
182
|
if "elevation_gain_cum" in time_series.columns:
|
184
183
|
context["elevation_gain_cum_plot"] = elevation_gain_cum_plot(time_series)
|
@@ -380,9 +379,8 @@ def make_activity_blueprint(
|
|
380
379
|
if form_end:
|
381
380
|
activity.index_end = int(form_end)
|
382
381
|
|
383
|
-
|
384
|
-
|
385
|
-
DB.session.commit()
|
382
|
+
time_series = activity.time_series
|
383
|
+
update_and_commit(activity, time_series, config)
|
386
384
|
|
387
385
|
cmap = matplotlib.colormaps["turbo"]
|
388
386
|
num_points = len(activity.time_series)
|
@@ -509,7 +507,11 @@ def elevation_time_plot(time_series: pd.DataFrame) -> str:
|
|
509
507
|
.mark_line()
|
510
508
|
.encode(
|
511
509
|
alt.X("time", title="Time"),
|
512
|
-
alt.Y(
|
510
|
+
alt.Y(
|
511
|
+
"copernicus_elevation",
|
512
|
+
scale=alt.Scale(zero=False),
|
513
|
+
title="Elevation / m",
|
514
|
+
),
|
513
515
|
alt.Color("segment_id:N", title="Segment"),
|
514
516
|
)
|
515
517
|
.interactive(bind_y=False)
|
@@ -44,7 +44,7 @@ def register_entry_views(
|
|
44
44
|
.order_by(Activity.start.desc())
|
45
45
|
.limit(100)
|
46
46
|
):
|
47
|
-
context["latest_activities"][activity.
|
47
|
+
context["latest_activities"][activity.start_local_tz.date()].append(
|
48
48
|
{
|
49
49
|
"activity": activity,
|
50
50
|
"line_geojson": make_geojson_from_time_series(
|
@@ -2,7 +2,6 @@ import abc
|
|
2
2
|
import datetime
|
3
3
|
import hashlib
|
4
4
|
import io
|
5
|
-
import itertools
|
6
5
|
import logging
|
7
6
|
from collections.abc import Iterable
|
8
7
|
from typing import Union
|
@@ -13,10 +12,8 @@ import matplotlib
|
|
13
12
|
import matplotlib.pyplot as pl
|
14
13
|
import numpy as np
|
15
14
|
import pandas as pd
|
16
|
-
import sqlalchemy
|
17
15
|
from flask import Blueprint
|
18
16
|
from flask import flash
|
19
|
-
from flask import json
|
20
17
|
from flask import redirect
|
21
18
|
from flask import render_template
|
22
19
|
from flask import request
|
@@ -24,7 +21,6 @@ from flask import Response
|
|
24
21
|
from flask import url_for
|
25
22
|
from flask.typing import ResponseReturnValue
|
26
23
|
|
27
|
-
from ...core.activities import ActivityRepository
|
28
24
|
from ...core.config import ConfigAccessor
|
29
25
|
from ...core.coordinates import Bounds
|
30
26
|
from ...core.datamodel import Activity
|
@@ -34,8 +30,6 @@ from ...core.raster_map import TileGetter
|
|
34
30
|
from ...core.tiles import compute_tile
|
35
31
|
from ...core.tiles import get_tile_upper_left_lat_lon
|
36
32
|
from ...explorer.grid_file import get_border_tiles
|
37
|
-
from ...explorer.grid_file import make_explorer_rectangle
|
38
|
-
from ...explorer.grid_file import make_explorer_tile
|
39
33
|
from ...explorer.grid_file import make_grid_file_geojson
|
40
34
|
from ...explorer.grid_file import make_grid_file_gpx
|
41
35
|
from ...explorer.grid_file import make_grid_points
|
@@ -231,7 +225,7 @@ def make_explorer_blueprint(
|
|
231
225
|
tile_evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
232
226
|
tile_history = tile_visit_accessor.tile_state["tile_history"][zoom]
|
233
227
|
|
234
|
-
medians = tile_history.median()
|
228
|
+
medians = tile_history[["tile_x", "tile_y"]].median()
|
235
229
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
236
230
|
medians["tile_x"], medians["tile_y"], zoom
|
237
231
|
)
|
@@ -51,7 +51,7 @@ def make_heatmap_blueprint(
|
|
51
51
|
|
52
52
|
zoom = 14
|
53
53
|
tiles = tile_histories[zoom]
|
54
|
-
medians = tiles.median(skipna=True)
|
54
|
+
medians = tiles[["tile_x", "tile_y"]].median(skipna=True)
|
55
55
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
56
56
|
medians["tile_x"], medians["tile_y"], zoom
|
57
57
|
)
|
@@ -162,7 +162,7 @@ def _get_counts(
|
|
162
162
|
)
|
163
163
|
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
164
164
|
parsed_activities.clear()
|
165
|
-
for activity_id in activity_ids:
|
165
|
+
for activity_id in list(activity_ids):
|
166
166
|
if activity_id in parsed_activities:
|
167
167
|
continue
|
168
168
|
parsed_activities.add(activity_id)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
|
-
import shutil
|
4
3
|
import urllib.parse
|
5
4
|
from typing import Any
|
6
5
|
from typing import Optional
|
@@ -21,11 +20,8 @@ from ...core.datamodel import DB
|
|
21
20
|
from ...core.datamodel import Equipment
|
22
21
|
from ...core.datamodel import Kind
|
23
22
|
from ...core.datamodel import Tag
|
24
|
-
from ...core.enrichment import
|
25
|
-
from ...core.enrichment import update_via_time_series
|
23
|
+
from ...core.enrichment import update_and_commit
|
26
24
|
from ...core.heart_rate import HeartRateZoneComputer
|
27
|
-
from ...core.paths import _activity_enriched_dir
|
28
|
-
from ...core.paths import TIME_SERIES_DIR
|
29
25
|
from ..authenticator import Authenticator
|
30
26
|
from ..authenticator import needs_authentication
|
31
27
|
from ..flasher import Flasher
|
@@ -357,15 +353,8 @@ def make_settings_blueprint(
|
|
357
353
|
DB.session.scalars(sqlalchemy.select(Activity)).all(),
|
358
354
|
desc="Recomputing segments",
|
359
355
|
):
|
360
|
-
time_series =
|
361
|
-
|
362
|
-
None,
|
363
|
-
threshold,
|
364
|
-
)
|
365
|
-
update_via_time_series(activity, time_series)
|
366
|
-
enriched_time_series_path = TIME_SERIES_DIR() / f"{activity.id}.parquet"
|
367
|
-
time_series.to_parquet(enriched_time_series_path)
|
368
|
-
DB.session.commit()
|
356
|
+
time_series = activity.time_series
|
357
|
+
update_and_commit(activity, time_series, config_accessor())
|
369
358
|
return render_template(
|
370
359
|
"settings/segmentation.html.j2",
|
371
360
|
threshold=config_accessor().time_diff_threshold_seconds,
|
@@ -178,10 +178,10 @@ def make_summary_blueprint(
|
|
178
178
|
def index():
|
179
179
|
query = search_query_from_form(request.args)
|
180
180
|
search_query_history.register_query(query)
|
181
|
-
|
181
|
+
df = apply_search_query(repository.meta, query)
|
182
182
|
|
183
183
|
kind_scale = make_kind_scale(repository.meta, config)
|
184
|
-
|
184
|
+
df_without_nan = df.loc[~pd.isna(df["start"])]
|
185
185
|
|
186
186
|
return render_template(
|
187
187
|
"summary/index.html.j2",
|
@@ -191,19 +191,19 @@ def make_summary_blueprint(
|
|
191
191
|
for spec in DB.session.scalars(sqlalchemy.select(PlotSpec)).all()
|
192
192
|
],
|
193
193
|
plot_per_year_per_kind={
|
194
|
-
column.display_name: plot_per_year_per_kind(
|
194
|
+
column.display_name: plot_per_year_per_kind(df_without_nan, column)
|
195
195
|
for column in META_COLUMNS
|
196
196
|
},
|
197
197
|
plot_per_year_cumulative={
|
198
|
-
column.display_name: plot_year_cumulative(
|
198
|
+
column.display_name: plot_year_cumulative(df_without_nan, column)
|
199
199
|
for column in META_COLUMNS
|
200
200
|
},
|
201
201
|
plot_per_iso_week={
|
202
|
-
column.display_name: plot_per_iso_week(
|
202
|
+
column.display_name: plot_per_iso_week(df_without_nan, column)
|
203
203
|
for column in META_COLUMNS
|
204
204
|
},
|
205
205
|
heatmap_per_day={
|
206
|
-
column.display_name: heatmap_per_day(
|
206
|
+
column.display_name: heatmap_per_day(df_without_nan, column)
|
207
207
|
for column in META_COLUMNS
|
208
208
|
},
|
209
209
|
)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import sqlalchemy
|
4
|
+
from flask import Blueprint
|
5
|
+
from flask import redirect
|
6
|
+
from flask import render_template
|
7
|
+
from flask import Response
|
8
|
+
from flask import url_for
|
9
|
+
|
10
|
+
from ...core.config import Config
|
11
|
+
from ...core.datamodel import Activity
|
12
|
+
from ...core.datamodel import DB
|
13
|
+
from ...core.enrichment import enrichment_set_timezone
|
14
|
+
from ...core.enrichment import update_and_commit
|
15
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
16
|
+
from ..authenticator import Authenticator
|
17
|
+
from ..authenticator import needs_authentication
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
def make_time_zone_fixer_blueprint(
|
23
|
+
authenticator: Authenticator, config: Config, tile_visit_accessor: TileVisitAccessor
|
24
|
+
) -> Blueprint:
|
25
|
+
|
26
|
+
blueprint = Blueprint("time_zone_fixer", __name__, template_folder="templates")
|
27
|
+
|
28
|
+
@blueprint.route("/")
|
29
|
+
def index() -> str:
|
30
|
+
return render_template("time_zone_fixer/index.html.j2")
|
31
|
+
|
32
|
+
@blueprint.route("/local-to-utc")
|
33
|
+
@needs_authentication(authenticator)
|
34
|
+
def local_to_utc():
|
35
|
+
convert(True)
|
36
|
+
return redirect(url_for("index"))
|
37
|
+
|
38
|
+
@blueprint.route("/utc-to-utc")
|
39
|
+
@needs_authentication(authenticator)
|
40
|
+
def utc_to_utc():
|
41
|
+
convert(False)
|
42
|
+
return redirect(url_for("index"))
|
43
|
+
|
44
|
+
def convert(from_iana: bool) -> None:
|
45
|
+
for activity in DB.session.scalars(sqlalchemy.select(Activity)).all():
|
46
|
+
if activity.start is None:
|
47
|
+
continue
|
48
|
+
|
49
|
+
logger.info(f"Changing time zone for {activity.name} …")
|
50
|
+
|
51
|
+
time_series = activity.raw_time_series
|
52
|
+
enrichment_set_timezone(activity, time_series, config)
|
53
|
+
if time_series["time"].dt.tz is None:
|
54
|
+
time_series["time"] = time_series["time"].dt.tz_localize(
|
55
|
+
activity.iana_timezone if from_iana else "UTC"
|
56
|
+
)
|
57
|
+
time_series["time"] = time_series["time"].dt.tz_convert("UTC")
|
58
|
+
update_and_commit(activity, time_series, config)
|
59
|
+
|
60
|
+
@blueprint.route("/truncate-activities")
|
61
|
+
@needs_authentication(authenticator)
|
62
|
+
def truncate_activities():
|
63
|
+
DB.session.query(Activity).delete()
|
64
|
+
DB.session.commit()
|
65
|
+
tile_visit_accessor.reset()
|
66
|
+
tile_visit_accessor.save()
|
67
|
+
return redirect(url_for("upload.reload"))
|
68
|
+
|
69
|
+
return blueprint
|
@@ -13,13 +13,9 @@ from ...core.activities import ActivityRepository
|
|
13
13
|
from ...core.config import Config
|
14
14
|
from ...core.datamodel import Activity
|
15
15
|
from ...core.datamodel import DB
|
16
|
-
from ...core.datamodel import Kind
|
17
|
-
from ...core.enrichment import populate_database_from_extracted
|
18
|
-
from ...core.tasks import work_tracker_path
|
19
16
|
from ...explorer.tile_visits import compute_tile_evolution
|
20
17
|
from ...explorer.tile_visits import compute_tile_visits_new
|
21
18
|
from ...explorer.tile_visits import TileVisitAccessor
|
22
|
-
from ...importers.directory import get_file_hash
|
23
19
|
from ...importers.directory import import_from_directory
|
24
20
|
from ...importers.strava_api import import_from_strava_api
|
25
21
|
from ...importers.strava_checkout import import_from_strava_checkout
|
@@ -122,22 +118,13 @@ def scan_for_activities(
|
|
122
118
|
skip_strava: bool = False,
|
123
119
|
) -> None:
|
124
120
|
if pathlib.Path("Activities").exists():
|
125
|
-
import_from_directory(
|
121
|
+
import_from_directory(repository, tile_visit_accessor, config)
|
126
122
|
if pathlib.Path("Strava Export").exists():
|
127
|
-
import_from_strava_checkout()
|
123
|
+
import_from_strava_checkout(config)
|
128
124
|
if config.strava_client_code and not skip_strava:
|
129
|
-
import_from_strava_api(config)
|
130
|
-
|
131
|
-
populate_database_from_extracted(config)
|
125
|
+
import_from_strava_api(config, repository, tile_visit_accessor)
|
132
126
|
|
133
127
|
if len(repository) > 0:
|
134
|
-
kinds = DB.session.scalars(sqlalchemy.select(Kind)).all()
|
135
|
-
if all(kind.consider_for_achievements == False for kind in kinds):
|
136
|
-
for kind in kinds:
|
137
|
-
kind.consider_for_achievements = True
|
138
|
-
DB.session.commit()
|
139
|
-
tile_visit_accessor.reset()
|
140
|
-
work_tracker_path("tile-state").unlink(missing_ok=True)
|
141
128
|
compute_tile_visits_new(repository, tile_visit_accessor)
|
142
129
|
compute_tile_evolution(tile_visit_accessor.tile_state, config)
|
143
130
|
tile_visit_accessor.save()
|
@@ -60,6 +60,7 @@ column_speed = ColumnDescription(
|
|
60
60
|
unit="km/h",
|
61
61
|
format=".1f",
|
62
62
|
)
|
63
|
+
|
63
64
|
column_elevation = ColumnDescription(
|
64
65
|
name="elevation",
|
65
66
|
display_name="Elevation",
|
@@ -67,4 +68,11 @@ column_elevation = ColumnDescription(
|
|
67
68
|
format=".0f",
|
68
69
|
)
|
69
70
|
|
70
|
-
|
71
|
+
column_copernicus_elevation = ColumnDescription(
|
72
|
+
name="copernicus_elevation",
|
73
|
+
display_name="Elevation (Copernicus DEM)",
|
74
|
+
unit="m",
|
75
|
+
format=".0f",
|
76
|
+
)
|
77
|
+
|
78
|
+
TIME_SERIES_COLUMNS = [column_speed, column_elevation, column_copernicus_elevation]
|
@@ -44,7 +44,7 @@
|
|
44
44
|
(60/activity.average_speed_elapsed_kmh)|round(1) }} min/km</dd>
|
45
45
|
<dt>Start time</dt>
|
46
46
|
<dd><a href="{{ url_for('activity.day', year=date.year, month=date.month, day=date.day) }}">{{ date }}</a>
|
47
|
-
{{ time }}
|
47
|
+
{{ time }} {{ activity.iana_timezone }}
|
48
48
|
</dd>
|
49
49
|
{% endif %}
|
50
50
|
|
@@ -79,6 +79,8 @@
|
|
79
79
|
<dd>{{ activity.upstream_id }}</dd>
|
80
80
|
<dt>Source path</dt>
|
81
81
|
<dd><a href="{{ url_for('.download_original', id=activity.id) }}">{{ activity.path }}</a></dd>
|
82
|
+
<dt>Time series path</dt>
|
83
|
+
<dd>{{ activity.time_series_path }}</dd>
|
82
84
|
</dl>
|
83
85
|
|
84
86
|
<a href="{{ url_for('.edit', id=activity['id']) }}" class="btn btn-secondary btn-small">Edit</a>
|
@@ -48,8 +48,9 @@
|
|
48
48
|
<p class="card-text">
|
49
49
|
{{ elem.activity.emoji_string }}
|
50
50
|
</p>
|
51
|
-
{% if elem.activity.
|
52
|
-
<p class="card-text"><small class="text-body-secondary">{{ elem.activity.
|
51
|
+
{% if elem.activity.start_local_tz %}
|
52
|
+
<p class="card-text"><small class="text-body-secondary">{{ elem.activity.start_local_tz|dt }} {{
|
53
|
+
elem.activity.iana_timezone }}</small></p>
|
53
54
|
{% endif %}
|
54
55
|
</div>
|
55
56
|
</div>
|
@@ -151,6 +151,8 @@
|
|
151
151
|
</li>
|
152
152
|
<li><a class="dropdown-item" href="{{ url_for('settings.index') }}">Settings</a></li>
|
153
153
|
<li><a class="dropdown-item" href="{{ url_for('export.index') }}">Data Export</a></li>
|
154
|
+
<li><a class="dropdown-item" href="{{ url_for('time_zone_fixer.index') }}">Time Zone
|
155
|
+
Fixer</a></li>
|
154
156
|
</ul>
|
155
157
|
</li>
|
156
158
|
|