geo-activity-playground 1.2.0__py3-none-any.whl → 1.3.1__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.
Files changed (39) hide show
  1. geo_activity_playground/alembic/versions/85fe0348e8a2_add_time_series_uuid_field.py +28 -0
  2. geo_activity_playground/alembic/versions/f2f50843be2d_make_all_fields_in_activity_nullable.py +34 -0
  3. geo_activity_playground/core/coordinates.py +12 -1
  4. geo_activity_playground/core/copernicus_dem.py +95 -0
  5. geo_activity_playground/core/datamodel.py +43 -16
  6. geo_activity_playground/core/enrichment.py +229 -164
  7. geo_activity_playground/core/paths.py +8 -0
  8. geo_activity_playground/core/test_pandas_timezone.py +36 -0
  9. geo_activity_playground/core/test_time_zone_from_location.py +7 -0
  10. geo_activity_playground/core/test_time_zone_import.py +93 -0
  11. geo_activity_playground/core/test_timezone_sqlalchemy.py +44 -0
  12. geo_activity_playground/core/tiles.py +4 -1
  13. geo_activity_playground/core/time_conversion.py +42 -14
  14. geo_activity_playground/explorer/tile_visits.py +7 -4
  15. geo_activity_playground/importers/activity_parsers.py +31 -23
  16. geo_activity_playground/importers/directory.py +69 -108
  17. geo_activity_playground/importers/strava_api.py +55 -36
  18. geo_activity_playground/importers/strava_checkout.py +32 -57
  19. geo_activity_playground/webui/app.py +46 -2
  20. geo_activity_playground/webui/blueprints/activity_blueprint.py +13 -11
  21. geo_activity_playground/webui/blueprints/entry_views.py +1 -1
  22. geo_activity_playground/webui/blueprints/explorer_blueprint.py +1 -7
  23. geo_activity_playground/webui/blueprints/heatmap_blueprint.py +2 -2
  24. geo_activity_playground/webui/blueprints/settings_blueprint.py +3 -14
  25. geo_activity_playground/webui/blueprints/summary_blueprint.py +6 -6
  26. geo_activity_playground/webui/blueprints/time_zone_fixer_blueprint.py +69 -0
  27. geo_activity_playground/webui/blueprints/upload_blueprint.py +3 -16
  28. geo_activity_playground/webui/columns.py +9 -1
  29. geo_activity_playground/webui/templates/activity/show.html.j2 +5 -1
  30. geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +1 -1
  31. geo_activity_playground/webui/templates/home.html.j2 +3 -2
  32. geo_activity_playground/webui/templates/page.html.j2 +2 -0
  33. geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2 +31 -0
  34. {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.1.dist-info}/METADATA +8 -3
  35. {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.1.dist-info}/RECORD +38 -30
  36. geo_activity_playground/core/test_time_conversion.py +0 -37
  37. {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.1.dist-info}/LICENSE +0 -0
  38. {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.1.dist-info}/WHEEL +0 -0
  39. {geo_activity_playground-1.2.0.dist-info → geo_activity_playground-1.3.1.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,10 @@
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 traceback
6
+ import urllib.parse
7
+ import zoneinfo
8
8
  from typing import Optional
9
9
 
10
10
  import dateutil.parser
@@ -12,17 +12,16 @@ import numpy as np
12
12
  import pandas as pd
13
13
  from tqdm import tqdm
14
14
 
15
- from ..core.datamodel import ActivityMeta
15
+ from ..core.config import Config
16
16
  from ..core.datamodel import DEFAULT_UNKNOWN_NAME
17
+ from ..core.datamodel import get_or_make_equipment
18
+ from ..core.datamodel import get_or_make_kind
19
+ from ..core.enrichment import update_and_commit
17
20
  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
21
  from ..core.tasks import get_state
21
22
  from ..core.tasks import set_state
22
23
  from ..core.tasks import work_tracker_path
23
24
  from ..core.tasks import WorkTracker
24
- from ..core.time_conversion import convert_to_datetime_ns
25
- from .activity_parsers import ActivityParseError
26
25
  from .activity_parsers import read_activity
27
26
  from .csv_parser import parse_csv
28
27
 
@@ -144,7 +143,7 @@ def float_with_comma_or_period(x: str) -> Optional[float]:
144
143
  return float(x)
145
144
 
146
145
 
147
- def import_from_strava_checkout() -> None:
146
+ def import_from_strava_checkout(config: Config) -> None:
148
147
  checkout_path = pathlib.Path("Strava Export")
149
148
  with open(checkout_path / "activities.csv", encoding="utf-8") as f:
150
149
  rows = parse_csv(f.read())
@@ -191,47 +190,13 @@ def import_from_strava_checkout() -> None:
191
190
  if not row["Filename"]:
192
191
  continue
193
192
 
194
- start_datetime = dateutil.parser.parse(row["Activity Date"], dayfirst=dayfirst)
193
+ start_datetime = dateutil.parser.parse(
194
+ row["Activity Date"], dayfirst=dayfirst
195
+ ).replace(tzinfo=zoneinfo.ZoneInfo("utc"))
195
196
 
196
197
  activity_file = checkout_path / row["Filename"]
197
- table_activity_meta: ActivityMeta = {
198
- "calories": float_with_comma_or_period(row["Calories"]),
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
198
+
199
+ activity, time_series = read_activity(activity_file)
235
200
 
236
201
  if not len(time_series):
237
202
  continue
@@ -239,16 +204,26 @@ def import_from_strava_checkout() -> None:
239
204
  if "latitude" not in time_series.columns:
240
205
  continue
241
206
 
242
- meta_path = activity_extracted_meta_dir() / f"{activity_id}.pickle"
243
- with open(meta_path, "wb") as f:
244
- pickle.dump(table_activity_meta, f)
245
- time_series.to_parquet(time_series_path)
246
-
247
- start_str = (
248
- pd.Timestamp(table_activity_meta["start"]).to_pydatetime().isoformat() + "Z"
207
+ activity.upstream_id = activity_id
208
+ activity.calories = float_with_comma_or_period(row["Calories"])
209
+ activity.distance_km = row["Distance"]
210
+ activity.elapsed_time = datetime.timedelta(
211
+ seconds=float_with_comma_or_period(row["Elapsed Time"])
212
+ )
213
+ activity.equipment = get_or_make_equipment(
214
+ nan_as_none(row["Activity Gear"])
215
+ or nan_as_none(row["Bike"])
216
+ or nan_as_none(row["Gear"])
217
+ or DEFAULT_UNKNOWN_NAME,
218
+ config,
249
219
  )
250
- latest_start_str = get_state(strava_last_activity_date_path(), start_str)
251
- set_state(strava_last_activity_date_path(), max(start_str, latest_start_str))
220
+ activity.kind = get_or_make_kind(row["Activity Type"])
221
+ activity.name = row["Activity Name"]
222
+ activity.path = str(activity_file)
223
+ activity.start = start_datetime
224
+ activity.steps = float_with_comma_or_period(row["Total Steps"])
225
+
226
+ update_and_commit(activity, time_series, config)
252
227
 
253
228
  work_tracker.close()
254
229
 
@@ -292,7 +267,7 @@ def convert_strava_checkout(
292
267
  "-",
293
268
  f"{activity_date.hour:02d}-{activity_date.minute:02d}-{activity_date.second:02d}",
294
269
  " ",
295
- activity_name.replace("/", "_"),
270
+ urllib.parse.quote_plus(activity_name),
296
271
  ]
297
272
  + activity_file.suffixes
298
273
  )
@@ -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,27 @@ 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
+ if not activity.time_series_path.exists():
135
+ logger.error(
136
+ f"Time series for {activity.id=}, expected at {activity.time_series_path}, does not exist. Deleting activity."
137
+ )
138
+ DB.session.delete(activity)
139
+ DB.session.commit()
140
+
103
141
  if not skip_reload:
104
- with app.app_context():
105
- scan_for_activities(repository, tile_visit_accessor, config_accessor())
142
+ thread = threading.Thread(
143
+ target=importer_thread,
144
+ args=(app, repository, tile_visit_accessor, config_accessor()),
145
+ )
146
+ thread.start()
106
147
 
107
148
  app.config["UPLOAD_FOLDER"] = "Activities"
108
149
  app.secret_key = get_secret_key()
@@ -183,6 +224,9 @@ def web_ui_main(
183
224
  ),
184
225
  "/summary": make_summary_blueprint(repository, config, search_query_history),
185
226
  "/tile": make_tile_blueprint(image_transforms, tile_getter),
227
+ "/time-zone-fixer": make_time_zone_fixer_blueprint(
228
+ authenticator, config, tile_visit_accessor
229
+ ),
186
230
  "/upload": make_upload_blueprint(
187
231
  repository, tile_visit_accessor, config_accessor(), authenticator, flasher
188
232
  ),
@@ -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 update_via_time_series
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 column_elevation
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 [column_speed, column_elevation]]
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.start.date(),
169
- "time": activity.start.time(),
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 "elevation" in time_series.columns:
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
- update_via_time_series(activity, activity.time_series)
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("elevation", scale=alt.Scale(zero=False), title="Elevation / m"),
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.start.date()].append(
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 _embellish_single_time_series
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 = _embellish_single_time_series(
361
- activity.raw_time_series,
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
- activities = apply_search_query(repository.meta, query)
181
+ df = apply_search_query(repository.meta, query)
182
182
 
183
183
  kind_scale = make_kind_scale(repository.meta, config)
184
- df = activities
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(df, column)
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(df, column)
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(df, column)
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(df, column)
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(config.metadata_extraction_regexes, config)
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
- TIME_SERIES_COLUMNS = [column_speed, column_elevation]
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]
@@ -42,9 +42,11 @@
42
42
  <dt>Average elapsed speed</dt>
43
43
  <dd>{{ activity.average_speed_elapsed_kmh|round(1) }} km/h = {{
44
44
  (60/activity.average_speed_elapsed_kmh)|round(1) }} min/km</dd>
45
+ {% endif %}
46
+ {% if date is defined %}
45
47
  <dt>Start time</dt>
46
48
  <dd><a href="{{ url_for('activity.day', year=date.year, month=date.month, day=date.day) }}">{{ date }}</a>
47
- {{ time }}
49
+ {{ time }} {{ activity.iana_timezone }}
48
50
  </dd>
49
51
  {% endif %}
50
52
 
@@ -79,6 +81,8 @@
79
81
  <dd>{{ activity.upstream_id }}</dd>
80
82
  <dt>Source path</dt>
81
83
  <dd><a href="{{ url_for('.download_original', id=activity.id) }}">{{ activity.path }}</a></dd>
84
+ <dt>Time series path</dt>
85
+ <dd>{{ activity.time_series_path }}</dd>
82
86
  </dl>
83
87
 
84
88
  <a href="{{ url_for('.edit', id=activity['id']) }}" class="btn btn-secondary btn-small">Edit</a>
@@ -45,7 +45,7 @@
45
45
  </ul>
46
46
  </p>
47
47
  <p class="card-text"><small class="text-body-secondary"></small>{{ activity.emoji_string }} on {{
48
- activity.start|dt }}</small></p>
48
+ activity.start_local_tz|dt }} {{ activity.iana_timezone }}</small></p>
49
49
  </div>
50
50
  </div>
51
51
  </div>
@@ -48,8 +48,9 @@
48
48
  <p class="card-text">
49
49
  {{ elem.activity.emoji_string }}
50
50
  </p>
51
- {% if elem.activity.start %}
52
- <p class="card-text"><small class="text-body-secondary">{{ elem.activity.start|dt }}</small></p>
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