from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response import requests class OverpassViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] BASE_URL = "https://overpass-api.de/api/interpreter" HEADERS = {'User-Agent': 'AdventureLog Server'} def make_overpass_query(self, query): """ Sends a query to the Overpass API and returns the response data. Args: query (str): The Overpass QL query string. Returns: dict: Parsed JSON response from the Overpass API. Raises: Response: DRF Response object with an error message in case of failure. """ url = f"{self.BASE_URL}?data={query}" try: response = requests.get(url, headers=self.HEADERS) response.raise_for_status() # Raise an exception for HTTP errors return response.json() except requests.exceptions.RequestException: return Response({"error": "Failed to connect to Overpass API"}, status=500) except requests.exceptions.JSONDecodeError: return Response({"error": "Invalid response from Overpass API"}, status=400) def parse_overpass_response(self, data, request): """ Parses the JSON response from the Overpass API and extracts relevant data, turning it into an adventure-structured object. Args: response (dict): The JSON response from the Overpass API. Returns: list: A list of adventure objects with structured data. """ # Extract elements (nodes/ways/relations) from the response nodes = data.get('elements', []) adventures = [] # include all entries, even the ones that do not have lat long all = request.query_params.get('all', False) for node in nodes: # Ensure we are working with a "node" type (can also handle "way" or "relation" if needed) if node.get('type') not in ['node', 'way', 'relation']: continue # Extract tags and general data tags = node.get('tags', {}) adventure = { "id": node.get('id'), # Include the unique OSM ID "type": node.get('type'), # Type of element (node, way, relation) "name": tags.get('name', tags.get('official_name', '')), # Fallback to 'official_name' "description": tags.get('description', None), # Additional descriptive information "latitude": node.get('lat', None), # Use None for consistency with missing values "longitude": node.get('lon', None), "address": { "city": tags.get('addr:city', None), "housenumber": tags.get('addr:housenumber', None), "postcode": tags.get('addr:postcode', None), "state": tags.get('addr:state', None), "street": tags.get('addr:street', None), "country": tags.get('addr:country', None), # Add 'country' if available "suburb": tags.get('addr:suburb', None), # Add 'suburb' for more granularity }, "feature_id": tags.get('gnis:feature_id', None), "tag": next((tags.get(key, None) for key in ['leisure', 'tourism', 'natural', 'historic', 'amenity'] if key in tags), None), "contact": { "phone": tags.get('phone', None), "email": tags.get('contact:email', None), "website": tags.get('website', None), "facebook": tags.get('contact:facebook', None), # Social media links "twitter": tags.get('contact:twitter', None), }, # "tags": tags, # Include all raw tags for future use } # Filter out adventures with no name, latitude, or longitude if (adventure["name"] and adventure["latitude"] is not None and -90 <= adventure["latitude"] <= 90 and adventure["longitude"] is not None and -180 <= adventure["longitude"] <= 180) or all: adventures.append(adventure) return adventures @action(detail=False, methods=['get']) def query(self, request): """ Radius-based search for tourism-related locations around given coordinates. """ lat = request.query_params.get('lat') lon = request.query_params.get('lon') radius = request.query_params.get('radius', '1000') # Default radius: 1000 meters valid_categories = ['lodging', 'food', 'tourism'] category = request.query_params.get('category', 'all') if category not in valid_categories: return Response({"error": f"Invalid category. Valid categories: {', '.join(valid_categories)}"}, status=400) if category == 'tourism': query = f""" [out:json]; ( node(around:{radius},{lat},{lon})["tourism"]; node(around:{radius},{lat},{lon})["leisure"]; node(around:{radius},{lat},{lon})["historic"]; node(around:{radius},{lat},{lon})["sport"]; node(around:{radius},{lat},{lon})["natural"]; node(around:{radius},{lat},{lon})["attraction"]; node(around:{radius},{lat},{lon})["museum"]; node(around:{radius},{lat},{lon})["zoo"]; node(around:{radius},{lat},{lon})["aquarium"]; ); out; """ if category == 'lodging': query = f""" [out:json]; ( node(around:{radius},{lat},{lon})["tourism"="hotel"]; node(around:{radius},{lat},{lon})["tourism"="motel"]; node(around:{radius},{lat},{lon})["tourism"="guest_house"]; node(around:{radius},{lat},{lon})["tourism"="hostel"]; node(around:{radius},{lat},{lon})["tourism"="camp_site"]; node(around:{radius},{lat},{lon})["tourism"="caravan_site"]; node(around:{radius},{lat},{lon})["tourism"="chalet"]; node(around:{radius},{lat},{lon})["tourism"="alpine_hut"]; node(around:{radius},{lat},{lon})["tourism"="apartment"]; ); out; """ if category == 'food': query = f""" [out:json]; ( node(around:{radius},{lat},{lon})["amenity"="restaurant"]; node(around:{radius},{lat},{lon})["amenity"="cafe"]; node(around:{radius},{lat},{lon})["amenity"="fast_food"]; node(around:{radius},{lat},{lon})["amenity"="pub"]; node(around:{radius},{lat},{lon})["amenity"="bar"]; node(around:{radius},{lat},{lon})["amenity"="food_court"]; node(around:{radius},{lat},{lon})["amenity"="ice_cream"]; node(around:{radius},{lat},{lon})["amenity"="bakery"]; node(around:{radius},{lat},{lon})["amenity"="confectionery"]; ); out; """ # Validate required parameters if not lat or not lon: return Response( {"error": "Latitude and longitude parameters are required."}, status=400 ) data = self.make_overpass_query(query) adventures = self.parse_overpass_response(data, request) return Response(adventures) @action(detail=False, methods=['get'], url_path='search') def search(self, request): """ Name-based search for nodes with the specified name. """ name = request.query_params.get('name') # Validate required parameter if not name: return Response({"error": "Name parameter is required."}, status=400) # Construct Overpass API query query = f'[out:json];node["name"~"{name}",i];out;' data = self.make_overpass_query(query) adventures = self.parse_overpass_response(data, request) return Response(adventures)