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]), # west float(bbox[0]), # south float(bbox[3]), # east float(bbox[1]) # north )) @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)