112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
from datetime import UTC, datetime, timedelta
|
||
from xml.etree.ElementTree import Element, SubElement, tostring
|
||
|
||
from flask import Blueprint, Response, abort, current_app, jsonify, render_template, request
|
||
from flask_login import current_user, login_required
|
||
|
||
from .extensions import db, limiter
|
||
from .models import PlannedRoute
|
||
from .planner_engine import PlannerError, generate, validate
|
||
|
||
planner = Blueprint("planner", __name__, url_prefix="/routes")
|
||
|
||
|
||
def owned(route_id, saved_only=False):
|
||
route = db.get_or_404(PlannedRoute, route_id)
|
||
if route.owner_id != current_user.id or (saved_only and not route.saved):
|
||
abort(404)
|
||
return route
|
||
|
||
|
||
def serialise(route):
|
||
return dict(id=route.id, title=route.title, saved=route.saved, **route.payload)
|
||
|
||
|
||
@planner.get("")
|
||
@login_required
|
||
def index():
|
||
saved = db.session.scalars(
|
||
db.select(PlannedRoute)
|
||
.where(PlannedRoute.owner_id == current_user.id, PlannedRoute.saved.is_(True))
|
||
.order_by(PlannedRoute.created_at.desc(), PlannedRoute.id.desc())
|
||
.limit(100)
|
||
).all()
|
||
return render_template(
|
||
"planner.html", configured=bool(current_app.config.get("ORS_API_KEY")), saved_routes=saved
|
||
)
|
||
|
||
|
||
@planner.post("/generate")
|
||
@login_required
|
||
@limiter.limit("6 per minute; 60 per day")
|
||
def candidates():
|
||
try:
|
||
preferences = validate(request.get_json(silent=True))
|
||
routes, warnings = generate(preferences, current_app.config.get("ORS_API_KEY"))
|
||
except PlannerError as error:
|
||
return jsonify(error=str(error)), 400
|
||
cutoff = datetime.now(UTC) - timedelta(days=1)
|
||
db.session.execute(
|
||
db.delete(PlannedRoute).where(
|
||
PlannedRoute.owner_id == current_user.id,
|
||
PlannedRoute.saved.is_(False),
|
||
PlannedRoute.created_at < cutoff,
|
||
)
|
||
)
|
||
rows = []
|
||
for route in routes:
|
||
row = PlannedRoute(owner_id=current_user.id, title=route["label"], payload=route)
|
||
db.session.add(row)
|
||
rows.append(row)
|
||
db.session.commit()
|
||
return jsonify(routes=[serialise(row) for row in rows], warnings=warnings)
|
||
|
||
|
||
@planner.get("/<int:route_id>")
|
||
@login_required
|
||
def detail(route_id):
|
||
return jsonify(serialise(owned(route_id, saved_only=True)))
|
||
|
||
|
||
@planner.post("/<int:route_id>/save")
|
||
@login_required
|
||
def save(route_id):
|
||
row = owned(route_id)
|
||
data = request.get_json(silent=True)
|
||
title = data.get("title") if isinstance(data, dict) else None
|
||
if not isinstance(title, str) or not 1 <= len(title.strip()) <= 160:
|
||
return jsonify(error="Name your route using 1–160 characters."), 400
|
||
row.title = title.strip()
|
||
row.saved = True
|
||
db.session.commit()
|
||
return jsonify(serialise(row))
|
||
|
||
|
||
@planner.post("/<int:route_id>/delete")
|
||
@login_required
|
||
def delete(route_id):
|
||
db.session.delete(owned(route_id, saved_only=True))
|
||
db.session.commit()
|
||
return jsonify(ok=True)
|
||
|
||
|
||
@planner.get("/<int:route_id>/gpx")
|
||
@login_required
|
||
def export(route_id):
|
||
row = owned(route_id, saved_only=True)
|
||
root = Element(
|
||
"gpx", {"version": "1.1", "creator": "Stride", "xmlns": "http://www.topografix.com/GPX/1/1"}
|
||
)
|
||
track = SubElement(root, "trk")
|
||
SubElement(track, "name").text = row.title
|
||
segment = SubElement(track, "trkseg")
|
||
for coordinate in row.payload["coordinates"]:
|
||
point = SubElement(segment, "trkpt", {"lat": str(coordinate[1]), "lon": str(coordinate[0])})
|
||
if len(coordinate) == 3:
|
||
SubElement(point, "ele").text = str(coordinate[2])
|
||
return Response(
|
||
tostring(root, encoding="utf-8", xml_declaration=True),
|
||
mimetype="application/gpx+xml",
|
||
headers={"Content-Disposition": f'attachment; filename="stride-route-{row.id}.gpx"'},
|
||
)
|