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.
Files changed (37) hide show
  1. geo_activity_playground/alembic/script.py.mako +0 -6
  2. geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +40 -0
  3. geo_activity_playground/alembic/versions/script.py.mako +6 -0
  4. geo_activity_playground/core/activities.py +7 -15
  5. geo_activity_playground/core/datamodel.py +91 -85
  6. geo_activity_playground/core/enrichment.py +15 -6
  7. geo_activity_playground/core/meta_search.py +78 -34
  8. geo_activity_playground/core/missing_values.py +16 -0
  9. geo_activity_playground/core/paths.py +2 -0
  10. geo_activity_playground/core/test_missing_values.py +19 -0
  11. geo_activity_playground/explorer/tile_visits.py +1 -1
  12. geo_activity_playground/webui/app.py +22 -8
  13. geo_activity_playground/webui/blueprints/activity_blueprint.py +18 -10
  14. geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
  15. geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
  16. geo_activity_playground/webui/search_util.py +23 -7
  17. geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
  18. geo_activity_playground/webui/templates/activity/show.html.j2 +56 -12
  19. geo_activity_playground/webui/templates/eddington/distance.html.j2 +1 -2
  20. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +1 -2
  21. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +18 -15
  22. geo_activity_playground/webui/templates/heatmap/index.html.j2 +1 -2
  23. geo_activity_playground/webui/templates/page.html.j2 +8 -0
  24. geo_activity_playground/webui/templates/photo/map.html.j2 +45 -0
  25. geo_activity_playground/webui/templates/photo/new.html.j2 +13 -0
  26. geo_activity_playground/webui/templates/search/index.html.j2 +1 -2
  27. geo_activity_playground/webui/templates/search_form.html.j2 +47 -22
  28. geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
  29. geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
  30. geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
  31. geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
  32. geo_activity_playground/webui/templates/summary/index.html.j2 +12 -10
  33. {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +3 -1
  34. {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +37 -28
  35. {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +1 -1
  36. {geo_activity_playground-0.41.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
  37. {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 = basedir / "database.sqlite"
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
- if len(repository):
186
- variables["equipments_avail"] = sorted(
187
- repository.meta["equipment"].unique()
188
- )
189
- variables["kinds_avail"] = sorted(repository.meta["kind"].unique())
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["id"]
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["name"],
81
- "activity_id": str(activity["id"]),
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["name"]) & (meta.id != activity["id"])
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["id"]
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["id"]
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["start"].date(),
156
- "time": activity["start"].time(),
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.get(Equipment, int(form_equipment))
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.get(Kind, int(form_kind))
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
- equipment=args.getlist("equipment"),
12
- kind=args.getlist("kind"),
13
- name=args.get("name", None),
14
- name_case_sensitive=_parse_bool(args.get("name_case_sensitive", "false")),
15
- start_begin=_parse_date_or_none(args.get("start_begin", None)),
16
- start_end=_parse_date_or_none(args.get("start_end", None)),
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" onchange="this.form.submit()">
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 == line_color_column %} selected {% endif %} value="{{ name }}">{{ column.display_name }}</option>
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 zoom
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
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
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
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
7
+ {% include "search_form.html.j2" %}
9
8
  </div>
10
9
 
11
10
  <div class="row mb-3">