geo-activity-playground 0.45.0__py3-none-any.whl → 1.0.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/core/datamodel.py +3 -0
- geo_activity_playground/core/export.py +129 -0
- geo_activity_playground/core/meta_search.py +1 -1
- geo_activity_playground/webui/app.py +3 -1
- geo_activity_playground/webui/authenticator.py +4 -2
- geo_activity_playground/webui/blueprints/auth_blueprint.py +3 -0
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +300 -188
- geo_activity_playground/webui/blueprints/export_blueprint.py +30 -0
- geo_activity_playground/webui/blueprints/upload_blueprint.py +9 -0
- geo_activity_playground/webui/static/server-side-explorer.js +55 -0
- geo_activity_playground/webui/templates/auth/index.html.j2 +1 -0
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +41 -36
- geo_activity_playground/webui/templates/export/index.html.j2 +39 -0
- geo_activity_playground/webui/templates/page.html.j2 +3 -6
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/RECORD +19 -16
- geo_activity_playground/webui/templates/explorer/index.html.j2 +0 -148
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.0.0.dist-info}/entry_points.txt +0 -0
@@ -229,6 +229,9 @@ def query_activity_meta(clauses: list = []) -> pd.DataFrame:
|
|
229
229
|
.order_by(Activity.start)
|
230
230
|
).all()
|
231
231
|
df = pd.DataFrame(rows)
|
232
|
+
# If the search yields only activities without time information, the dtype isn't derived correctly.
|
233
|
+
df["start"] = pd.to_datetime(df["start"])
|
234
|
+
df["elapsed_time"] = pd.to_timedelta(df["elapsed_time"])
|
232
235
|
|
233
236
|
if len(df):
|
234
237
|
for old, new in [
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import io
|
2
|
+
import zipfile
|
3
|
+
from typing import IO
|
4
|
+
|
5
|
+
import geojson
|
6
|
+
import gpxpy.gpx
|
7
|
+
import pandas as pd
|
8
|
+
import sqlalchemy
|
9
|
+
from tqdm import tqdm
|
10
|
+
|
11
|
+
from .datamodel import Activity
|
12
|
+
from .datamodel import DB
|
13
|
+
from .datamodel import query_activity_meta
|
14
|
+
|
15
|
+
|
16
|
+
def export_all(meta_format: str, activity_format: str) -> bytes:
|
17
|
+
meta = query_activity_meta()
|
18
|
+
buffer = io.BytesIO()
|
19
|
+
with zipfile.ZipFile(buffer, "x") as zf:
|
20
|
+
with zf.open(f"activities.{meta_format}", mode="w") as f:
|
21
|
+
match meta_format:
|
22
|
+
case "csv":
|
23
|
+
export_meta_as_csv(meta, f)
|
24
|
+
case "json":
|
25
|
+
export_meta_as_json(meta, f)
|
26
|
+
case "ods":
|
27
|
+
export_meta_as_xlsx(meta, f)
|
28
|
+
case "parquet":
|
29
|
+
export_meta_as_parquet(meta, f)
|
30
|
+
case "xlsx":
|
31
|
+
export_meta_as_xlsx(meta, f)
|
32
|
+
case _:
|
33
|
+
raise ValueError(
|
34
|
+
f"Format {meta_format} is not supported for metadata."
|
35
|
+
)
|
36
|
+
if activity_format:
|
37
|
+
zf.mkdir("activities")
|
38
|
+
for activity in tqdm(
|
39
|
+
DB.session.scalars(sqlalchemy.select(Activity)).all(),
|
40
|
+
desc="Export activity time series",
|
41
|
+
):
|
42
|
+
with zf.open(
|
43
|
+
f"activities/{activity.id}.{activity_format}", mode="w"
|
44
|
+
) as f:
|
45
|
+
match activity_format:
|
46
|
+
case "csv":
|
47
|
+
export_activity_as_csv(activity, f)
|
48
|
+
case "geojson":
|
49
|
+
export_activity_as_geojson(activity, f)
|
50
|
+
case "gpx":
|
51
|
+
export_activity_as_gpx(activity, f)
|
52
|
+
case "ods":
|
53
|
+
export_activity_as_xlsx(activity, f)
|
54
|
+
case "parquet":
|
55
|
+
export_activity_as_parquet(activity, f)
|
56
|
+
case "xlsx":
|
57
|
+
export_activity_as_xlsx(activity, f)
|
58
|
+
case _:
|
59
|
+
raise ValueError(
|
60
|
+
f"Format {activity_format} is not supported for activity time series."
|
61
|
+
)
|
62
|
+
return bytes(buffer.getbuffer())
|
63
|
+
|
64
|
+
|
65
|
+
def export_meta_as_csv(meta: pd.DataFrame, target: IO[bytes]) -> None:
|
66
|
+
meta.to_csv(target, index=False)
|
67
|
+
|
68
|
+
|
69
|
+
def export_meta_as_json(meta: pd.DataFrame, target: IO[bytes]) -> None:
|
70
|
+
buffer = io.StringIO()
|
71
|
+
meta.to_json(buffer, index=False)
|
72
|
+
target.write(buffer.getvalue().encode())
|
73
|
+
|
74
|
+
|
75
|
+
def export_meta_as_parquet(meta: pd.DataFrame, target: IO[bytes]) -> None:
|
76
|
+
meta.to_parquet(target, index=False)
|
77
|
+
|
78
|
+
|
79
|
+
def export_meta_as_xlsx(meta: pd.DataFrame, target: IO[bytes]) -> None:
|
80
|
+
meta.to_excel(target, index=False)
|
81
|
+
|
82
|
+
|
83
|
+
def export_activity_as_csv(activity: Activity, target: IO[bytes]) -> None:
|
84
|
+
activity.time_series.to_csv(target, index=False)
|
85
|
+
|
86
|
+
|
87
|
+
def export_activity_as_geojson(activity: Activity, target: IO[bytes]) -> None:
|
88
|
+
ts = activity.time_series
|
89
|
+
result = geojson.MultiLineString(
|
90
|
+
coordinates=[
|
91
|
+
[(lon, lat) for lat, lon in zip(group["latitude"], group["longitude"])]
|
92
|
+
for segment_id, group in ts.groupby("segment_id")
|
93
|
+
]
|
94
|
+
)
|
95
|
+
buffer = io.StringIO()
|
96
|
+
geojson.dump(result, buffer)
|
97
|
+
target.write(buffer.getvalue().encode())
|
98
|
+
|
99
|
+
|
100
|
+
def export_activity_as_gpx(activity: Activity, target: IO[bytes]) -> None:
|
101
|
+
g = gpxpy.gpx.GPX()
|
102
|
+
|
103
|
+
gpx_track = gpxpy.gpx.GPXTrack()
|
104
|
+
g.tracks.append(gpx_track)
|
105
|
+
|
106
|
+
ts = activity.time_series
|
107
|
+
for segment_id, group in ts.groupby("segment_id"):
|
108
|
+
gpx_segment = gpxpy.gpx.GPXTrackSegment()
|
109
|
+
gpx_track.segments.append(gpx_segment)
|
110
|
+
|
111
|
+
for index, row in group.iterrows():
|
112
|
+
gpx_segment.points.append(
|
113
|
+
gpxpy.gpx.GPXTrackPoint(
|
114
|
+
row["latitude"],
|
115
|
+
row["longitude"],
|
116
|
+
elevation=row.get("elevation", None),
|
117
|
+
time=row.get("time", None),
|
118
|
+
)
|
119
|
+
)
|
120
|
+
|
121
|
+
target.write(g.to_xml().encode())
|
122
|
+
|
123
|
+
|
124
|
+
def export_activity_as_parquet(activity: Activity, target: IO[bytes]) -> None:
|
125
|
+
activity.time_series.to_parquet(target, index=False)
|
126
|
+
|
127
|
+
|
128
|
+
def export_activity_as_xlsx(activity: Activity, target: IO[bytes]) -> None:
|
129
|
+
activity.time_series.to_excel(target, index=False)
|
@@ -158,7 +158,7 @@ def apply_search_query(
|
|
158
158
|
)
|
159
159
|
|
160
160
|
if search_query.start_begin:
|
161
|
-
filter_clauses.append(Activity.start
|
161
|
+
filter_clauses.append(Activity.start >= search_query.start_begin)
|
162
162
|
if search_query.start_end:
|
163
163
|
filter_clauses.append(Activity.start < search_query.start_end)
|
164
164
|
|
@@ -38,6 +38,7 @@ from .blueprints.eddington_blueprints import register_eddington_blueprint
|
|
38
38
|
from .blueprints.entry_views import register_entry_views
|
39
39
|
from .blueprints.equipment_blueprint import make_equipment_blueprint
|
40
40
|
from .blueprints.explorer_blueprint import make_explorer_blueprint
|
41
|
+
from .blueprints.export_blueprint import make_export_blueprint
|
41
42
|
from .blueprints.heatmap_blueprint import make_heatmap_blueprint
|
42
43
|
from .blueprints.photo_blueprint import make_photo_blueprint
|
43
44
|
from .blueprints.plot_builder_blueprint import make_plot_builder_blueprint
|
@@ -160,6 +161,7 @@ def web_ui_main(
|
|
160
161
|
tile_getter,
|
161
162
|
image_transforms,
|
162
163
|
),
|
164
|
+
"/export": make_export_blueprint(authenticator),
|
163
165
|
"/heatmap": make_heatmap_blueprint(
|
164
166
|
repository, tile_visit_accessor, config_accessor(), search_query_history
|
165
167
|
),
|
@@ -175,7 +177,7 @@ def web_ui_main(
|
|
175
177
|
"/summary": make_summary_blueprint(repository, config, search_query_history),
|
176
178
|
"/tile": make_tile_blueprint(image_transforms, tile_getter),
|
177
179
|
"/upload": make_upload_blueprint(
|
178
|
-
repository, tile_visit_accessor, config_accessor(), authenticator
|
180
|
+
repository, tile_visit_accessor, config_accessor(), authenticator, flasher
|
179
181
|
),
|
180
182
|
}
|
181
183
|
|
@@ -3,8 +3,10 @@ from typing import Callable
|
|
3
3
|
|
4
4
|
from flask import flash
|
5
5
|
from flask import redirect
|
6
|
+
from flask import request
|
6
7
|
from flask import session
|
7
8
|
from flask import url_for
|
9
|
+
from flask.typing import RouteCallable
|
8
10
|
|
9
11
|
from ..core.config import Config
|
10
12
|
|
@@ -32,14 +34,14 @@ class Authenticator:
|
|
32
34
|
|
33
35
|
|
34
36
|
def needs_authentication(authenticator: Authenticator) -> Callable:
|
35
|
-
def decorator(route:
|
37
|
+
def decorator(route: RouteCallable) -> RouteCallable:
|
36
38
|
@functools.wraps(route)
|
37
39
|
def wrapped_route(*args, **kwargs):
|
38
40
|
if authenticator.is_authenticated():
|
39
41
|
return route(*args, **kwargs)
|
40
42
|
else:
|
41
43
|
flash("You need to be logged in to view that site.", category="Warning")
|
42
|
-
return redirect(url_for("auth.index"))
|
44
|
+
return redirect(url_for("auth.index", redirect=request.url))
|
43
45
|
|
44
46
|
return wrapped_route
|
45
47
|
|
@@ -14,9 +14,12 @@ def make_auth_blueprint(authenticator: Authenticator) -> Blueprint:
|
|
14
14
|
def index():
|
15
15
|
if request.method == "POST":
|
16
16
|
authenticator.authenticate(request.form["password"])
|
17
|
+
if redirect_to := request.form["redirect"]:
|
18
|
+
return redirect(redirect_to)
|
17
19
|
return render_template(
|
18
20
|
"auth/index.html.j2",
|
19
21
|
is_authenticated=authenticator.is_authenticated(),
|
22
|
+
redirect=request.args.get("redirect", ""),
|
20
23
|
)
|
21
24
|
|
22
25
|
@blueprint.route("/logout")
|