from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from pydantic import BaseModel from typing import Optional, List from datetime import datetime import polyline as polyline_lib from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User, Segment, SegmentEffort, Activity, ActivityDataPoint from app.services.route_matcher import haversine_m router = APIRouter() class SegmentCreate(BaseModel): name: str activity_id: int start_distance_m: float end_distance_m: float class EffortOut(BaseModel): activity_id: int activity_name: str date: Optional[datetime] duration_s: float rank: Optional[int] class SegmentOut(BaseModel): id: int name: str sport_type: Optional[str] polyline: Optional[str] distance_m: Optional[float] created_from_activity_id: Optional[int] effort_count: int = 0 best_s: Optional[float] = None class SegmentDetailOut(SegmentOut): leaderboard: List[EffortOut] = [] class ActivitySegmentOut(BaseModel): segment_id: int name: str polyline: Optional[str] distance_m: Optional[float] duration_s: float rank: Optional[int] # this activity's place on the leaderboard best_s: Optional[float] # current gold time effort_count: int def _bbox(coords): lats = [c[0] for c in coords] lons = [c[1] for c in coords] return {"min_lat": min(lats), "max_lat": max(lats), "min_lon": min(lons), "max_lon": max(lons)} async def _own_segment(segment_id: int, user_id: int, db: AsyncSession) -> Segment: seg = (await db.execute( select(Segment).where(Segment.id == segment_id, Segment.user_id == user_id) )).scalar_one_or_none() if not seg: raise HTTPException(status_code=404, detail="Segment not found") return seg @router.get("/", response_model=List[SegmentOut]) async def list_segments( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): segs = (await db.execute( select(Segment).where(Segment.user_id == current_user.id).order_by(Segment.created_at.desc()) )).scalars().all() out = [] for s in segs: agg = (await db.execute( select(func.count(SegmentEffort.id), func.min(SegmentEffort.duration_s)) .where(SegmentEffort.segment_id == s.id) )).one() out.append(SegmentOut( id=s.id, name=s.name, sport_type=s.sport_type, polyline=s.polyline, distance_m=s.distance_m, created_from_activity_id=s.created_from_activity_id, effort_count=agg[0] or 0, best_s=agg[1], )) return out @router.post("/", response_model=SegmentOut) async def create_segment( body: SegmentCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): activity = (await db.execute( select(Activity).where(Activity.id == body.activity_id, Activity.user_id == current_user.id) )).scalar_one_or_none() if not activity: raise HTTPException(status_code=404, detail="Activity not found") lo, hi = sorted((body.start_distance_m, body.end_distance_m)) dps = (await db.execute( select(ActivityDataPoint) .where(ActivityDataPoint.activity_id == activity.id) .order_by(ActivityDataPoint.timestamp) )).scalars().all() coords = [ (p.latitude, p.longitude) for p in dps if p.distance_m is not None and p.latitude is not None and p.longitude is not None and lo <= p.distance_m <= hi ] if len(coords) < 2: raise HTTPException(status_code=400, detail="Selected range has too few GPS points") seg = Segment( user_id=current_user.id, name=body.name.strip() or "Segment", sport_type=activity.sport_type, polyline=polyline_lib.encode(coords), start_lat=coords[0][0], start_lng=coords[0][1], end_lat=coords[-1][0], end_lng=coords[-1][1], distance_m=max(0.0, hi - lo), bounding_box=_bbox(coords), created_from_activity_id=activity.id, ) db.add(seg) await db.commit() await db.refresh(seg) # Match across all activities in the background. from app.workers.tasks import match_segment match_segment.delay(seg.id) return SegmentOut( id=seg.id, name=seg.name, sport_type=seg.sport_type, polyline=seg.polyline, distance_m=seg.distance_m, created_from_activity_id=seg.created_from_activity_id, effort_count=0, best_s=None, ) @router.get("/by-activity/{activity_id}", response_model=List[ActivitySegmentOut]) async def segments_for_activity( activity_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Segments that this activity has an effort on, with the activity's place + the gold time.""" rows = (await db.execute( select(Segment, SegmentEffort) .join(SegmentEffort, SegmentEffort.segment_id == Segment.id) .where(Segment.user_id == current_user.id, SegmentEffort.activity_id == activity_id) .order_by(Segment.created_at.desc()) )).all() out = [] for seg, effort in rows: agg = (await db.execute( select(func.count(SegmentEffort.id), func.min(SegmentEffort.duration_s)) .where(SegmentEffort.segment_id == seg.id) )).one() out.append(ActivitySegmentOut( segment_id=seg.id, name=seg.name, polyline=seg.polyline, distance_m=seg.distance_m, duration_s=effort.duration_s, rank=effort.rank, best_s=agg[1], effort_count=agg[0] or 0, )) return out @router.get("/{segment_id}", response_model=SegmentDetailOut) async def get_segment( segment_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): seg = await _own_segment(segment_id, current_user.id, db) rows = (await db.execute( select(SegmentEffort, Activity) .join(Activity, Activity.id == SegmentEffort.activity_id) .where(SegmentEffort.segment_id == seg.id) .order_by(SegmentEffort.duration_s) )).all() leaderboard = [ EffortOut( activity_id=e.activity_id, activity_name=a.name, date=e.achieved_at or a.start_time, duration_s=e.duration_s, rank=e.rank, ) for e, a in rows ] return SegmentDetailOut( id=seg.id, name=seg.name, sport_type=seg.sport_type, polyline=seg.polyline, distance_m=seg.distance_m, created_from_activity_id=seg.created_from_activity_id, effort_count=len(leaderboard), best_s=leaderboard[0].duration_s if leaderboard else None, leaderboard=leaderboard, ) @router.delete("/{segment_id}", status_code=204) async def delete_segment( segment_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): seg = await _own_segment(segment_id, current_user.id, db) await db.delete(seg) await db.commit()