from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional import httpx import os from dotenv import load_dotenv # Загрузка переменных окружения из .env файла load_dotenv() app = FastAPI(title="OSRM API Wrapper") # --- Модели для запросов и ответов --- # Входная модель для обоих эндпоинтов: массив точек class PointsRequest(BaseModel): points: List[List[float]] # Формат: [[lat, lon], [lat, lon], ...] # Модель ответа для /table class TableResponse(BaseModel): distances: List[List[float]] durations: List[List[float]] # Модель для одного шага в маршруте (для /route) class RouteStep(BaseModel): location: List[float] # [lon, lat] distance: float type: str modifier: Optional[str] = "" name: str # Модель ответа для /route class RouteResponse(BaseModel): steps: List[RouteStep] # --- Конфигурация --- # Чтение URL OSRM из переменной окружения OSRM_BASE_URL = os.getenv("OSRM_URL") if not OSRM_BASE_URL: raise RuntimeError("Переменная окружения OSRM_URL не установлена.") # --- Эндпоинты --- @app.post("/table", response_model=TableResponse) async def table(request: PointsRequest): """ Принимает список координат и возвращает матрицы расстояний и времени в пути между ними. """ if len(request.points) < 2: raise HTTPException(status_code=400, detail="Нужно минимум 2 точки") # OSRM ожидает формат lon,lat coords = ";".join(f"{lon},{lat}" for lat, lon in request.points) # Эндпоинт для Table service url = f"{OSRM_BASE_URL}/table/v1/foot/{coords}?annotations=duration,distance" print(f"Запрос к OSRM: {url}") async with httpx.AsyncClient() as client: try: resp = await client.get(url, timeout=10) resp.raise_for_status() except httpx.RequestError as exc: raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}") except httpx.HTTPStatusError as exc: raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}") data = resp.json() distances = data.get("distances") durations = data.get("durations") if distances is None or durations is None: raise HTTPException(status_code=500, detail="OSRM вернул некорректный ответ") return TableResponse(distances=distances, durations=durations) @app.post("/route", response_model=RouteResponse) async def route(request: PointsRequest): """ Принимает две точки (начальную и конечную) и возвращает пошаговый маршрут. """ if len(request.points) != 2: raise HTTPException(status_code=400, detail="Для маршрута нужно ровно 2 точки") coords = ";".join(f"{lon},{lat}" for lat, lon in request.points) # Эндпоинт для Route service с параметром steps=true url = f"{OSRM_BASE_URL}/route/v1/foot/{coords}?steps=true" print(f"Запрос к OSRM: {url}") async with httpx.AsyncClient() as client: try: resp = await client.get(url, timeout=10) resp.raise_for_status() except httpx.RequestError as exc: raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}") except httpx.HTTPStatusError as exc: raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}") data = resp.json() if "routes" not in data or len(data["routes"]) == 0: raise HTTPException(status_code=500, detail="OSRM не вернул маршруты") # Извлекаем шаги из ответа raw_steps = data["routes"][0]["legs"][0]["steps"] formatted_steps = [] for step in raw_steps: maneuver = step["maneuver"] formatted_steps.append( RouteStep( location=[maneuver["location"][1], maneuver["location"][0]], distance=step["distance"], type=maneuver["type"], modifier=maneuver.get("modifier"), name=step.get("name", "") ) ) return RouteResponse(steps=formatted_steps)