geo-activity-playground 1.1.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 +78 -22
- 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/photo_blueprint.py +65 -56
- geo_activity_playground/webui/blueprints/settings_blueprint.py +20 -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/equipment/index.html.j2 +3 -3
- geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +2 -3
- geo_activity_playground/webui/templates/home.html.j2 +4 -10
- geo_activity_playground/webui/templates/page.html.j2 +2 -0
- geo_activity_playground/webui/templates/photo/new.html.j2 +1 -1
- geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
- geo_activity_playground/webui/templates/settings/tile-source.html.j2 +33 -0
- geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2 +31 -0
- {geo_activity_playground-1.1.0.dist-info → geo_activity_playground-1.3.0.dist-info}/METADATA +7 -3
- {geo_activity_playground-1.1.0.dist-info → geo_activity_playground-1.3.0.dist-info}/RECORD +43 -34
- geo_activity_playground/core/test_time_conversion.py +0 -37
- {geo_activity_playground-1.1.0.dist-info → geo_activity_playground-1.3.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-1.1.0.dist-info → geo_activity_playground-1.3.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-1.1.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)
|
@@ -12,6 +12,7 @@ from flask import render_template
|
|
12
12
|
from flask import request
|
13
13
|
from flask import Response
|
14
14
|
from flask import url_for
|
15
|
+
from flask.typing import ResponseReturnValue
|
15
16
|
from PIL import Image
|
16
17
|
from PIL import ImageOps
|
17
18
|
|
@@ -123,7 +124,7 @@ def make_photo_blueprint(
|
|
123
124
|
|
124
125
|
@blueprint.route("/new", methods=["GET", "POST"])
|
125
126
|
@needs_authentication(authenticator)
|
126
|
-
def new() ->
|
127
|
+
def new() -> ResponseReturnValue:
|
127
128
|
if request.method == "POST":
|
128
129
|
# check if the post request has the file part
|
129
130
|
if "file" not in request.files:
|
@@ -132,66 +133,74 @@ def make_photo_blueprint(
|
|
132
133
|
)
|
133
134
|
return redirect(url_for(".new"))
|
134
135
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
136
|
+
new_photos: list[Photo] = []
|
137
|
+
for file in request.files.getlist("file"):
|
138
|
+
# If the user does not select a file, the browser submits an
|
139
|
+
# empty file without a filename.
|
140
|
+
if file.filename == "":
|
141
|
+
flasher.flash_message("No selected file.", FlashTypes.WARNING)
|
142
|
+
return redirect(url_for(".new"))
|
143
|
+
if not file:
|
144
|
+
flasher.flash_message("Empty file uploaded.", FlashTypes.WARNING)
|
145
|
+
return redirect(url_for(".new"))
|
146
|
+
|
147
|
+
filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix
|
148
|
+
path = PHOTOS_DIR() / "original" / filename
|
149
|
+
path.parent.mkdir(exist_ok=True)
|
150
|
+
file.save(path)
|
151
|
+
metadata = get_metadata_from_image(path)
|
152
|
+
|
153
|
+
if "time" not in metadata:
|
154
|
+
flasher.flash_message(
|
155
|
+
f"Your image '{file.filename}' doesn't have the EXIF attribute 'EXIF DateTimeOriginal' and hence cannot be dated.",
|
156
|
+
FlashTypes.DANGER,
|
157
|
+
)
|
158
|
+
continue
|
159
|
+
time: datetime.datetime = metadata["time"]
|
160
|
+
|
161
|
+
activity = DB.session.scalar(
|
162
|
+
sqlalchemy.select(Activity)
|
163
|
+
.where(
|
164
|
+
Activity.start.is_not(None),
|
165
|
+
Activity.elapsed_time.is_not(None),
|
166
|
+
Activity.start <= time,
|
167
|
+
)
|
168
|
+
.order_by(Activity.start.desc())
|
169
|
+
.limit(1)
|
170
|
+
)
|
171
|
+
if activity is None or activity.start + activity.elapsed_time < time:
|
172
|
+
flasher.flash_message(
|
173
|
+
f"Your image '{file.filename}' is from {time} but no activity could be found. Please first upload an activity or fix the time in the photo.",
|
174
|
+
FlashTypes.DANGER,
|
175
|
+
)
|
176
|
+
continue
|
177
|
+
|
178
|
+
if "latitude" not in metadata:
|
179
|
+
time_series = activity.time_series
|
180
|
+
print(time_series)
|
181
|
+
row = time_series.loc[time_series["time"] >= time].iloc[0]
|
182
|
+
metadata["latitude"] = row["latitude"]
|
183
|
+
metadata["longitude"] = row["longitude"]
|
184
|
+
|
185
|
+
photo = Photo(
|
186
|
+
filename=filename,
|
187
|
+
time=time,
|
188
|
+
latitude=metadata["latitude"],
|
189
|
+
longitude=metadata["longitude"],
|
190
|
+
activity=activity,
|
191
|
+
)
|
144
192
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
file.save(path)
|
149
|
-
metadata = get_metadata_from_image(path)
|
193
|
+
DB.session.add(photo)
|
194
|
+
DB.session.commit()
|
195
|
+
new_photos.append(photo)
|
150
196
|
|
151
|
-
if
|
197
|
+
if new_photos:
|
152
198
|
flasher.flash_message(
|
153
|
-
"
|
154
|
-
FlashTypes.DANGER,
|
199
|
+
f"Added {len(new_photos)} new photos.", FlashTypes.SUCCESS
|
155
200
|
)
|
201
|
+
return redirect(f"/activity/{new_photos[-1].activity.id}")
|
202
|
+
else:
|
156
203
|
return redirect(url_for(".new"))
|
157
|
-
time: datetime.datetime = metadata["time"]
|
158
|
-
|
159
|
-
activity = DB.session.scalar(
|
160
|
-
sqlalchemy.select(Activity)
|
161
|
-
.where(
|
162
|
-
Activity.start.is_not(None),
|
163
|
-
Activity.elapsed_time.is_not(None),
|
164
|
-
Activity.start <= time,
|
165
|
-
)
|
166
|
-
.order_by(Activity.start.desc())
|
167
|
-
.limit(1)
|
168
|
-
)
|
169
|
-
if activity is None or activity.start + activity.elapsed_time < time:
|
170
|
-
flasher.flash_message(
|
171
|
-
f"Your image is from {time} but no activity could be found. Please first upload an activity or fix the time in the photo",
|
172
|
-
FlashTypes.DANGER,
|
173
|
-
)
|
174
|
-
print(activity)
|
175
|
-
|
176
|
-
if "latitude" not in metadata:
|
177
|
-
time_series = activity.time_series
|
178
|
-
print(time_series)
|
179
|
-
row = time_series.loc[time_series["time"] >= time].iloc[0]
|
180
|
-
metadata["latitude"] = row["latitude"]
|
181
|
-
metadata["longitude"] = row["longitude"]
|
182
|
-
|
183
|
-
photo = Photo(
|
184
|
-
filename=filename,
|
185
|
-
time=time,
|
186
|
-
latitude=metadata["latitude"],
|
187
|
-
longitude=metadata["longitude"],
|
188
|
-
activity=activity,
|
189
|
-
)
|
190
|
-
|
191
|
-
DB.session.add(photo)
|
192
|
-
DB.session.commit()
|
193
|
-
|
194
|
-
return redirect(f"/activity/{activity.id}")
|
195
204
|
else:
|
196
205
|
return render_template("photo/new.html.j2")
|
197
206
|
|
@@ -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,
|
@@ -447,6 +436,23 @@ def make_settings_blueprint(
|
|
447
436
|
else:
|
448
437
|
return render_template("settings/tags-edit.html.j2", tag=tag)
|
449
438
|
|
439
|
+
@blueprint.route("/tile-source", methods=["GET", "POST"])
|
440
|
+
@needs_authentication(authenticator)
|
441
|
+
def tile_source() -> str:
|
442
|
+
if request.method == "POST":
|
443
|
+
config_accessor().map_tile_url = request.form["map_tile_url"]
|
444
|
+
config_accessor().map_tile_attribution = request.form[
|
445
|
+
"map_tile_attribution"
|
446
|
+
]
|
447
|
+
config_accessor.save()
|
448
|
+
flasher.flash_message("Tile source updated.", FlashTypes.SUCCESS)
|
449
|
+
return render_template(
|
450
|
+
"settings/tile-source.html.j2",
|
451
|
+
map_tile_url=config_accessor().map_tile_url,
|
452
|
+
map_tile_attribution=config_accessor().map_tile_attribution,
|
453
|
+
test_url=config_accessor().map_tile_url.format(zoom=14, x=8514, y=5504),
|
454
|
+
)
|
455
|
+
|
450
456
|
return blueprint
|
451
457
|
|
452
458
|
|
@@ -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
|
)
|