2025-01-17 16:50:01 -05:00
|
|
|
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()
|
2025-01-17 18:22:03 -05:00
|
|
|
except requests.exceptions.RequestException:
|
|
|
|
return Response({"error": "Failed to connect to Overpass API"}, status=500)
|
2025-01-17 16:50:01 -05:00
|
|
|
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)
|