mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:51:08 -05:00
- Implemented a new HTML template for the Road Trip Planner. - Integrated Leaflet.js for interactive mapping and routing. - Added functionality for searching and selecting parks to include in a trip. - Enabled drag-and-drop reordering of selected parks. - Included trip optimization and route calculation features. - Created a summary display for trip statistics. - Added functionality to save trips and manage saved trips. - Enhanced UI with responsive design and dark mode support.
400 lines
14 KiB
Python
400 lines
14 KiB
Python
"""
|
|
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/<type>/<id>/
|
|
"""
|
|
|
|
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 |