geo-activity-playground 0.42.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 +3 -1
- geo_activity_playground/core/datamodel.py +48 -22
- geo_activity_playground/core/enrichment.py +4 -2
- geo_activity_playground/core/meta_search.py +78 -34
- geo_activity_playground/core/missing_values.py +5 -2
- geo_activity_playground/core/paths.py +2 -0
- geo_activity_playground/webui/app.py +21 -7
- geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
- geo_activity_playground/webui/search_util.py +23 -7
- geo_activity_playground/webui/templates/activity/show.html.j2 +43 -10
- 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/summary/index.html.j2 +12 -10
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +28 -24
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -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
|
@@ -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
|
@@ -10,14 +10,16 @@
|
|
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
|
+
|
21
23
|
{% if activity.tags %}
|
22
24
|
<dt>Tags</dt>
|
23
25
|
<dd>
|
@@ -61,11 +63,6 @@
|
|
61
63
|
<dd>{{ activity.elevation_gain|round(0)|int }} m</dd>
|
62
64
|
{% endif %}
|
63
65
|
|
64
|
-
{% if activity.equipment %}
|
65
|
-
<dt>Equipment</dt>
|
66
|
-
<dd>{{ activity.equipment.name }}</dd>
|
67
|
-
{% endif %}
|
68
|
-
|
69
66
|
{% if new_tiles[14] %}
|
70
67
|
<dt>New Explorer Tiles</dt>
|
71
68
|
<dd>{{ new_tiles[14] }}</dd>
|
@@ -100,6 +97,30 @@
|
|
100
97
|
style: function (feature) { return { color: feature.properties.color } }
|
101
98
|
}).addTo(map)
|
102
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
|
+
});
|
103
124
|
</script>
|
104
125
|
|
105
126
|
|
@@ -135,8 +156,19 @@
|
|
135
156
|
</div>
|
136
157
|
</div>
|
137
158
|
|
138
|
-
|
159
|
+
{% if activity.photos %}
|
160
|
+
<h2 class="mb-3">Photos</h2>
|
139
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">
|
140
172
|
<div class="col">
|
141
173
|
<h2>Distance & speed</h2>
|
142
174
|
</div>
|
@@ -212,7 +244,8 @@
|
|
212
244
|
|
213
245
|
{% if new_tiles_geojson %}
|
214
246
|
<h2>New explorer tiles</h2>
|
215
|
-
<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
|
216
249
|
levels.</p>
|
217
250
|
<script>
|
218
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">
|
@@ -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">
|
@@ -13,20 +12,21 @@
|
|
13
12
|
<form method="GET">
|
14
13
|
<div class="mb-3">
|
15
14
|
<label class="form-label">Divisor</label>
|
16
|
-
<select class="form-select" aria-label="Divisor" name="elevation_eddington_divisor"
|
15
|
+
<select class="form-select" aria-label="Divisor" name="elevation_eddington_divisor"
|
16
|
+
onchange="this.form.submit()">
|
17
17
|
{% for dv in divisor_values_avail %}
|
18
18
|
<option {% if dv==elevation_eddington_divisor %} selected {% endif %}>{{ dv }}</option>
|
19
19
|
{% endfor %}
|
20
20
|
{% for key, value in query.items() %}
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
{% if value and key != "active" %}
|
22
|
+
{% if value is sequence %}
|
23
|
+
{% for v in value %}
|
24
|
+
<input type="hidden" name="{{ key }}" value="{{ v }}" />
|
25
|
+
{% endfor %}
|
26
|
+
{% else %}
|
27
|
+
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
28
|
+
{% endif %}
|
29
|
+
{% endif %}
|
30
30
|
{% endfor %}
|
31
31
|
</select>
|
32
32
|
</div>
|
@@ -37,9 +37,11 @@
|
|
37
37
|
<p>Your Eddington number with divisor {{ elevation_eddington_divisor }} is <b>{{ eddington_number }}</b>.</p>
|
38
38
|
|
39
39
|
<p>That means that on {{ (eddington_number / elevation_eddington_divisor) | int }} separate days you
|
40
|
-
have recorded activities with an elevation gain of more than {{ eddington_number }} m. Going high is one
|
40
|
+
have recorded activities with an elevation gain of more than {{ eddington_number }} m. Going high is one
|
41
|
+
thing, going often is
|
41
42
|
another. But going high often is hard. Also if you increment the Eddington number, all days with less
|
42
|
-
elevation gain will not count any more. It becomes increasingly hard to increment the Eddington number
|
43
|
+
elevation gain will not count any more. It becomes increasingly hard to increment the Eddington number
|
44
|
+
because you
|
43
45
|
don't only need to achieve a higher count, but all flatter activities don't count towards the bigger number.
|
44
46
|
</p>
|
45
47
|
</div>
|
@@ -74,7 +76,8 @@
|
|
74
76
|
<div class="col">
|
75
77
|
<h2>Plot</h2>
|
76
78
|
|
77
|
-
<p>In a graphical representation, the Eddington number is the elevation gain where the red line intersects with
|
79
|
+
<p>In a graphical representation, the Eddington number is the elevation gain where the red line intersects with
|
80
|
+
the
|
78
81
|
blue area.</p>
|
79
82
|
|
80
83
|
{{ vega_direct("logarithmic_plot", logarithmic_plot) }}
|
@@ -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">Heatmap</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">
|
@@ -123,6 +123,12 @@
|
|
123
123
|
</li>
|
124
124
|
{% endif %}
|
125
125
|
|
126
|
+
{% if photo_count > 0 %}
|
127
|
+
<li class="nav-item">
|
128
|
+
<a class="nav-link" aria-current="page" href="{{ url_for('photo.map') }}">Photo Map</a>
|
129
|
+
</li>
|
130
|
+
{% endif %}
|
131
|
+
|
126
132
|
<li class="nav-item dropdown">
|
127
133
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
128
134
|
aria-expanded="false">
|
@@ -131,6 +137,8 @@
|
|
131
137
|
<ul class="dropdown-menu">
|
132
138
|
<li><a class="dropdown-item" href="{{ url_for('upload.index') }}">Upload Activities</a>
|
133
139
|
</li>
|
140
|
+
<li><a class="dropdown-item" href="{{ url_for('photo.new') }}">Upload Photos</a>
|
141
|
+
</li>
|
134
142
|
<li><a class="dropdown-item" href="{{ url_for('upload.reload') }}">Scan New
|
135
143
|
Activities</a>
|
136
144
|
</li>
|
@@ -0,0 +1,45 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<h1>Photo Map</h1>
|
5
|
+
|
6
|
+
<div id="photo-map" style="height: 800px;" class="mb-3"></div>
|
7
|
+
<script>
|
8
|
+
var map = L.map('photo-map', {
|
9
|
+
fullscreenControl: true
|
10
|
+
});
|
11
|
+
L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
|
12
|
+
maxZoom: 19,
|
13
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
14
|
+
}).addTo(map);
|
15
|
+
|
16
|
+
fetch("{{ url_for('photo.map_for_all') }}")
|
17
|
+
.then(function (response) {
|
18
|
+
return response.json();
|
19
|
+
})
|
20
|
+
.then(function (data) {
|
21
|
+
let layer = L.geoJSON(data, {
|
22
|
+
pointToLayer: function (feature, latlng) {
|
23
|
+
return L.marker(latlng, {
|
24
|
+
icon: new L.Icon({
|
25
|
+
iconSize: [32, 32],
|
26
|
+
iconAnchor: [16, 16],
|
27
|
+
popupAnchor: [16, 0],
|
28
|
+
iconUrl: feature.properties.url_marker,
|
29
|
+
})
|
30
|
+
});
|
31
|
+
},
|
32
|
+
onEachFeature: function (feature, layer) {
|
33
|
+
layer.bindPopup(`<a href="${feature.properties.url_full}" target="_blank"><img src="${feature.properties.url_popup}" /></a>`)
|
34
|
+
}
|
35
|
+
})
|
36
|
+
|
37
|
+
let group = L.markerClusterGroup()
|
38
|
+
group.addLayer(layer)
|
39
|
+
|
40
|
+
group.addTo(map);
|
41
|
+
map.fitBounds(group.getBounds());
|
42
|
+
});
|
43
|
+
</script>
|
44
|
+
|
45
|
+
{% endblock %}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
<h1>Upload Photo</h1>
|
5
|
+
|
6
|
+
<form method="POST" enctype="multipart/form-data">
|
7
|
+
<div class="mb-3">
|
8
|
+
<label for="file" class="form-label">Photo file</label>
|
9
|
+
<input type="file" name="file" id="file" class="form-control">
|
10
|
+
</div>
|
11
|
+
<button type="submit" class="btn btn-primary">Upload</button>
|
12
|
+
</form>
|
13
|
+
{% endblock %}
|
@@ -1,12 +1,11 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
|
-
{% from "search_form.html.j2" import search_form %}
|
3
2
|
|
4
3
|
{% block container %}
|
5
4
|
|
6
5
|
<h1 class="row mb-3">Activities Overview & Search</h1>
|
7
6
|
|
8
7
|
<div class="mb-3">
|
9
|
-
{
|
8
|
+
{% include "search_form.html.j2" %}
|
10
9
|
</div>
|
11
10
|
|
12
11
|
<table class="table table-sort table-arrows">
|
@@ -1,4 +1,14 @@
|
|
1
|
-
{% macro
|
1
|
+
{% macro _show_search_query(description, url_parameters, is_favorite, request_url) %}
|
2
|
+
<li>
|
3
|
+
<a href="?{{ url_parameters }}">{{ description }}</a>
|
4
|
+
{% if is_favorite %}
|
5
|
+
<a href="{{ url_for('search.delete_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">🗑️</a>
|
6
|
+
{% else %}
|
7
|
+
<a href="{{ url_for('search.save_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">💾</a>
|
8
|
+
{% endif %}
|
9
|
+
</li>
|
10
|
+
{% endmacro %}
|
11
|
+
|
2
12
|
<div class="accordion" id="search_form_accordion">
|
3
13
|
<div class="accordion-item">
|
4
14
|
<h2 class="accordion-header">
|
@@ -41,16 +51,27 @@
|
|
41
51
|
value="{{ query.start_end }}">
|
42
52
|
</div>
|
43
53
|
|
54
|
+
<div class="col-6">
|
55
|
+
<label for="distance_km_min" class="form-label">Distance minimum</label>
|
56
|
+
<input type="number" class="form-control" id="distance_km_min"
|
57
|
+
name="distance_km_min" value="{{ query.distance_km_min }}">
|
58
|
+
</div>
|
59
|
+
<div class="col-6">
|
60
|
+
<label for="distance_km_max" class="form-label">Distance maximum</label>
|
61
|
+
<input type="number" class="form-control" id="distance_km_max"
|
62
|
+
name="distance_km_max" value="{{ query.distance_km_max }}">
|
63
|
+
</div>
|
64
|
+
|
44
65
|
<div class="col-12">
|
45
66
|
<label for="" class="form-label">Kind</label>
|
46
67
|
<div class="form-control">
|
47
68
|
{% for kind in kinds_avail %}
|
48
69
|
<div class="form-check form-check-inline">
|
49
70
|
<input class="form-check-input" type="checkbox" name="kind"
|
50
|
-
value="{{ kind }}" id="kind_{{ kind }}" {% if kind in
|
51
|
-
checked {% endif %}>
|
52
|
-
<label class="form-check-label" for="kind_{{ kind }}">
|
53
|
-
{{ kind }}
|
71
|
+
value="{{ kind.id }}" id="kind_{{ kind.id }}" {% if kind.id in
|
72
|
+
query.kind %} checked {% endif %}>
|
73
|
+
<label class="form-check-label" for="kind_{{ kind.id }}">
|
74
|
+
{{ kind.name }}
|
54
75
|
</label>
|
55
76
|
</div>
|
56
77
|
{% endfor %}
|
@@ -63,10 +84,26 @@
|
|
63
84
|
{% for equipment in equipments_avail %}
|
64
85
|
<div class="form-check form-check-inline">
|
65
86
|
<input class="form-check-input" type="checkbox" name="equipment"
|
66
|
-
value="{{ equipment }}" id="equipment_{{ equipment }}" {% if
|
67
|
-
in query.equipment %} checked {% endif %}>
|
68
|
-
<label class="form-check-label" for="equipment_{{ equipment }}">
|
69
|
-
{{ equipment }}
|
87
|
+
value="{{ equipment.id }}" id="equipment_{{ equipment.id }}" {% if
|
88
|
+
equipment.id in query.equipment %} checked {% endif %}>
|
89
|
+
<label class="form-check-label" for="equipment_{{ equipment.id }}">
|
90
|
+
{{ equipment.name }}
|
91
|
+
</label>
|
92
|
+
</div>
|
93
|
+
{% endfor %}
|
94
|
+
</div>
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<div class="col-12">
|
98
|
+
<label for="" class="form-label">Tags</label>
|
99
|
+
<div class="form-control">
|
100
|
+
{% for tag in tags_avail %}
|
101
|
+
<div class="form-check form-check-inline">
|
102
|
+
<input class="form-check-input" type="checkbox" name="tag"
|
103
|
+
value="{{ tag.id }}" id="tag_{{ tag.id }}" {% if tag.id in query.tag %}
|
104
|
+
checked {% endif %}>
|
105
|
+
<label class="form-check-label" for="tag_{{ tag.id }}">
|
106
|
+
{{ tag.tag }}
|
70
107
|
</label>
|
71
108
|
</div>
|
72
109
|
{% endfor %}
|
@@ -101,16 +138,4 @@
|
|
101
138
|
</div>
|
102
139
|
</div>
|
103
140
|
</div>
|
104
|
-
</div>
|
105
|
-
{% endmacro %}
|
106
|
-
|
107
|
-
{% macro _show_search_query(description, url_parameters, is_favorite, request_url) %}
|
108
|
-
<li>
|
109
|
-
<a href="?{{ url_parameters }}">{{ description }}</a>
|
110
|
-
{% if is_favorite %}
|
111
|
-
<a href="{{ url_for('search.delete_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">🗑️</a>
|
112
|
-
{% else %}
|
113
|
-
<a href="{{ url_for('search.save_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">💾</a>
|
114
|
-
{% endif %}
|
115
|
-
</li>
|
116
|
-
{% endmacro %}
|
141
|
+
</div>
|