geo-activity-playground 0.41.0__py3-none-any.whl → 0.43.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/script.py.mako +0 -6
- geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +40 -0
- geo_activity_playground/alembic/versions/script.py.mako +6 -0
- geo_activity_playground/core/activities.py +7 -15
- geo_activity_playground/core/datamodel.py +91 -85
- geo_activity_playground/core/enrichment.py +15 -6
- geo_activity_playground/core/meta_search.py +78 -34
- geo_activity_playground/core/missing_values.py +16 -0
- geo_activity_playground/core/paths.py +2 -0
- geo_activity_playground/core/test_missing_values.py +19 -0
- geo_activity_playground/explorer/tile_visits.py +1 -1
- geo_activity_playground/webui/app.py +22 -8
- geo_activity_playground/webui/blueprints/activity_blueprint.py +18 -10
- geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
- geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
- geo_activity_playground/webui/search_util.py +23 -7
- geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
- geo_activity_playground/webui/templates/activity/show.html.j2 +56 -12
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +1 -2
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +1 -2
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +18 -15
- geo_activity_playground/webui/templates/heatmap/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/page.html.j2 +8 -0
- geo_activity_playground/webui/templates/photo/map.html.j2 +45 -0
- geo_activity_playground/webui/templates/photo/new.html.j2 +13 -0
- geo_activity_playground/webui/templates/search/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/search_form.html.j2 +47 -22
- geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
- geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
- geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
- geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +12 -10
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +3 -1
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +37 -28
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +1 -1
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -8,6 +8,7 @@ import secrets
|
|
8
8
|
import shutil
|
9
9
|
import urllib.parse
|
10
10
|
|
11
|
+
import sqlalchemy
|
11
12
|
from flask import Flask
|
12
13
|
from flask import request
|
13
14
|
from flask_alembic import Alembic
|
@@ -17,6 +18,10 @@ from ..core.config import ConfigAccessor
|
|
17
18
|
from ..core.config import import_old_config
|
18
19
|
from ..core.config import import_old_strava_config
|
19
20
|
from ..core.datamodel import DB
|
21
|
+
from ..core.datamodel import Equipment
|
22
|
+
from ..core.datamodel import Kind
|
23
|
+
from ..core.datamodel import Photo
|
24
|
+
from ..core.datamodel import Tag
|
20
25
|
from ..core.heart_rate import HeartRateZoneComputer
|
21
26
|
from ..core.raster_map import GrayscaleImageTransform
|
22
27
|
from ..core.raster_map import IdentityImageTransform
|
@@ -33,6 +38,7 @@ from .blueprints.entry_views import register_entry_views
|
|
33
38
|
from .blueprints.equipment_blueprint import make_equipment_blueprint
|
34
39
|
from .blueprints.explorer_blueprint import make_explorer_blueprint
|
35
40
|
from .blueprints.heatmap_blueprint import make_heatmap_blueprint
|
41
|
+
from .blueprints.photo_blueprint import make_photo_blueprint
|
36
42
|
from .blueprints.plot_builder_blueprint import make_plot_builder_blueprint
|
37
43
|
from .blueprints.search_blueprint import make_search_blueprint
|
38
44
|
from .blueprints.settings_blueprint import make_settings_blueprint
|
@@ -70,7 +76,7 @@ def web_ui_main(
|
|
70
76
|
|
71
77
|
app = Flask(__name__)
|
72
78
|
|
73
|
-
database_path =
|
79
|
+
database_path = pathlib.Path("database.sqlite")
|
74
80
|
logger.info(f"Using database file at '{database_path.absolute()}'.")
|
75
81
|
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{database_path.absolute()}"
|
76
82
|
app.config["ALEMBIC"] = {"script_location": "../alembic/versions"}
|
@@ -146,6 +152,7 @@ def web_ui_main(
|
|
146
152
|
"/heatmap": make_heatmap_blueprint(
|
147
153
|
repository, tile_visit_accessor, config_accessor(), search_query_history
|
148
154
|
),
|
155
|
+
"/photo": make_photo_blueprint(config_accessor, authenticator, flasher),
|
149
156
|
"/plot-builder": make_plot_builder_blueprint(
|
150
157
|
repository, flasher, authenticator
|
151
158
|
),
|
@@ -178,15 +185,22 @@ def web_ui_main(
|
|
178
185
|
"version": _try_get_version(),
|
179
186
|
"num_activities": len(repository),
|
180
187
|
"map_tile_attribution": config_accessor().map_tile_attribution,
|
181
|
-
"search_query_favorites": search_query_history.prepare_favorites(),
|
182
|
-
"search_query_last": search_query_history.prepare_last(),
|
188
|
+
# "search_query_favorites": search_query_history.prepare_favorites(),
|
189
|
+
# "search_query_last": search_query_history.prepare_last(),
|
183
190
|
"request_url": urllib.parse.quote_plus(request.url),
|
184
191
|
}
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
192
|
+
variables["equipments_avail"] = DB.session.scalars(
|
193
|
+
sqlalchemy.select(Equipment).order_by(Equipment.name)
|
194
|
+
).all()
|
195
|
+
variables["kinds_avail"] = DB.session.scalars(
|
196
|
+
sqlalchemy.select(Kind).order_by(Kind.name)
|
197
|
+
).all()
|
198
|
+
variables["tags_avail"] = DB.session.scalars(
|
199
|
+
sqlalchemy.select(Tag).order_by(Tag.tag)
|
200
|
+
).all()
|
201
|
+
variables["photo_count"] = DB.session.scalar(
|
202
|
+
sqlalchemy.select(sqlalchemy.func.count()).select_from(Photo)
|
203
|
+
)
|
190
204
|
return variables
|
191
205
|
|
192
206
|
app.run(host=host, port=port)
|
@@ -29,6 +29,7 @@ from ...core.datamodel import Activity
|
|
29
29
|
from ...core.datamodel import DB
|
30
30
|
from ...core.datamodel import Equipment
|
31
31
|
from ...core.datamodel import Kind
|
32
|
+
from ...core.datamodel import Tag
|
32
33
|
from ...core.enrichment import update_via_time_series
|
33
34
|
from ...core.heart_rate import HeartRateZoneComputer
|
34
35
|
from ...core.privacy_zones import PrivacyZone
|
@@ -71,14 +72,14 @@ def make_activity_blueprint(
|
|
71
72
|
)
|
72
73
|
]
|
73
74
|
for _, group in repository.get_time_series(
|
74
|
-
activity
|
75
|
+
activity.id
|
75
76
|
).groupby("segment_id")
|
76
77
|
]
|
77
78
|
),
|
78
79
|
properties={
|
79
80
|
"color": matplotlib.colors.to_hex(cmap(i % 8)),
|
80
|
-
"activity_name": activity
|
81
|
-
"activity_id": str(activity
|
81
|
+
"activity_name": activity.name,
|
82
|
+
"activity_id": str(activity.id),
|
82
83
|
},
|
83
84
|
)
|
84
85
|
for i, activity in enumerate(repository.iter_activities())
|
@@ -99,7 +100,7 @@ def make_activity_blueprint(
|
|
99
100
|
|
100
101
|
meta = repository.meta
|
101
102
|
similar_activities = meta.loc[
|
102
|
-
(meta.name == activity
|
103
|
+
(meta.name == activity.name) & (meta.id != activity.id)
|
103
104
|
]
|
104
105
|
similar_activities = [row for _, row in similar_activities.iterrows()]
|
105
106
|
similar_activities.reverse()
|
@@ -107,7 +108,7 @@ def make_activity_blueprint(
|
|
107
108
|
new_tiles = {
|
108
109
|
zoom: sum(
|
109
110
|
tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
|
110
|
-
== activity
|
111
|
+
== activity.id
|
111
112
|
)
|
112
113
|
for zoom in sorted(config.explorer_zoom_levels)
|
113
114
|
}
|
@@ -117,7 +118,7 @@ def make_activity_blueprint(
|
|
117
118
|
for zoom in sorted(config.explorer_zoom_levels):
|
118
119
|
new_tiles = tile_visit_accessor.tile_state["tile_history"][zoom].loc[
|
119
120
|
tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
|
120
|
-
== activity
|
121
|
+
== activity.id
|
121
122
|
]
|
122
123
|
if len(new_tiles):
|
123
124
|
points = make_grid_points(
|
@@ -152,8 +153,8 @@ def make_activity_blueprint(
|
|
152
153
|
time_series[line_color_column],
|
153
154
|
line_color_columns_avail[line_color_column].format,
|
154
155
|
),
|
155
|
-
"date": activity
|
156
|
-
"time": activity
|
156
|
+
"date": activity.start.date(),
|
157
|
+
"time": activity.start.time(),
|
157
158
|
"new_tiles": new_tiles_per_zoom,
|
158
159
|
"new_tiles_geojson": new_tiles_geojson,
|
159
160
|
"line_color_column": line_color_column,
|
@@ -318,6 +319,7 @@ def make_activity_blueprint(
|
|
318
319
|
abort(404)
|
319
320
|
equipments = DB.session.scalars(sqlalchemy.select(Equipment)).all()
|
320
321
|
kinds = DB.session.scalars(sqlalchemy.select(Kind)).all()
|
322
|
+
tags = DB.session.scalars(sqlalchemy.select(Tag)).all()
|
321
323
|
|
322
324
|
if request.method == "POST":
|
323
325
|
activity.name = request.form.get("name")
|
@@ -326,13 +328,18 @@ def make_activity_blueprint(
|
|
326
328
|
if form_equipment == "null":
|
327
329
|
activity.equipment = None
|
328
330
|
else:
|
329
|
-
activity.equipment = DB.session.
|
331
|
+
activity.equipment = DB.session.get_one(Equipment, int(form_equipment))
|
330
332
|
|
331
333
|
form_kind = request.form.get("kind")
|
332
334
|
if form_kind == "null":
|
333
335
|
activity.kind = None
|
334
336
|
else:
|
335
|
-
activity.kind = DB.session.
|
337
|
+
activity.kind = DB.session.get_one(Kind, int(form_kind))
|
338
|
+
|
339
|
+
form_tags = request.form.getlist("tag")
|
340
|
+
activity.tags = [
|
341
|
+
DB.session.get_one(Tag, int(tag_id_str)) for tag_id_str in form_tags
|
342
|
+
]
|
336
343
|
|
337
344
|
DB.session.commit()
|
338
345
|
return redirect(url_for(".show", id=activity.id))
|
@@ -342,6 +349,7 @@ def make_activity_blueprint(
|
|
342
349
|
activity=activity,
|
343
350
|
kinds=kinds,
|
344
351
|
equipments=equipments,
|
352
|
+
tags=tags,
|
345
353
|
)
|
346
354
|
|
347
355
|
@blueprint.route("/trim/<id>", methods=["GET", "POST"])
|
@@ -0,0 +1,198 @@
|
|
1
|
+
import datetime
|
2
|
+
import pathlib
|
3
|
+
import uuid
|
4
|
+
|
5
|
+
import dateutil.parser
|
6
|
+
import exifread
|
7
|
+
import geojson
|
8
|
+
import sqlalchemy
|
9
|
+
from flask import Blueprint
|
10
|
+
from flask import redirect
|
11
|
+
from flask import render_template
|
12
|
+
from flask import request
|
13
|
+
from flask import Response
|
14
|
+
from flask import url_for
|
15
|
+
from PIL import Image
|
16
|
+
from PIL import ImageOps
|
17
|
+
|
18
|
+
from ...core.config import ConfigAccessor
|
19
|
+
from ...core.datamodel import Activity
|
20
|
+
from ...core.datamodel import DB
|
21
|
+
from ...core.datamodel import Photo
|
22
|
+
from ...core.paths import PHOTOS_DIR
|
23
|
+
from ..authenticator import Authenticator
|
24
|
+
from ..authenticator import needs_authentication
|
25
|
+
from ..flasher import Flasher
|
26
|
+
from ..flasher import FlashTypes
|
27
|
+
|
28
|
+
|
29
|
+
def ratio_to_decimal(numbers: list[exifread.utils.Ratio]) -> float:
|
30
|
+
deg, min, sec = numbers.values
|
31
|
+
return deg.decimal() + min.decimal() / 60 + sec.decimal() / 3600
|
32
|
+
|
33
|
+
|
34
|
+
def get_metadata_from_image(path: pathlib.Path) -> dict:
|
35
|
+
with open(path, "rb") as f:
|
36
|
+
tags = exifread.process_file(f)
|
37
|
+
metadata = {}
|
38
|
+
try:
|
39
|
+
metadata["latitude"] = ratio_to_decimal(tags["GPS GPSLatitude"])
|
40
|
+
metadata["longitude"] = ratio_to_decimal(tags["GPS GPSLongitude"])
|
41
|
+
except KeyError:
|
42
|
+
pass
|
43
|
+
try:
|
44
|
+
metadata["time"] = datetime.datetime.strptime(
|
45
|
+
str(tags["EXIF DateTimeOriginal"]), "%Y:%m:%d %H:%M:%S"
|
46
|
+
)
|
47
|
+
except KeyError:
|
48
|
+
pass
|
49
|
+
|
50
|
+
return metadata
|
51
|
+
|
52
|
+
|
53
|
+
def make_photo_blueprint(
|
54
|
+
config_accessor: ConfigAccessor, authenticator: Authenticator, flasher: Flasher
|
55
|
+
) -> Blueprint:
|
56
|
+
blueprint = Blueprint("photo", __name__, template_folder="templates")
|
57
|
+
|
58
|
+
@blueprint.route("/get/<int:id>/<int:size>.webp")
|
59
|
+
def get(id: int, size: int) -> Response:
|
60
|
+
assert size < 5000
|
61
|
+
photo = DB.session.get_one(Photo, id)
|
62
|
+
|
63
|
+
original_path = PHOTOS_DIR() / "original" / photo.path
|
64
|
+
small_path = PHOTOS_DIR() / f"size-{size}" / photo.path.with_suffix(".webp")
|
65
|
+
|
66
|
+
if not small_path.exists():
|
67
|
+
with Image.open(original_path) as im:
|
68
|
+
target_size = (size, size)
|
69
|
+
im = ImageOps.contain(im, target_size)
|
70
|
+
small_path.parent.mkdir(exist_ok=True)
|
71
|
+
im.save(small_path)
|
72
|
+
|
73
|
+
with open(small_path, "rb") as f:
|
74
|
+
return Response(f.read(), mimetype="image/webp")
|
75
|
+
|
76
|
+
@blueprint.route("/map")
|
77
|
+
def map() -> str:
|
78
|
+
return render_template("photo/map.html.j2")
|
79
|
+
|
80
|
+
@blueprint.route("/map-for-all/photos.geojson")
|
81
|
+
def map_for_all() -> Response:
|
82
|
+
photos = DB.session.scalars(sqlalchemy.select(Photo)).all()
|
83
|
+
fc = geojson.FeatureCollection(
|
84
|
+
features=[
|
85
|
+
geojson.Feature(
|
86
|
+
geometry=geojson.Point((photo.longitude, photo.latitude)),
|
87
|
+
properties={
|
88
|
+
"photo_id": photo.id,
|
89
|
+
"url_marker": url_for(".get", id=photo.id, size=128),
|
90
|
+
"url_popup": url_for(".get", id=photo.id, size=512),
|
91
|
+
"url_full": url_for(".get", id=photo.id, size=4096),
|
92
|
+
},
|
93
|
+
)
|
94
|
+
for photo in photos
|
95
|
+
]
|
96
|
+
)
|
97
|
+
return Response(
|
98
|
+
geojson.dumps(fc, sort_keys=True, indent=2, ensure_ascii=False),
|
99
|
+
mimetype="application/json",
|
100
|
+
)
|
101
|
+
|
102
|
+
@blueprint.route("/map-for-activity/<int:activity_id>/photos.geojson")
|
103
|
+
def map_for_activity(activity_id: int) -> Response:
|
104
|
+
activity = DB.session.get_one(Activity, activity_id)
|
105
|
+
fc = geojson.FeatureCollection(
|
106
|
+
features=[
|
107
|
+
geojson.Feature(
|
108
|
+
geometry=geojson.Point((photo.longitude, photo.latitude)),
|
109
|
+
properties={
|
110
|
+
"photo_id": photo.id,
|
111
|
+
"url_marker": url_for(".get", id=photo.id, size=128),
|
112
|
+
"url_popup": url_for(".get", id=photo.id, size=512),
|
113
|
+
"url_full": url_for(".get", id=photo.id, size=4096),
|
114
|
+
},
|
115
|
+
)
|
116
|
+
for photo in activity.photos
|
117
|
+
]
|
118
|
+
)
|
119
|
+
return Response(
|
120
|
+
geojson.dumps(fc, sort_keys=True, indent=2, ensure_ascii=False),
|
121
|
+
mimetype="application/json",
|
122
|
+
)
|
123
|
+
|
124
|
+
@blueprint.route("/new", methods=["GET", "POST"])
|
125
|
+
@needs_authentication(authenticator)
|
126
|
+
def new() -> Response:
|
127
|
+
if request.method == "POST":
|
128
|
+
# check if the post request has the file part
|
129
|
+
if "file" not in request.files:
|
130
|
+
flasher.flash_message(
|
131
|
+
"No file could be found. Did you select a file?", FlashTypes.WARNING
|
132
|
+
)
|
133
|
+
return redirect(url_for(".new"))
|
134
|
+
|
135
|
+
file = request.files["file"]
|
136
|
+
# If the user does not select a file, the browser submits an
|
137
|
+
# empty file without a filename.
|
138
|
+
if file.filename == "":
|
139
|
+
flasher.flash_message("No selected file.", FlashTypes.WARNING)
|
140
|
+
return redirect(url_for(".new"))
|
141
|
+
if not file:
|
142
|
+
flasher.flash_message("Empty file uploaded.", FlashTypes.WARNING)
|
143
|
+
return redirect(url_for(".new"))
|
144
|
+
|
145
|
+
filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix
|
146
|
+
path = PHOTOS_DIR() / "original" / filename
|
147
|
+
path.parent.mkdir(exist_ok=True)
|
148
|
+
file.save(path)
|
149
|
+
metadata = get_metadata_from_image(path)
|
150
|
+
|
151
|
+
if "time" not in metadata:
|
152
|
+
flasher.flash_message(
|
153
|
+
"Your image doesn't have the EXIF attribute 'EXIF DateTimeOriginal' and hence cannot be dated.",
|
154
|
+
FlashTypes.DANGER,
|
155
|
+
)
|
156
|
+
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
|
+
else:
|
196
|
+
return render_template("photo/new.html.j2")
|
197
|
+
|
198
|
+
return blueprint
|
@@ -18,6 +18,7 @@ from ...core.config import ConfigAccessor
|
|
18
18
|
from ...core.datamodel import DB
|
19
19
|
from ...core.datamodel import Equipment
|
20
20
|
from ...core.datamodel import Kind
|
21
|
+
from ...core.datamodel import Tag
|
21
22
|
from ...core.heart_rate import HeartRateZoneComputer
|
22
23
|
from ...core.paths import _activity_enriched_dir
|
23
24
|
from ..authenticator import Authenticator
|
@@ -395,6 +396,37 @@ def make_settings_blueprint(
|
|
395
396
|
strava_login_helper.save_strava_code(code)
|
396
397
|
return redirect(url_for(".strava"))
|
397
398
|
|
399
|
+
@blueprint.route("/tags")
|
400
|
+
@needs_authentication(authenticator)
|
401
|
+
def tags_list():
|
402
|
+
return render_template(
|
403
|
+
"settings/tags-list.html.j2",
|
404
|
+
tags=DB.session.scalars(sqlalchemy.select(Tag)).all(),
|
405
|
+
)
|
406
|
+
|
407
|
+
@blueprint.route("/tags/new", methods=["GET", "POST"])
|
408
|
+
@needs_authentication(authenticator)
|
409
|
+
def tags_new():
|
410
|
+
if request.method == "POST":
|
411
|
+
tag_str = request.form["tag"]
|
412
|
+
tag = Tag(tag=tag_str)
|
413
|
+
DB.session.add(tag)
|
414
|
+
DB.session.commit()
|
415
|
+
return redirect(url_for(".tags_list"))
|
416
|
+
else:
|
417
|
+
return render_template("settings/tags-new.html.j2")
|
418
|
+
|
419
|
+
@blueprint.route("/tags/edit/<int:id>", methods=["GET", "POST"])
|
420
|
+
@needs_authentication(authenticator)
|
421
|
+
def tags_edit(id: int):
|
422
|
+
tag = DB.session.get_one(Tag, id)
|
423
|
+
if request.method == "POST":
|
424
|
+
tag.tag = request.form["tag"]
|
425
|
+
DB.session.commit()
|
426
|
+
return redirect(url_for(".tags_list"))
|
427
|
+
else:
|
428
|
+
return render_template("settings/tags-edit.html.j2", tag=tag)
|
429
|
+
|
398
430
|
return blueprint
|
399
431
|
|
400
432
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
1
3
|
from werkzeug.datastructures import MultiDict
|
2
4
|
|
3
5
|
from ..core.config import ConfigAccessor
|
@@ -7,13 +9,20 @@ from .authenticator import Authenticator
|
|
7
9
|
|
8
10
|
|
9
11
|
def search_query_from_form(args: MultiDict) -> SearchQuery:
|
10
|
-
query = SearchQuery(
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
query = SearchQuery.from_primitives(
|
13
|
+
{
|
14
|
+
"equipment": map(int, args.getlist("equipment")),
|
15
|
+
"kind": map(int, args.getlist("kind")),
|
16
|
+
"tag": map(int, args.getlist("tag")),
|
17
|
+
"name": args.get("name", None),
|
18
|
+
"name_case_sensitive": _parse_bool(
|
19
|
+
args.get("name_case_sensitive", "false")
|
20
|
+
),
|
21
|
+
"start_begin": args.get("start_begin", None),
|
22
|
+
"start_end": args.get("start_end", None),
|
23
|
+
"distance_km_min": _optional_float(args.get("distance_km_min", None)),
|
24
|
+
"distance_km_max": _optional_float(args.get("distance_km_max", None)),
|
25
|
+
}
|
17
26
|
)
|
18
27
|
|
19
28
|
return query
|
@@ -23,6 +32,13 @@ def _parse_bool(s: str) -> bool:
|
|
23
32
|
return s == "true"
|
24
33
|
|
25
34
|
|
35
|
+
def _optional_float(s: str) -> Optional[float]:
|
36
|
+
if s:
|
37
|
+
return float(s)
|
38
|
+
else:
|
39
|
+
return None
|
40
|
+
|
41
|
+
|
26
42
|
class SearchQueryHistory:
|
27
43
|
def __init__(
|
28
44
|
self, config_accessor: ConfigAccessor, authenticator: Authenticator
|
@@ -32,6 +32,21 @@
|
|
32
32
|
</select>
|
33
33
|
</div>
|
34
34
|
|
35
|
+
<div class="mb-3">
|
36
|
+
<label for="" class="form-label">Tags</label>
|
37
|
+
<div class="form-control">
|
38
|
+
{% for tag in tags %}
|
39
|
+
<div class="form-check form-check-inline">
|
40
|
+
<input class="form-check-input" type="checkbox" name="tag" value="{{ tag.id }}" id="tag_{{ tag.id }}" {%
|
41
|
+
if tag in activity.tags %} checked {% endif %}>
|
42
|
+
<label class="form-check-label" for="tag_{{ tag.id }}">
|
43
|
+
{{ tag.tag }}
|
44
|
+
</label>
|
45
|
+
</div>
|
46
|
+
{% endfor %}
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
|
35
50
|
<button type="submit" class="btn btn-primary">Save</button>
|
36
51
|
</form>
|
37
52
|
|
@@ -10,14 +10,25 @@
|
|
10
10
|
<div class="row mb-3">
|
11
11
|
<div class="col-sm-12 col-md-4">
|
12
12
|
<dl>
|
13
|
-
<dt>Name</dt>
|
14
|
-
<dd>{{ activity["name"] }}</dd>
|
15
|
-
|
16
13
|
{% if activity.kind %}
|
17
14
|
<dt>Kind</dt>
|
18
15
|
<dd>{{ activity.kind.name }}</dd>
|
19
16
|
{% endif %}
|
20
17
|
|
18
|
+
{% if activity.equipment %}
|
19
|
+
<dt>Equipment</dt>
|
20
|
+
<dd>{{ activity.equipment.name }}</dd>
|
21
|
+
{% endif %}
|
22
|
+
|
23
|
+
{% if activity.tags %}
|
24
|
+
<dt>Tags</dt>
|
25
|
+
<dd>
|
26
|
+
{% for tag in activity.tags %}
|
27
|
+
<span class="badge text-bg-primary">{{ tag.tag }}</span>
|
28
|
+
{% endfor %}
|
29
|
+
</dd>
|
30
|
+
{% endif %}
|
31
|
+
|
21
32
|
<dt>Distance</dt>
|
22
33
|
<dd>{{ activity.distance_km|round(1) }} km</dd>
|
23
34
|
|
@@ -52,11 +63,6 @@
|
|
52
63
|
<dd>{{ activity.elevation_gain|round(0)|int }} m</dd>
|
53
64
|
{% endif %}
|
54
65
|
|
55
|
-
{% if activity.equipment %}
|
56
|
-
<dt>Equipment</dt>
|
57
|
-
<dd>{{ activity.equipment.name }}</dd>
|
58
|
-
{% endif %}
|
59
|
-
|
60
66
|
{% if new_tiles[14] %}
|
61
67
|
<dt>New Explorer Tiles</dt>
|
62
68
|
<dd>{{ new_tiles[14] }}</dd>
|
@@ -91,6 +97,30 @@
|
|
91
97
|
style: function (feature) { return { color: feature.properties.color } }
|
92
98
|
}).addTo(map)
|
93
99
|
map.fitBounds(geojson.getBounds());
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
fetch("{{ url_for('photo.map_for_activity', activity_id = activity.id) }}")
|
104
|
+
.then(function (response) {
|
105
|
+
return response.json();
|
106
|
+
})
|
107
|
+
.then(function (data) {
|
108
|
+
L.geoJSON(data, {
|
109
|
+
pointToLayer: function (feature, latlng) {
|
110
|
+
return L.marker(latlng, {
|
111
|
+
icon: new L.Icon({
|
112
|
+
iconSize: [32, 32],
|
113
|
+
iconAnchor: [16, 16],
|
114
|
+
popupAnchor: [16, 0],
|
115
|
+
iconUrl: feature.properties.url_marker,
|
116
|
+
})
|
117
|
+
});
|
118
|
+
},
|
119
|
+
onEachFeature: function (feature, layer) {
|
120
|
+
layer.bindPopup(`<a href="${feature.properties.url_full}" target="_blank"><img src="${feature.properties.url_popup}" /></a>`)
|
121
|
+
}
|
122
|
+
}).addTo(map);
|
123
|
+
});
|
94
124
|
</script>
|
95
125
|
|
96
126
|
|
@@ -114,9 +144,11 @@
|
|
114
144
|
<div class="mb-3" style="padding-top: 10px;">
|
115
145
|
<form method="GET">
|
116
146
|
<label class="form-label">Line Color by</label>
|
117
|
-
<select class="form-select" aria-label="Line Color by" name="line_color_column"
|
147
|
+
<select class="form-select" aria-label="Line Color by" name="line_color_column"
|
148
|
+
onchange="this.form.submit()">
|
118
149
|
{% for name, column in line_color_columns_avail.items() %}
|
119
|
-
<option {% if name
|
150
|
+
<option {% if name==line_color_column %} selected {% endif %} value="{{ name }}">{{
|
151
|
+
column.display_name }}</option>
|
120
152
|
{% endfor %}
|
121
153
|
</select>
|
122
154
|
</form>
|
@@ -124,8 +156,19 @@
|
|
124
156
|
</div>
|
125
157
|
</div>
|
126
158
|
|
127
|
-
|
159
|
+
{% if activity.photos %}
|
160
|
+
<h2 class="mb-3">Photos</h2>
|
128
161
|
<div class="row mb-3">
|
162
|
+
{% for photo in activity.photos %}
|
163
|
+
<div class="col-md-3">
|
164
|
+
<img src="{{ url_for('photo.get', id=photo.id, size=512) }}" width="100%" />
|
165
|
+
</div>
|
166
|
+
{% endfor %}
|
167
|
+
</div>
|
168
|
+
{% endif %}
|
169
|
+
|
170
|
+
|
171
|
+
<div class=" row mb-3">
|
129
172
|
<div class="col">
|
130
173
|
<h2>Distance & speed</h2>
|
131
174
|
</div>
|
@@ -201,7 +244,8 @@
|
|
201
244
|
|
202
245
|
{% if new_tiles_geojson %}
|
203
246
|
<h2>New explorer tiles</h2>
|
204
|
-
<p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective
|
247
|
+
<p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective
|
248
|
+
zoom
|
205
249
|
levels.</p>
|
206
250
|
<script>
|
207
251
|
function add_map(id, geojson) {
|
@@ -1,11 +1,10 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
-
{% from "search_form.html.j2" import search_form %}
|
3
2
|
|
4
3
|
{% block container %}
|
5
4
|
<h1 class="mb-3">Eddington Number</h1>
|
6
5
|
|
7
6
|
<div class="mb-3">
|
8
|
-
{
|
7
|
+
{% include "search_form.html.j2" %}
|
9
8
|
</div>
|
10
9
|
|
11
10
|
<div class="row mb-3">
|
@@ -1,11 +1,10 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
-
{% from "search_form.html.j2" import search_form %}
|
3
2
|
|
4
3
|
{% block container %}
|
5
4
|
<h1 class="mb-3">Eddington Number for Elevation Gain</h1>
|
6
5
|
|
7
6
|
<div class="mb-3">
|
8
|
-
{
|
7
|
+
{% include "search_form.html.j2" %}
|
9
8
|
</div>
|
10
9
|
|
11
10
|
<div class="row mb-3">
|