from typing import Literal, List import urllib import json import xml.etree.ElementTree as ET from .caching_strategy import get_cache_key, CachingStrategy ElementTypes = List[Literal['way', 'node', 'relation']] def build_query(area: tuple, element_types: ElementTypes, selector: str, conditions=[], out='center'): """ Constructs a query string for the Overpass API to retrieve OpenStreetMap (OSM) data. Args: area (tuple): A tuple representing the geographical search area, typically in the format (radius, latitude, longitude). The first element is a string like "around:2000" specifying the search radius, and the second and third elements represent the latitude and longitude as floats or strings. element_types (list[str]): A list of OSM element types to search for. Must be one or more of 'Way', 'Node', or 'Relation'. selector (str): The key or tag to filter the OSM elements (e.g., 'amenity', 'highway', etc.). conditions (list, optional): A list of conditions to apply as additional filters for the selected OSM elements. The conditions should be written in the Overpass QL format, and they are combined with '&&' if multiple are provided. Defaults to an empty list. out (str, optional): Specifies the output type, such as 'center', 'body', or 'tags'. Defaults to 'center'. Returns: str: The constructed Overpass QL query string. Notes: - If no conditions are provided, the query will just use the `selector` to filter the OSM elements without additional constraints. - The search area must always formatted as "(radius, lat, lon)". """ if not isinstance(conditions, list) : conditions = [conditions] if not isinstance(element_types, list) : element_types = [element_types] query = '(' # Round the radius to nearest 50 and coordinates to generate less queries search_radius = round(area[0] / 50) * 50 loc = tuple((round(area[1], 2), round(area[2], 2))) search_area = f"(around:{search_radius}, {str(loc[0])}, {str(loc[1])})" if conditions : conditions = '(if: ' + ' && '.join(conditions) + ')' else : conditions = '' for elem in element_types : query += elem + '[' + selector + ']' + conditions + search_area + ';' query += ');' + f'out {out};' return query def send_overpass_query(query: str) -> dict: """ Sends the Overpass QL query to the Overpass API and returns the parsed JSON response. Args: query (str): The Overpass QL query to be sent to the Overpass API. Returns: dict: The parsed JSON response from the Overpass API, or None if the request fails. """ # Generate a cache key for the current query cache_key = get_cache_key(query) # Try to fetch the result from the cache cached_response = CachingStrategy.get(cache_key) if cached_response is not None : print("Cache hit!") return cached_response # Define the Overpass API endpoint overpass_url = "https://overpass-api.de/api/interpreter" # Prepare the data to be sent as POST request, encoded as bytes data = urllib.parse.urlencode({'data': query}).encode('utf-8') # Create a custom header with a User-Agent headers = { 'User-Agent': 'Mozilla/5.0 (compatible; OverpassQuery/1.0; +http://example.com)', } try: # Create a Request object with the specified URL, data, and headers request = urllib.request.Request(overpass_url, data=data, headers=headers) # Send the request and read the response with urllib.request.urlopen(request) as response: # Read and decode the response response_data = response.read().decode('utf-8') root = ET.fromstring(response_data) # Cache the response data as an ElementTree root CachingStrategy.set(cache_key, root) return root except urllib.error.URLError as e: print(f"Error connecting to Overpass API: {e}") return None except json.JSONDecodeError: print("Error decoding the JSON response from Overpass API.") return None