Files
thrillwiki_django_no_react/backend/apps/parks/views_roadtrip.py
pacnpal d504d41de2 feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
2025-08-23 18:40:07 -04:00

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, MapFilters
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)