""" HTML views for the unified map service. Provides web interfaces for map functionality with HTMX integration. """ import json from typing import Dict, Any, Optional, Set from django.shortcuts import render, get_object_or_404 from django.http import JsonResponse, HttpRequest, HttpResponse from django.views.generic import TemplateView, View from django.views.decorators.http import require_http_methods from django.utils.decorators import method_decorator from django.contrib.auth.mixins import LoginRequiredMixin from django.core.paginator import Paginator from django.core.exceptions import ValidationError from django.db.models import Q from ..services.map_service import unified_map_service from ..services.data_structures import GeoBounds, MapFilters, LocationType class MapViewMixin: """Mixin providing common functionality for map views.""" def get_map_context(self, request: HttpRequest) -> Dict[str, Any]: """Get common context data for map views.""" return { 'map_api_urls': { 'locations': '/api/map/locations/', 'search': '/api/map/search/', 'bounds': '/api/map/bounds/', 'location_detail': '/api/map/locations/', }, 'location_types': [lt.value for lt in LocationType], 'default_zoom': 10, 'enable_clustering': True, 'enable_search': True, } def parse_location_types(self, request: HttpRequest) -> Optional[Set[LocationType]]: """Parse location types from request parameters.""" types_param = request.GET.get('types') if types_param: try: return { LocationType(t.strip()) for t in types_param.split(',') if t.strip() in [lt.value for lt in LocationType] } except ValueError: return None return None class UniversalMapView(MapViewMixin, TemplateView): """ Main universal map view showing all location types. URL: /maps/ """ template_name = 'maps/universal_map.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.get_map_context(self.request)) # Additional context for universal map context.update({ 'page_title': 'Interactive Map - All Locations', 'map_type': 'universal', 'show_all_types': True, 'initial_location_types': [lt.value for lt in LocationType], 'filters_enabled': True, }) # Handle initial bounds from query parameters if all(param in self.request.GET for param in ['north', 'south', 'east', 'west']): try: context['initial_bounds'] = { 'north': float(self.request.GET['north']), 'south': float(self.request.GET['south']), 'east': float(self.request.GET['east']), 'west': float(self.request.GET['west']), } except (ValueError, TypeError): pass return context class ParkMapView(MapViewMixin, TemplateView): """ Map view focused specifically on parks. URL: /maps/parks/ """ template_name = 'maps/park_map.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.get_map_context(self.request)) # Park-specific context context.update({ 'page_title': 'Theme Parks Map', 'map_type': 'parks', 'show_all_types': False, 'initial_location_types': [LocationType.PARK.value], 'filters_enabled': True, 'park_specific_filters': True, }) return context class NearbyLocationsView(MapViewMixin, TemplateView): """ View for showing locations near a specific point. URL: /maps/nearby/ """ template_name = 'maps/nearby_locations.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.get_map_context(self.request)) # Parse coordinates from query parameters lat = self.request.GET.get('lat') lng = self.request.GET.get('lng') radius = self.request.GET.get('radius', '50') # Default 50km radius if lat and lng: try: center_lat = float(lat) center_lng = float(lng) search_radius = min(200, max(1, float(radius))) # Clamp between 1-200km context.update({ 'page_title': f'Locations Near {center_lat:.4f}, {center_lng:.4f}', 'map_type': 'nearby', 'center_coordinates': {'lat': center_lat, 'lng': center_lng}, 'search_radius': search_radius, 'show_radius_circle': True, }) except (ValueError, TypeError): context['error'] = 'Invalid coordinates provided' else: context.update({ 'page_title': 'Nearby Locations', 'map_type': 'nearby', 'prompt_for_location': True, }) return context class LocationFilterView(MapViewMixin, View): """ HTMX endpoint for updating map when filters change. URL: /maps/htmx/filter/ """ def get(self, request: HttpRequest) -> HttpResponse: """Return filtered location data for HTMX updates.""" try: # Parse filter parameters location_types = self.parse_location_types(request) search_query = request.GET.get('q', '').strip() country = request.GET.get('country', '').strip() state = request.GET.get('state', '').strip() # Create filters filters = None if any([location_types, search_query, country, state]): filters = MapFilters( location_types=location_types, search_query=search_query or None, country=country or None, state=state or None, has_coordinates=True ) # Get filtered locations map_response = unified_map_service.get_map_data( filters=filters, zoom_level=int(request.GET.get('zoom', '10')), cluster=request.GET.get('cluster', 'true').lower() == 'true' ) # Return JSON response for HTMX return JsonResponse({ 'status': 'success', 'data': map_response.to_dict(), 'filters_applied': map_response.filters_applied }) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=400) class LocationSearchView(MapViewMixin, View): """ HTMX endpoint for real-time location search. URL: /maps/htmx/search/ """ def get(self, request: HttpRequest) -> HttpResponse: """Return search results for HTMX updates.""" query = request.GET.get('q', '').strip() if not query or len(query) < 3: return render(request, 'maps/partials/search_results.html', { 'results': [], 'query': query, 'message': 'Enter at least 3 characters to search' }) try: # Parse optional location types location_types = self.parse_location_types(request) limit = min(20, max(5, int(request.GET.get('limit', '10')))) # Perform search results = unified_map_service.search_locations( query=query, location_types=location_types, limit=limit ) return render(request, 'maps/partials/search_results.html', { 'results': results, 'query': query, 'count': len(results) }) except Exception as e: return render(request, 'maps/partials/search_results.html', { 'results': [], 'query': query, 'error': str(e) }) class MapBoundsUpdateView(MapViewMixin, View): """ HTMX endpoint for updating locations when map bounds change. URL: /maps/htmx/bounds/ """ def post(self, request: HttpRequest) -> HttpResponse: """Update map data when bounds change.""" try: data = json.loads(request.body) # Parse bounds bounds = GeoBounds( north=float(data['north']), south=float(data['south']), east=float(data['east']), west=float(data['west']) ) # Parse additional parameters zoom_level = int(data.get('zoom', 10)) location_types = None if 'types' in data: location_types = { LocationType(t) for t in data['types'] if t in [lt.value for lt in LocationType] } # Create filters if needed filters = None if location_types: filters = MapFilters(location_types=location_types) # Get updated map data map_response = unified_map_service.get_locations_by_bounds( north=bounds.north, south=bounds.south, east=bounds.east, west=bounds.west, location_types=location_types, zoom_level=zoom_level ) return JsonResponse({ 'status': 'success', 'data': map_response.to_dict() }) except (json.JSONDecodeError, ValueError, KeyError) as e: return JsonResponse({ 'status': 'error', 'message': f'Invalid request data: {str(e)}' }, status=400) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=500) class LocationDetailModalView(MapViewMixin, View): """ HTMX endpoint for showing location details in modal. URL: /maps/htmx/location/// """ def get(self, request: HttpRequest, location_type: str, location_id: int) -> HttpResponse: """Return location detail modal content.""" try: # Validate location type if location_type not in [lt.value for lt in LocationType]: return render(request, 'maps/partials/location_modal.html', { 'error': f'Invalid location type: {location_type}' }) # Get location details location = unified_map_service.get_location_details(location_type, location_id) if not location: return render(request, 'maps/partials/location_modal.html', { 'error': 'Location not found' }) return render(request, 'maps/partials/location_modal.html', { 'location': location, 'location_type': location_type }) except Exception as e: return render(request, 'maps/partials/location_modal.html', { 'error': str(e) }) class LocationListView(MapViewMixin, TemplateView): """ View for listing locations with pagination (non-map view). URL: /maps/list/ """ template_name = 'maps/location_list.html' paginate_by = 20 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Parse filters location_types = self.parse_location_types(self.request) search_query = self.request.GET.get('q', '').strip() country = self.request.GET.get('country', '').strip() state = self.request.GET.get('state', '').strip() # Create filters filters = None if any([location_types, search_query, country, state]): filters = MapFilters( location_types=location_types, search_query=search_query or None, country=country or None, state=state or None, has_coordinates=True ) # Get locations without clustering map_response = unified_map_service.get_map_data( filters=filters, cluster=False, use_cache=True ) # Paginate results paginator = Paginator(map_response.locations, self.paginate_by) page_number = self.request.GET.get('page') page_obj = paginator.get_page(page_number) context.update({ 'page_title': 'All Locations', 'locations': page_obj, 'total_count': map_response.total_count, 'applied_filters': filters, 'location_types': [lt.value for lt in LocationType], 'current_filters': { 'types': self.request.GET.getlist('types'), 'q': search_query, 'country': country, 'state': state, } }) return context