Files
Stride/app/planner.py
T

125 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, search_locations, 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.post("/search")
@login_required
@limiter.limit("30 per minute; 600 per day")
def search():
try:
locations = search_locations(
request.get_json(silent=True), current_app.config.get("ORS_API_KEY")
)
except PlannerError as error:
return jsonify(error=str(error)), 400
return jsonify(locations=locations)
@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"'},
)