geo-activity-playground 1.0.0__py3-none-any.whl → 1.2.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/dc8073871da7_add_plotspec_group_by.py +28 -0
- geo_activity_playground/core/config.py +1 -0
- geo_activity_playground/core/datamodel.py +41 -3
- geo_activity_playground/core/parametric_plot.py +101 -47
- geo_activity_playground/webui/app.py +7 -0
- geo_activity_playground/webui/blueprints/activity_blueprint.py +11 -10
- geo_activity_playground/webui/blueprints/auth_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/calendar_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/eddington_blueprints.py +3 -2
- geo_activity_playground/webui/blueprints/entry_views.py +11 -11
- geo_activity_playground/webui/blueprints/equipment_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +47 -13
- geo_activity_playground/webui/blueprints/export_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +79 -0
- geo_activity_playground/webui/blueprints/photo_blueprint.py +65 -56
- geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +38 -19
- geo_activity_playground/webui/blueprints/settings_blueprint.py +17 -0
- geo_activity_playground/webui/blueprints/summary_blueprint.py +114 -240
- geo_activity_playground/webui/columns.py +40 -7
- geo_activity_playground/webui/static/{browserconfig.xml → favicons/browserconfig.xml} +1 -1
- geo_activity_playground/webui/static/{site.webmanifest → favicons/site.webmanifest} +2 -2
- geo_activity_playground/webui/static/server-side-explorer.js +7 -2
- geo_activity_playground/webui/templates/activity/name.html.j2 +4 -4
- geo_activity_playground/webui/templates/activity/show.html.j2 +8 -8
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +3 -3
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +3 -3
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +3 -3
- geo_activity_playground/webui/templates/equipment/index.html.j2 +4 -4
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +5 -4
- geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +57 -0
- geo_activity_playground/webui/templates/home.html.j2 +2 -12
- geo_activity_playground/webui/templates/page.html.j2 +23 -37
- geo_activity_playground/webui/templates/photo/new.html.j2 +1 -1
- geo_activity_playground/webui/templates/plot-macros.html.j2 +72 -0
- geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +12 -7
- geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +24 -0
- geo_activity_playground/webui/templates/plot_builder/index.html.j2 +5 -0
- 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/summary/index.html.j2 +23 -230
- geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +3 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.2.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.2.0.dist-info}/RECORD +73 -66
- /geo_activity_playground/webui/static/{bootstrap-dark-mode.js → bootstrap/bootstrap-dark-mode.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.bundle.min.js → bootstrap/bootstrap.bundle.min.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.min.css → bootstrap/bootstrap.min.css} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-192x192.png → favicons/android-chrome-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-512x512.png → favicons/android-chrome-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{apple-touch-icon.png → favicons/apple-touch-icon.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-16x16.png → favicons/favicon-16x16.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-32x32.png → favicons/favicon-32x32.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-48x48.png → favicons/favicon-48x48.png} +0 -0
- /geo_activity_playground/webui/static/{favicon.ico → favicons/favicon.ico} +0 -0
- /geo_activity_playground/webui/static/{favicon.svg → favicons/favicon.svg} +0 -0
- /geo_activity_playground/webui/static/{mstile-150x150.png → favicons/mstile-150x150.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-192x192.png → favicons/web-app-manifest-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-512x512.png → favicons/web-app-manifest-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{Leaflet.fullscreen.min.js → leaflet/Leaflet.fullscreen.min.js} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.Default.css → leaflet/MarkerCluster.Default.css} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.css → leaflet/MarkerCluster.css} +0 -0
- /geo_activity_playground/webui/static/{fullscreen.png → leaflet/fullscreen.png} +0 -0
- /geo_activity_playground/webui/static/{fullscreen@2x.png → leaflet/fullscreen@2x.png} +0 -0
- /geo_activity_playground/webui/static/{leaflet.css → leaflet/leaflet.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.fullscreen.css → leaflet/leaflet.fullscreen.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.js → leaflet/leaflet.js} +0 -0
- /geo_activity_playground/webui/static/{leaflet.markercluster.js → leaflet/leaflet.markercluster.js} +0 -0
- /geo_activity_playground/webui/static/{vega-embed@6 → vega/vega-embed@6.js} +0 -0
- /geo_activity_playground/webui/static/{vega-lite@4 → vega/vega-lite@4.js} +0 -0
- /geo_activity_playground/webui/static/{vega@5 → vega/vega@5.js} +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.2.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.2.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
|
|
1
1
|
import abc
|
2
2
|
import datetime
|
3
|
+
import hashlib
|
3
4
|
import io
|
4
5
|
import itertools
|
5
6
|
import logging
|
@@ -21,6 +22,7 @@ from flask import render_template
|
|
21
22
|
from flask import request
|
22
23
|
from flask import Response
|
23
24
|
from flask import url_for
|
25
|
+
from flask.typing import ResponseReturnValue
|
24
26
|
|
25
27
|
from ...core.activities import ActivityRepository
|
26
28
|
from ...core.config import ConfigAccessor
|
@@ -62,7 +64,7 @@ class ColorStrategy(abc.ABC):
|
|
62
64
|
pass
|
63
65
|
|
64
66
|
|
65
|
-
class
|
67
|
+
class MaxClusterColorStrategy(ColorStrategy):
|
66
68
|
def __init__(self, evolution_state, tile_visits):
|
67
69
|
self.evolution_state = evolution_state
|
68
70
|
self.tile_visits = tile_visits
|
@@ -84,6 +86,31 @@ class ClusterColorStrategy(ColorStrategy):
|
|
84
86
|
return grayscale
|
85
87
|
|
86
88
|
|
89
|
+
class ColorfulClusterColorStrategy(ColorStrategy):
|
90
|
+
def __init__(self, evolution_state: TileEvolutionState, tile_visits):
|
91
|
+
self.evolution_state = evolution_state
|
92
|
+
self.tile_visits = tile_visits
|
93
|
+
self.max_cluster_members = max(
|
94
|
+
evolution_state.clusters.values(),
|
95
|
+
key=len,
|
96
|
+
)
|
97
|
+
self._cmap = matplotlib.colormaps["hsv"]
|
98
|
+
|
99
|
+
def color_image(
|
100
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
101
|
+
) -> np.ndarray:
|
102
|
+
if tile_xy in self.evolution_state.memberships:
|
103
|
+
cluster_id = self.evolution_state.memberships[tile_xy]
|
104
|
+
m = hashlib.sha256()
|
105
|
+
m.update(str(cluster_id).encode())
|
106
|
+
d = int(m.hexdigest(), base=16) / (256.0**m.digest_size)
|
107
|
+
return blend_color(grayscale, np.array([[self._cmap(d)[:3]]]), 0.3)
|
108
|
+
elif tile_xy in self.tile_visits:
|
109
|
+
return blend_color(grayscale, 0.0, 0.3)
|
110
|
+
else:
|
111
|
+
return grayscale
|
112
|
+
|
113
|
+
|
87
114
|
class VisitTimeColorStrategy(ColorStrategy):
|
88
115
|
def __init__(self, tile_visits, use_first=True):
|
89
116
|
self.tile_visits = tile_visits
|
@@ -133,7 +160,7 @@ def make_explorer_blueprint(
|
|
133
160
|
|
134
161
|
@blueprint.route("/enable-zoom-level/<int:zoom>")
|
135
162
|
@needs_authentication(authenticator)
|
136
|
-
def enable_zoom_level(zoom: int):
|
163
|
+
def enable_zoom_level(zoom: int) -> ResponseReturnValue:
|
137
164
|
if 0 <= zoom <= 19:
|
138
165
|
config_accessor().explorer_zoom_levels.append(zoom)
|
139
166
|
config_accessor().explorer_zoom_levels.sort()
|
@@ -145,11 +172,11 @@ def make_explorer_blueprint(
|
|
145
172
|
return redirect(url_for(".map", zoom=zoom))
|
146
173
|
|
147
174
|
@blueprint.route(
|
148
|
-
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/
|
175
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/missing.<suffix>"
|
149
176
|
)
|
150
|
-
def
|
177
|
+
def download_missing(
|
151
178
|
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
152
|
-
):
|
179
|
+
) -> ResponseReturnValue:
|
153
180
|
x1, y1 = compute_tile(north, west, zoom)
|
154
181
|
x2, y2 = compute_tile(south, east, zoom)
|
155
182
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
@@ -170,11 +197,11 @@ def make_explorer_blueprint(
|
|
170
197
|
)
|
171
198
|
|
172
199
|
@blueprint.route(
|
173
|
-
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/
|
200
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/explored.<suffix>"
|
174
201
|
)
|
175
|
-
def
|
202
|
+
def download_explored(
|
176
203
|
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
177
|
-
):
|
204
|
+
) -> ResponseReturnValue:
|
178
205
|
x1, y1 = compute_tile(north, west, zoom)
|
179
206
|
x2, y2 = compute_tile(south, east, zoom)
|
180
207
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
@@ -197,7 +224,7 @@ def make_explorer_blueprint(
|
|
197
224
|
)
|
198
225
|
|
199
226
|
@blueprint.route("/<int:zoom>/server-side")
|
200
|
-
def server_side(zoom: int):
|
227
|
+
def server_side(zoom: int) -> ResponseReturnValue:
|
201
228
|
if zoom not in config_accessor().explorer_zoom_levels:
|
202
229
|
return {"zoom_level_not_generated": zoom}
|
203
230
|
|
@@ -239,7 +266,7 @@ def make_explorer_blueprint(
|
|
239
266
|
return render_template("explorer/server-side.html.j2", **context)
|
240
267
|
|
241
268
|
@blueprint.route("/<int:zoom>/tile/<int:z>/<int:x>/<int:y>.png")
|
242
|
-
def tile(zoom: int, z: int, x: int, y: int) ->
|
269
|
+
def tile(zoom: int, z: int, x: int, y: int) -> ResponseReturnValue:
|
243
270
|
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
244
271
|
evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
245
272
|
|
@@ -248,9 +275,16 @@ def make_explorer_blueprint(
|
|
248
275
|
square_line_width = 3
|
249
276
|
square_color = np.array([[[228, 26, 28]]]) / 256
|
250
277
|
|
251
|
-
|
252
|
-
|
253
|
-
|
278
|
+
color_strategy_name = request.args.get("color_strategy", "colorful_cluster")
|
279
|
+
if color_strategy_name == "default":
|
280
|
+
color_strategy_name = config_accessor().cluster_color_strategy
|
281
|
+
match color_strategy_name:
|
282
|
+
case "max_cluster":
|
283
|
+
color_strategy = MaxClusterColorStrategy(evolution_state, tile_visits)
|
284
|
+
case "colorful_cluster":
|
285
|
+
color_strategy = ColorfulClusterColorStrategy(
|
286
|
+
evolution_state, tile_visits
|
287
|
+
)
|
254
288
|
case "first":
|
255
289
|
color_strategy = VisitTimeColorStrategy(tile_visits, use_first=True)
|
256
290
|
case "last":
|
@@ -2,6 +2,7 @@ from flask import Blueprint
|
|
2
2
|
from flask import render_template
|
3
3
|
from flask import request
|
4
4
|
from flask import Response
|
5
|
+
from flask.typing import ResponseReturnValue
|
5
6
|
|
6
7
|
from ...core.export import export_all
|
7
8
|
from ..authenticator import Authenticator
|
@@ -13,12 +14,12 @@ def make_export_blueprint(authenticator: Authenticator) -> Blueprint:
|
|
13
14
|
|
14
15
|
@needs_authentication(authenticator)
|
15
16
|
@blueprint.route("/")
|
16
|
-
def index():
|
17
|
+
def index() -> str:
|
17
18
|
return render_template("export/index.html.j2")
|
18
19
|
|
19
20
|
@needs_authentication(authenticator)
|
20
21
|
@blueprint.route("/export")
|
21
|
-
def export():
|
22
|
+
def export() -> Response:
|
22
23
|
meta_format = request.args["meta_format"]
|
23
24
|
activity_format = request.args["activity_format"]
|
24
25
|
return Response(
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import collections
|
2
|
+
|
3
|
+
import pandas as pd
|
4
|
+
from flask import Blueprint
|
5
|
+
from flask import render_template
|
6
|
+
from flask import request
|
7
|
+
|
8
|
+
from ...core.activities import ActivityRepository
|
9
|
+
from ...core.activities import make_geojson_from_time_series
|
10
|
+
from ...core.meta_search import apply_search_query
|
11
|
+
from ..search_util import search_query_from_form
|
12
|
+
from ..search_util import SearchQueryHistory
|
13
|
+
|
14
|
+
|
15
|
+
def make_hall_of_fame_blueprint(
|
16
|
+
repository: ActivityRepository,
|
17
|
+
search_query_history: SearchQueryHistory,
|
18
|
+
) -> Blueprint:
|
19
|
+
blueprint = Blueprint("hall_of_fame", __name__, template_folder="templates")
|
20
|
+
|
21
|
+
@blueprint.route("/")
|
22
|
+
def index() -> str:
|
23
|
+
query = search_query_from_form(request.args)
|
24
|
+
search_query_history.register_query(query)
|
25
|
+
activities = apply_search_query(repository.meta, query)
|
26
|
+
df = activities
|
27
|
+
|
28
|
+
nominations = nominate_activities(df)
|
29
|
+
|
30
|
+
return render_template(
|
31
|
+
"hall_of_fame/index.html.j2",
|
32
|
+
nominations=[
|
33
|
+
(
|
34
|
+
repository.get_activity_by_id(activity_id),
|
35
|
+
reasons,
|
36
|
+
make_geojson_from_time_series(
|
37
|
+
repository.get_time_series(activity_id)
|
38
|
+
),
|
39
|
+
)
|
40
|
+
for activity_id, reasons in nominations.items()
|
41
|
+
],
|
42
|
+
query=query.to_jinja(),
|
43
|
+
)
|
44
|
+
|
45
|
+
return blueprint
|
46
|
+
|
47
|
+
|
48
|
+
def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
|
49
|
+
nominations: dict[int, list[str]] = collections.defaultdict(list)
|
50
|
+
|
51
|
+
_nominate_activities_inner(meta, "", nominations)
|
52
|
+
|
53
|
+
for kind, group in meta.groupby("kind"):
|
54
|
+
_nominate_activities_inner(group, f" for {kind}", nominations)
|
55
|
+
for equipment, group in meta.groupby("equipment"):
|
56
|
+
_nominate_activities_inner(group, f" with {equipment}", nominations)
|
57
|
+
|
58
|
+
return nominations
|
59
|
+
|
60
|
+
|
61
|
+
def _nominate_activities_inner(
|
62
|
+
meta: pd.DataFrame, title_suffix: str, nominations: dict[int, list[str]]
|
63
|
+
) -> None:
|
64
|
+
ratings = [
|
65
|
+
("distance_km", "Greatest distance", "{:.1f} km"),
|
66
|
+
("elapsed_time", "Longest elapsed time", "{}"),
|
67
|
+
("average_speed_moving_kmh", "Highest average moving speed", "{:.1f} km/h"),
|
68
|
+
("average_speed_elapsed_kmh", "Highest average elapsed speed", "{:.1f} km/h"),
|
69
|
+
("calories", "Most calories burnt", "{:.0f}"),
|
70
|
+
("steps", "Most steps", "{:.0f}"),
|
71
|
+
("elevation_gain", "Largest elevation gain", "{:.0f} m"),
|
72
|
+
]
|
73
|
+
|
74
|
+
for variable, title, format_str in ratings:
|
75
|
+
if variable in meta.columns and not pd.isna(meta[variable]).all():
|
76
|
+
i = meta[variable].idxmax()
|
77
|
+
value = meta.loc[i, variable]
|
78
|
+
format_applied = format_str.format(value)
|
79
|
+
nominations[i].append(f"{title}{title_suffix}: {format_applied}")
|
@@ -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,3 +1,5 @@
|
|
1
|
+
import json
|
2
|
+
|
1
3
|
import sqlalchemy
|
2
4
|
from flask import Blueprint
|
3
5
|
from flask import redirect
|
@@ -5,9 +7,12 @@ from flask import render_template
|
|
5
7
|
from flask import request
|
6
8
|
from flask import Response
|
7
9
|
from flask import url_for
|
10
|
+
from flask.typing import ResponseReturnValue
|
11
|
+
from flask.typing import RouteCallable
|
8
12
|
|
9
13
|
from ...core.activities import ActivityRepository
|
10
14
|
from ...core.datamodel import DB
|
15
|
+
from ...core.parametric_plot import GROUP_BY_VARIABLES
|
11
16
|
from ...core.parametric_plot import make_parametric_plot
|
12
17
|
from ...core.parametric_plot import MARKS
|
13
18
|
from ...core.parametric_plot import PlotSpec
|
@@ -25,7 +30,7 @@ def make_plot_builder_blueprint(
|
|
25
30
|
blueprint = Blueprint("plot_builder", __name__, template_folder="templates")
|
26
31
|
|
27
32
|
@blueprint.route("/")
|
28
|
-
def index() ->
|
33
|
+
def index() -> ResponseReturnValue:
|
29
34
|
return render_template(
|
30
35
|
"plot_builder/index.html.j2",
|
31
36
|
specs=DB.session.scalars(sqlalchemy.select(PlotSpec)).all(),
|
@@ -33,7 +38,7 @@ def make_plot_builder_blueprint(
|
|
33
38
|
|
34
39
|
@blueprint.route("/new")
|
35
40
|
@needs_authentication(authenticator)
|
36
|
-
def new() ->
|
41
|
+
def new() -> ResponseReturnValue:
|
37
42
|
spec = PlotSpec(
|
38
43
|
name="My New Plot",
|
39
44
|
mark="bar",
|
@@ -45,23 +50,36 @@ def make_plot_builder_blueprint(
|
|
45
50
|
DB.session.commit()
|
46
51
|
return redirect(url_for(".edit", id=spec.id))
|
47
52
|
|
48
|
-
@blueprint.route("/
|
53
|
+
@blueprint.route("/import-spec", methods=["GET", "POST"])
|
49
54
|
@needs_authentication(authenticator)
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
spec
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
spec.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
55
|
+
def import_spec() -> ResponseReturnValue:
|
56
|
+
if request.form:
|
57
|
+
parameters = json.loads(request.form["spec_json"])
|
58
|
+
spec = PlotSpec(**parameters)
|
59
|
+
DB.session.add(spec)
|
60
|
+
DB.session.commit()
|
61
|
+
return redirect(url_for(".edit", id=spec.id))
|
62
|
+
else:
|
63
|
+
return render_template("plot_builder/import-spec.html.j2")
|
64
|
+
|
65
|
+
@blueprint.route("/edit/<int:id>", methods=["GET", "POST"])
|
66
|
+
@needs_authentication(authenticator)
|
67
|
+
def edit(id: int) -> ResponseReturnValue:
|
68
|
+
spec = DB.session.get_one(PlotSpec, id)
|
69
|
+
if request.form:
|
70
|
+
spec.name = request.form["name"]
|
71
|
+
spec.mark = request.form["mark"]
|
72
|
+
spec.x = request.form["x"]
|
73
|
+
spec.y = request.form["y"]
|
74
|
+
spec.color = request.form["color"]
|
75
|
+
spec.shape = request.form["shape"]
|
76
|
+
spec.size = request.form["size"]
|
77
|
+
spec.size = request.form["size"]
|
78
|
+
spec.row = request.form["row"]
|
79
|
+
spec.column = request.form["column"]
|
80
|
+
spec.facet = request.form["facet"]
|
81
|
+
spec.opacity = request.form["opacity"]
|
82
|
+
spec.group_by = request.form["group_by"]
|
65
83
|
try:
|
66
84
|
plot = make_parametric_plot(repository.meta, spec)
|
67
85
|
DB.session.commit()
|
@@ -73,13 +91,14 @@ def make_plot_builder_blueprint(
|
|
73
91
|
marks=MARKS,
|
74
92
|
discrete=VARIABLES_1,
|
75
93
|
continuous=VARIABLES_2,
|
94
|
+
group_by=GROUP_BY_VARIABLES,
|
76
95
|
plot=plot,
|
77
96
|
spec=spec,
|
78
97
|
)
|
79
98
|
|
80
99
|
@blueprint.route("/delete/<int:id>")
|
81
100
|
@needs_authentication(authenticator)
|
82
|
-
def delete(id: int) ->
|
101
|
+
def delete(id: int) -> ResponseReturnValue:
|
83
102
|
spec = DB.session.get(PlotSpec, id)
|
84
103
|
DB.session.delete(spec)
|
85
104
|
flasher.flash_message(f"Deleted plot '{spec.name}'.", FlashTypes.SUCCESS)
|
@@ -447,6 +447,23 @@ def make_settings_blueprint(
|
|
447
447
|
else:
|
448
448
|
return render_template("settings/tags-edit.html.j2", tag=tag)
|
449
449
|
|
450
|
+
@blueprint.route("/tile-source", methods=["GET", "POST"])
|
451
|
+
@needs_authentication(authenticator)
|
452
|
+
def tile_source() -> str:
|
453
|
+
if request.method == "POST":
|
454
|
+
config_accessor().map_tile_url = request.form["map_tile_url"]
|
455
|
+
config_accessor().map_tile_attribution = request.form[
|
456
|
+
"map_tile_attribution"
|
457
|
+
]
|
458
|
+
config_accessor.save()
|
459
|
+
flasher.flash_message("Tile source updated.", FlashTypes.SUCCESS)
|
460
|
+
return render_template(
|
461
|
+
"settings/tile-source.html.j2",
|
462
|
+
map_tile_url=config_accessor().map_tile_url,
|
463
|
+
map_tile_attribution=config_accessor().map_tile_attribution,
|
464
|
+
test_url=config_accessor().map_tile_url.format(zoom=14, x=8514, y=5504),
|
465
|
+
)
|
466
|
+
|
450
467
|
return blueprint
|
451
468
|
|
452
469
|
|