mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:51:09 -05:00
481 lines
16 KiB
Python
481 lines
16 KiB
Python
"""
|
|
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
|
|
from django.shortcuts import render
|
|
from django.http import JsonResponse, HttpRequest, HttpResponse
|
|
from django.views.generic import TemplateView, View
|
|
from django.urls import reverse
|
|
|
|
from .models import Park
|
|
from .services.roadtrip import RoadTripService
|
|
from apps.core.services.map_service import unified_map_service
|
|
from apps.core.services.data_structures import LocationType
|
|
|
|
JSON_DECODE_ERROR_MSG = "Invalid JSON data"
|
|
PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html"
|
|
|
|
|
|
class RoadTripViewMixin:
|
|
"""Mixin providing common functionality for road trip views."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.roadtrip_service = RoadTripService()
|
|
|
|
def get_roadtrip_context(self) -> 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": JSON_DECODE_ERROR_MSG},
|
|
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/<trip_id>/
|
|
"""
|
|
|
|
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_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_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_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_ALONG_ROUTE_HTML,
|
|
{"error": JSON_DECODE_ERROR_MSG},
|
|
)
|
|
except Exception as e:
|
|
return render(request, 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,
|
|
)
|
|
|
|
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": JSON_DECODE_ERROR_MSG},
|
|
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": JSON_DECODE_ERROR_MSG},
|
|
status=400,
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|