""" Road trip planning views for theme parks. Provides interfaces for creating and managing multi-park road trips. """ import json from typing import Dict, Any, List, Optional from django.shortcuts import render, get_object_or_404, redirect from django.http import JsonResponse, HttpRequest, HttpResponse, Http404 from django.views.generic import TemplateView, View, DetailView 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.exceptions import ValidationError from django.contrib import messages from django.urls import reverse from django.db.models import Q from .models import Park from .services.roadtrip import RoadTripService from core.services.map_service import unified_map_service from core.services.data_structures import LocationType, MapFilters class RoadTripViewMixin: """Mixin providing common functionality for road trip views.""" def __init__(self): super().__init__() self.roadtrip_service = RoadTripService() def get_roadtrip_context(self, request: HttpRequest) -> Dict[str, Any]: """Get common context data for road trip views.""" return { 'roadtrip_api_urls': { 'create_trip': '/roadtrip/create/', 'find_parks_along_route': '/roadtrip/htmx/parks-along-route/', 'geocode': '/roadtrip/htmx/geocode/', }, 'max_parks_per_trip': 10, 'default_detour_km': 50, 'enable_osm_integration': True, } class RoadTripPlannerView(RoadTripViewMixin, TemplateView): """ Main road trip planning interface. URL: /roadtrip/ """ template_name = 'parks/roadtrip_planner.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.get_roadtrip_context(self.request)) # Get popular parks for suggestions popular_parks = Park.objects.filter( status='OPERATING', location__isnull=False ).select_related('location', 'operator').order_by('-ride_count')[:20] context.update({ 'page_title': 'Road Trip Planner', 'popular_parks': popular_parks, 'countries_with_parks': self._get_countries_with_parks(), 'enable_route_optimization': True, 'show_distance_estimates': True, }) return context def _get_countries_with_parks(self) -> List[str]: """Get list of countries that have theme parks.""" countries = Park.objects.filter( status='OPERATING', location__country__isnull=False ).values_list('location__country', flat=True).distinct().order_by('location__country') return list(countries) class CreateTripView(RoadTripViewMixin, View): """ Generate optimized road trip routes. URL: /roadtrip/create/ """ def post(self, request: HttpRequest) -> HttpResponse: """Create a new road trip with optimized routing.""" try: data = json.loads(request.body) # Parse park IDs park_ids = data.get('park_ids', []) if not park_ids or len(park_ids) < 2: return JsonResponse({ 'status': 'error', 'message': 'At least 2 parks are required for a road trip' }, status=400) if len(park_ids) > 10: return JsonResponse({ 'status': 'error', 'message': 'Maximum 10 parks allowed per trip' }, status=400) # Get parks parks = list(Park.objects.filter( id__in=park_ids, location__isnull=False ).select_related('location', 'operator')) if len(parks) != len(park_ids): return JsonResponse({ 'status': 'error', 'message': 'Some parks could not be found or do not have location data' }, status=400) # Create optimized trip trip = self.roadtrip_service.create_multi_park_trip(parks) if not trip: return JsonResponse({ 'status': 'error', 'message': 'Could not create optimized route for the selected parks' }, status=400) # Convert trip to dict for JSON response trip_data = { 'parks': [self._park_to_dict(park) for park in trip.parks], 'legs': [self._leg_to_dict(leg) for leg in trip.legs], 'total_distance_km': trip.total_distance_km, 'total_duration_minutes': trip.total_duration_minutes, 'formatted_total_distance': trip.formatted_total_distance, 'formatted_total_duration': trip.formatted_total_duration, } return JsonResponse({ 'status': 'success', 'data': trip_data, 'trip_url': reverse('parks:roadtrip_detail', kwargs={'trip_id': 'temp'}) }) except json.JSONDecodeError: return JsonResponse({ 'status': 'error', 'message': 'Invalid JSON data' }, status=400) except Exception as e: return JsonResponse({ 'status': 'error', 'message': f'Failed to create trip: {str(e)}' }, status=500) def _park_to_dict(self, park: Park) -> Dict[str, Any]: """Convert park instance to dictionary.""" return { 'id': park.id, 'name': park.name, 'slug': park.slug, 'formatted_location': getattr(park, 'formatted_location', ''), 'coordinates': park.coordinates, 'operator': park.operator.name if park.operator else None, 'ride_count': getattr(park, 'ride_count', 0), 'url': reverse('parks:park_detail', kwargs={'slug': park.slug}), } def _leg_to_dict(self, leg) -> Dict[str, Any]: """Convert trip leg to dictionary.""" return { 'from_park': self._park_to_dict(leg.from_park), 'to_park': self._park_to_dict(leg.to_park), 'distance_km': leg.route.distance_km, 'duration_minutes': leg.route.duration_minutes, 'formatted_distance': leg.route.formatted_distance, 'formatted_duration': leg.route.formatted_duration, 'geometry': leg.route.geometry, } class TripDetailView(RoadTripViewMixin, TemplateView): """ Show trip details and map. URL: /roadtrip// """ template_name = 'parks/trip_detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(self.get_roadtrip_context(self.request)) # For now, this is a placeholder since we don't persist trips # In a full implementation, you would retrieve the trip from database trip_id = kwargs.get('trip_id') context.update({ 'page_title': f'Road Trip #{trip_id}', 'trip_id': trip_id, 'message': 'Trip details would be loaded here. Currently trips are not persisted.', }) return context class FindParksAlongRouteView(RoadTripViewMixin, View): """ HTMX endpoint for route-based park discovery. URL: /roadtrip/htmx/parks-along-route/ """ def post(self, request: HttpRequest) -> HttpResponse: """Find parks along a route between two points.""" try: data = json.loads(request.body) start_park_id = data.get('start_park_id') end_park_id = data.get('end_park_id') max_detour_km = min(100, max(10, float(data.get('max_detour_km', 50)))) if not start_park_id or not end_park_id: return render(request, 'parks/partials/parks_along_route.html', { 'error': 'Start and end parks are required' }) # Get start and end parks try: start_park = Park.objects.select_related('location').get( id=start_park_id, location__isnull=False ) end_park = Park.objects.select_related('location').get( id=end_park_id, location__isnull=False ) except Park.DoesNotExist: return render(request, 'parks/partials/parks_along_route.html', { 'error': 'One or both parks could not be found' }) # Find parks along route parks_along_route = self.roadtrip_service.find_parks_along_route( start_park, end_park, max_detour_km ) return render(request, 'parks/partials/parks_along_route.html', { 'parks': parks_along_route, 'start_park': start_park, 'end_park': end_park, 'max_detour_km': max_detour_km, 'count': len(parks_along_route) }) except json.JSONDecodeError: return render(request, 'parks/partials/parks_along_route.html', { 'error': 'Invalid request data' }) except Exception as e: return render(request, 'parks/partials/parks_along_route.html', { 'error': str(e) }) class GeocodeAddressView(RoadTripViewMixin, View): """ HTMX endpoint for geocoding addresses. URL: /roadtrip/htmx/geocode/ """ def post(self, request: HttpRequest) -> HttpResponse: """Geocode an address and find nearby parks.""" try: data = json.loads(request.body) address = data.get('address', '').strip() if not address: return JsonResponse({ 'status': 'error', 'message': 'Address is required' }, status=400) # Geocode the address coordinates = self.roadtrip_service.geocode_address(address) if not coordinates: return JsonResponse({ 'status': 'error', 'message': 'Could not geocode the provided address' }, status=400) # Find nearby parks radius_km = min(200, max(10, float(data.get('radius_km', 100)))) # Use map service to find parks near coordinates from core.services.data_structures import GeoBounds # Create a bounding box around the coordinates lat_delta = radius_km / 111.0 # Rough conversion: 1 degree ≈ 111km lng_delta = radius_km / (111.0 * abs(coordinates.latitude / 90.0)) bounds = GeoBounds( north=coordinates.latitude + lat_delta, south=coordinates.latitude - lat_delta, east=coordinates.longitude + lng_delta, west=coordinates.longitude - lng_delta ) filters = MapFilters(location_types={LocationType.PARK}) map_response = unified_map_service.get_locations_by_bounds( north=bounds.north, south=bounds.south, east=bounds.east, west=bounds.west, location_types={LocationType.PARK} ) return JsonResponse({ 'status': 'success', 'data': { 'coordinates': { 'latitude': coordinates.latitude, 'longitude': coordinates.longitude }, 'address': address, 'nearby_parks': [loc.to_dict() for loc in map_response.locations[:20]], 'radius_km': radius_km } }) except json.JSONDecodeError: return JsonResponse({ 'status': 'error', 'message': 'Invalid JSON data' }, status=400) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=500) class ParkDistanceCalculatorView(RoadTripViewMixin, View): """ HTMX endpoint for calculating distances between parks. URL: /roadtrip/htmx/distance/ """ def post(self, request: HttpRequest) -> HttpResponse: """Calculate distance and duration between two parks.""" try: data = json.loads(request.body) park1_id = data.get('park1_id') park2_id = data.get('park2_id') if not park1_id or not park2_id: return JsonResponse({ 'status': 'error', 'message': 'Both park IDs are required' }, status=400) # Get parks try: park1 = Park.objects.select_related('location').get( id=park1_id, location__isnull=False ) park2 = Park.objects.select_related('location').get( id=park2_id, location__isnull=False ) except Park.DoesNotExist: return JsonResponse({ 'status': 'error', 'message': 'One or both parks could not be found' }, status=400) # Calculate route coords1 = park1.coordinates coords2 = park2.coordinates if not coords1 or not coords2: return JsonResponse({ 'status': 'error', 'message': 'One or both parks do not have coordinate data' }, status=400) from ..services.roadtrip import Coordinates route = self.roadtrip_service.calculate_route( Coordinates(*coords1), Coordinates(*coords2) ) if not route: return JsonResponse({ 'status': 'error', 'message': 'Could not calculate route between parks' }, status=400) return JsonResponse({ 'status': 'success', 'data': { 'distance_km': route.distance_km, 'duration_minutes': route.duration_minutes, 'formatted_distance': route.formatted_distance, 'formatted_duration': route.formatted_duration, 'park1': { 'name': park1.name, 'formatted_location': getattr(park1, 'formatted_location', '') }, 'park2': { 'name': park2.name, 'formatted_location': getattr(park2, 'formatted_location', '') } } }) except json.JSONDecodeError: return JsonResponse({ 'status': 'error', 'message': 'Invalid JSON data' }, status=400) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=500)