|
from urllib.parse import urlencode |
|
|
|
from pyproj import Transformer |
|
import requests |
|
import logging |
|
from typing import Tuple, Optional, Dict, Any |
|
|
|
logger = logging.getLogger("CamptocampAPI") |
|
|
|
class CamptocampAPI: |
|
""" |
|
A Python wrapper for the Camptocamp.org REST API v6. |
|
Supports querying outings, routes, waypoints, and more. |
|
""" |
|
|
|
BASE_URL = "https://api.camptocamp.org" |
|
|
|
def __init__(self, language: str = "en") -> None: |
|
self.language = language |
|
|
|
from urllib.parse import urlencode |
|
|
|
def _request(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]: |
|
params["pl"] = self.language |
|
url = f"{self.BASE_URL}{endpoint}" |
|
full_url = f"{url}?{urlencode(params)}" |
|
|
|
logger.info(f"[API REQUEST] {url} with params: {params}") |
|
logger.info(f"[DEBUG URL] curl '{full_url}'") |
|
|
|
response = requests.get(url, params=params) |
|
response.raise_for_status() |
|
return response.json() |
|
|
|
def get_outings( |
|
self, |
|
bbox: Tuple[float, float, float, float], |
|
date_range: Optional[Tuple[str, str]] = None, |
|
activity: Optional[str] = None, |
|
limit: int = 10 |
|
) -> Dict[str, Any]: |
|
params = { |
|
"bbox": ",".join(map(str, bbox)), |
|
"limit": limit, |
|
"orderby": "-date" |
|
} |
|
if date_range: |
|
params["date"] = f"{date_range[0]},{date_range[1]}" |
|
if activity: |
|
params["act"] = activity |
|
return self._request("/outings", params) |
|
|
|
def search_routes_by_activity( |
|
self, |
|
bbox: Tuple[float, float, float, float], |
|
activity: str, |
|
limit: int = 10 |
|
) -> Dict[str, Any]: |
|
params = { |
|
"bbox": ",".join(map(str, bbox)), |
|
"act": activity, |
|
"limit": limit, |
|
"orderby": "-date" |
|
} |
|
return self._request("/routes", params) |
|
|
|
def get_route_details(self, route_id: int) -> Dict[str, Any]: |
|
return self._request(f"/routes/{route_id}/{self.language}", {}) |
|
|
|
def search_waypoints( |
|
self, |
|
bbox: Tuple[float, float, float, float], |
|
limit: int = 10 |
|
) -> Dict[str, Any]: |
|
params = { |
|
"bbox": ",".join(map(str, bbox)), |
|
"limit": limit |
|
} |
|
return self._request("/waypoints", params) |
|
|
|
@staticmethod |
|
def get_bbox_from_location(query: str) -> Optional[Tuple[float, float, float, float]]: |
|
""" |
|
Geocode a location string and return a bounding box. |
|
|
|
Args: |
|
query: Name of the place or location (e.g., "Chamonix, France"). |
|
|
|
Returns: |
|
Bounding box as (west, south, east, north) or None if not found. |
|
""" |
|
url = "https://nominatim.openstreetmap.org/search" |
|
params = { |
|
"q": query, |
|
"format": "json", |
|
"limit": 1 |
|
} |
|
headers = {"User-Agent": "camptocamp-api-wrapper"} |
|
logger.info(f"Geocoding location: {query}") |
|
response = requests.get(url, params=params, headers=headers) |
|
response.raise_for_status() |
|
results = response.json() |
|
if not results: |
|
logger.warning(f"No results found for: {query}") |
|
return None |
|
bbox = results[0]["boundingbox"] |
|
logger.info(f"BBox for '{query}': {bbox}") |
|
return CamptocampAPI.convert_bbox_to_webmercator(( |
|
float(bbox[2]), |
|
float(bbox[0]), |
|
float(bbox[3]), |
|
float(bbox[1]) |
|
)) |
|
|
|
@staticmethod |
|
def convert_bbox_to_webmercator(bbox: Tuple[float, float, float, float]) -> Tuple[int, int, int, int]: |
|
""" |
|
Convert a WGS84 bbox (lon/lat) to EPSG:3857 (Web Mercator) in meters. |
|
|
|
Args: |
|
bbox: (west, south, east, north) in degrees |
|
|
|
Returns: |
|
(west, south, east, north) in meters |
|
""" |
|
transformer = Transformer.from_crs("epsg:4326", "epsg:3857", always_xy=True) |
|
west, south = transformer.transform(bbox[0], bbox[1]) |
|
east, north = transformer.transform(bbox[2], bbox[3]) |
|
return int(west), int(south), int(east), int(north) |