| 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) |