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