"""BAN YARO — Gassi-Routen""" import json, math, os, uuid from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from pydantic import BaseModel from typing import Optional, List from database import db from auth import get_current_user, get_current_user_optional router = APIRouter() def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float: R = 6_371_000 p1 = math.radians(lat1) p2 = math.radians(lat2) dp = math.radians(lat2 - lat1) dl = math.radians(lon2 - lon1) a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * R * math.asin(math.sqrt(a)) # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class GPSPoint(BaseModel): lat: float lon: float class RouteCreate(BaseModel): name: str beschreibung: Optional[str] = None gps_track: List[GPSPoint] distanz_km: Optional[float] = None dauer_min: Optional[int] = None schwierigkeit: Optional[str] = "leicht" # leicht | mittel | anspruchsvoll untergrund: Optional[str] = None # wald | asphalt | wiese | mix schatten: Optional[bool] = None leine_empfohlen: Optional[bool] = None is_public: Optional[bool] = True hunde_tauglichkeit: Optional[str] = None # eingeschränkt | gut | sehr_gut | premium class RouteUpdate(BaseModel): name: Optional[str] = None beschreibung: Optional[str] = None schwierigkeit: Optional[str] = None untergrund: Optional[str] = None schatten: Optional[bool] = None leine_empfohlen: Optional[bool] = None is_public: Optional[bool] = None hunde_tauglichkeit: Optional[str] = None def _simplify_track(track: list, max_pts: int = 40) -> list: """Reduziert GPS-Track auf max_pts Punkte für Vorschau.""" if len(track) <= max_pts: return track step = len(track) / max_pts return [track[round(i * step)] for i in range(max_pts)] def _parse(row) -> dict: d = dict(row) if isinstance(d.get('gps_track'), str): d['gps_track'] = json.loads(d['gps_track']) for k in ('schatten', 'leine_empfohlen', 'is_public'): if d.get(k) is not None: d[k] = bool(d[k]) if isinstance(d.get('foto_urls'), str): d['foto_urls'] = json.loads(d['foto_urls']) return d # ------------------------------------------------------------------ # GET /api/routes — Routen (optional: Umkreis vom Startpunkt) # ------------------------------------------------------------------ @router.get("") async def list_routes( lat: Optional[float] = None, lon: Optional[float] = None, radius: int = 10000, user = Depends(get_current_user_optional), ): with db() as conn: rows = conn.execute(""" SELECT r.id, r.user_id, r.name, r.beschreibung, r.distanz_km, r.dauer_min, r.schwierigkeit, r.untergrund, r.schatten, r.leine_empfohlen, r.bewertung, r.anz_bewertungen, r.created_at, r.gps_track, r.is_public, r.hunde_tauglichkeit, r.foto_urls, u.name AS user_name, json_extract(r.gps_track, '$[0].lat') AS start_lat, json_extract(r.gps_track, '$[0].lon') AS start_lon, json_extract(r.gps_track, '$[#-1].lat') AS end_lat, json_extract(r.gps_track, '$[#-1].lon') AS end_lon FROM routes r LEFT JOIN users u ON u.id = r.user_id ORDER BY r.created_at DESC """).fetchall() result = [] for row in rows: d = dict(row) for k in ('schatten', 'leine_empfohlen', 'is_public'): if d.get(k) is not None: d[k] = bool(d[k]) if isinstance(d.get('foto_urls'), str): d['foto_urls'] = json.loads(d['foto_urls']) raw_track = json.loads(d.get('gps_track') or '[]') d['preview_track'] = _simplify_track(raw_track, 40) del d['gps_track'] result.append(d) if lat is not None and lon is not None: result = [ r for r in result if r['start_lat'] and _haversine(lat, lon, r['start_lat'], r['start_lon']) <= radius ] user_id = user['id'] if user else None result = [r for r in result if r.get('is_public', True) or r.get('user_id') == user_id] return result # ------------------------------------------------------------------ # POST /api/routes — neue Route speichern (Login erforderlich) # ------------------------------------------------------------------ @router.post("", status_code=201) async def create_route(data: RouteCreate, user=Depends(get_current_user)): if len(data.gps_track) < 2: raise HTTPException(400, "GPS-Track braucht mindestens 2 Punkte.") gps_json = json.dumps([p.model_dump() for p in data.gps_track]) with db() as conn: cur = conn.execute(""" INSERT INTO routes (user_id, name, beschreibung, gps_track, distanz_km, dauer_min, schwierigkeit, untergrund, schatten, leine_empfohlen, is_public, hunde_tauglichkeit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user['id'], data.name, data.beschreibung, gps_json, data.distanz_km, data.dauer_min, data.schwierigkeit, data.untergrund, int(data.schatten) if data.schatten is not None else None, int(data.leine_empfohlen) if data.leine_empfohlen is not None else None, int(data.is_public) if data.is_public is not None else 1, data.hunde_tauglichkeit, )) row = conn.execute("SELECT * FROM routes WHERE id = ?", (cur.lastrowid,)).fetchone() return _parse(row) # ------------------------------------------------------------------ # GET /api/routes/{id} — Route mit vollem GPS-Track # ------------------------------------------------------------------ @router.get("/{route_id}") async def get_route(route_id: int): with db() as conn: row = conn.execute( "SELECT r.*, u.name AS user_name FROM routes r LEFT JOIN users u ON u.id = r.user_id WHERE r.id = ?", (route_id,) ).fetchone() if not row: raise HTTPException(404, "Route nicht gefunden.") return _parse(row) # ------------------------------------------------------------------ # PATCH /api/routes/{id} # ------------------------------------------------------------------ @router.patch("/{route_id}") async def update_route(route_id: int, data: RouteUpdate, user=Depends(get_current_user)): with db() as conn: row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone() if not row: raise HTTPException(404, "Route nicht gefunden.") if row['user_id'] != user['id']: raise HTTPException(403, "Nicht berechtigt.") updates = data.model_dump(exclude_none=True) if updates: for key in ('schatten', 'leine_empfohlen', 'is_public'): if key in updates: updates[key] = int(updates[key]) cols = ', '.join(f"{k} = ?" for k in updates) conn.execute(f"UPDATE routes SET {cols} WHERE id = ?", [*updates.values(), route_id]) row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone() return _parse(row) # ------------------------------------------------------------------ # DELETE /api/routes/{id} # ------------------------------------------------------------------ @router.delete("/{route_id}", status_code=204) async def delete_route(route_id: int, user=Depends(get_current_user)): with db() as conn: row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone() if not row: raise HTTPException(404, "Route nicht gefunden.") if row['user_id'] != user['id']: raise HTTPException(403, "Nicht berechtigt.") conn.execute("DELETE FROM routes WHERE id = ?", (route_id,)) # ------------------------------------------------------------------ # POST /api/routes/{id}/rate — Bewertung abgeben # ------------------------------------------------------------------ class RouteRate(BaseModel): wertung: float # 1–5 @router.post("/{route_id}/rate") async def rate_route(route_id: int, data: RouteRate, user=Depends(get_current_user)): if not 1 <= data.wertung <= 5: raise HTTPException(400, "Wertung muss zwischen 1 und 5 liegen.") with db() as conn: row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone() if not row: raise HTTPException(404, "Route nicht gefunden.") r = dict(row) n = (r['anz_bewertungen'] or 0) + 1 avg = ((r['bewertung'] or 0) * (n - 1) + data.wertung) / n conn.execute( "UPDATE routes SET bewertung=?, anz_bewertungen=? WHERE id=?", (round(avg, 2), n, route_id) ) return {'bewertung': round(avg, 2), 'anz_bewertungen': n} # ------------------------------------------------------------------ # POST /api/routes/{id}/photo — Foto hochladen # ------------------------------------------------------------------ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") @router.post("/{route_id}/photo", status_code=201) async def add_route_photo( route_id: int, file: UploadFile = File(...), user = Depends(get_current_user), ): with db() as conn: row = conn.execute("SELECT * FROM routes WHERE id=?", (route_id,)).fetchone() if not row: raise HTTPException(404, "Route nicht gefunden.") if dict(row)['user_id'] != user['id']: raise HTTPException(403, "Nicht berechtigt.") ext = os.path.splitext(file.filename or "")[1] or ".jpg" filename = f"route_{route_id}_{uuid.uuid4().hex[:8]}{ext}" path = os.path.join(MEDIA_DIR, "routes", filename) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(await file.read()) foto_url = f"/media/routes/{filename}" with db() as conn: row = conn.execute("SELECT foto_urls FROM routes WHERE id=?", (route_id,)).fetchone() urls = json.loads(dict(row)['foto_urls'] or '[]') urls.append(foto_url) conn.execute("UPDATE routes SET foto_urls=? WHERE id=?", (json.dumps(urls), route_id)) return {'foto_url': foto_url, 'foto_urls': urls}