mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
360 lines
12 KiB
Python
360 lines
12 KiB
Python
"""
|
|
Rides-specific location services with OpenStreetMap integration.
|
|
Handles location management for individual rides within parks.
|
|
"""
|
|
|
|
import requests
|
|
from typing import List, Dict, Any, Optional
|
|
from django.db import transaction
|
|
import logging
|
|
|
|
from ..models import RideLocation
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RideLocationService:
|
|
"""
|
|
Location service specifically for rides using OpenStreetMap integration.
|
|
Focuses on precise positioning within parks and navigation assistance.
|
|
"""
|
|
|
|
NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org"
|
|
USER_AGENT = "ThrillWiki/1.0 (https://thrillwiki.com)"
|
|
|
|
@classmethod
|
|
def create_ride_location(
|
|
cls,
|
|
*,
|
|
ride,
|
|
latitude: Optional[float] = None,
|
|
longitude: Optional[float] = None,
|
|
park_area: str = "",
|
|
notes: str = "",
|
|
entrance_notes: str = "",
|
|
accessibility_notes: str = "",
|
|
) -> RideLocation:
|
|
"""
|
|
Create a location for a ride within a park.
|
|
|
|
Args:
|
|
ride: Ride instance
|
|
latitude: Latitude coordinate (optional for rides)
|
|
longitude: Longitude coordinate (optional for rides)
|
|
park_area: Themed area within the park
|
|
notes: General location notes
|
|
entrance_notes: Entrance and navigation notes
|
|
accessibility_notes: Accessibility information
|
|
|
|
Returns:
|
|
Created RideLocation instance
|
|
"""
|
|
with transaction.atomic():
|
|
ride_location = RideLocation(
|
|
ride=ride,
|
|
park_area=park_area,
|
|
notes=notes,
|
|
entrance_notes=entrance_notes,
|
|
accessibility_notes=accessibility_notes,
|
|
)
|
|
|
|
# Set coordinates if provided
|
|
if latitude is not None and longitude is not None:
|
|
ride_location.set_coordinates(latitude, longitude)
|
|
|
|
ride_location.full_clean()
|
|
ride_location.save()
|
|
|
|
return ride_location
|
|
|
|
@classmethod
|
|
def update_ride_location(
|
|
cls, ride_location: RideLocation, **updates
|
|
) -> RideLocation:
|
|
"""
|
|
Update ride location with validation.
|
|
|
|
Args:
|
|
ride_location: RideLocation instance to update
|
|
**updates: Fields to update
|
|
|
|
Returns:
|
|
Updated RideLocation instance
|
|
"""
|
|
with transaction.atomic():
|
|
# Handle coordinates separately
|
|
latitude = updates.pop("latitude", None)
|
|
longitude = updates.pop("longitude", None)
|
|
|
|
# Update regular fields
|
|
for field, value in updates.items():
|
|
if hasattr(ride_location, field):
|
|
setattr(ride_location, field, value)
|
|
|
|
# Update coordinates if provided
|
|
if latitude is not None and longitude is not None:
|
|
ride_location.set_coordinates(latitude, longitude)
|
|
|
|
ride_location.full_clean()
|
|
ride_location.save()
|
|
|
|
return ride_location
|
|
|
|
@classmethod
|
|
def find_rides_in_area(cls, park, park_area: str) -> List[RideLocation]:
|
|
"""
|
|
Find all rides in a specific park area.
|
|
|
|
Args:
|
|
park: Park instance
|
|
park_area: Name of the park area/land
|
|
|
|
Returns:
|
|
List of RideLocation instances in the area
|
|
"""
|
|
return list(
|
|
RideLocation.objects.filter(ride__park=park, park_area__icontains=park_area)
|
|
.select_related("ride")
|
|
.order_by("ride__name")
|
|
)
|
|
|
|
@classmethod
|
|
def find_nearby_rides(
|
|
cls, latitude: float, longitude: float, park=None, radius_meters: float = 500
|
|
) -> List[RideLocation]:
|
|
"""
|
|
Find rides near given coordinates using PostGIS.
|
|
Useful for finding rides near a specific location within a park.
|
|
|
|
Args:
|
|
latitude: Center latitude
|
|
longitude: Center longitude
|
|
park: Optional park to limit search to
|
|
radius_meters: Search radius in meters (default: 500m)
|
|
|
|
Returns:
|
|
List of nearby RideLocation instances
|
|
"""
|
|
from django.contrib.gis.geos import Point
|
|
from django.contrib.gis.measure import Distance
|
|
|
|
center_point = Point(longitude, latitude, srid=4326)
|
|
|
|
queryset = RideLocation.objects.filter(
|
|
point__distance_lte=(center_point, Distance(m=radius_meters)),
|
|
point__isnull=False,
|
|
)
|
|
|
|
if park:
|
|
queryset = queryset.filter(ride__park=park)
|
|
|
|
return list(
|
|
queryset.select_related("ride", "ride__park").order_by("point__distance")
|
|
)
|
|
|
|
@classmethod
|
|
def get_ride_navigation_info(cls, ride_location: RideLocation) -> Dict[str, Any]:
|
|
"""
|
|
Get comprehensive navigation information for a ride.
|
|
|
|
Args:
|
|
ride_location: RideLocation instance
|
|
|
|
Returns:
|
|
Dictionary with navigation information
|
|
"""
|
|
info = {
|
|
"ride_name": ride_location.ride.name,
|
|
"park_name": ride_location.ride.park.name,
|
|
"park_area": ride_location.park_area,
|
|
"has_coordinates": ride_location.has_coordinates,
|
|
"entrance_notes": ride_location.entrance_notes,
|
|
"accessibility_notes": ride_location.accessibility_notes,
|
|
"general_notes": ride_location.notes,
|
|
}
|
|
|
|
# Add coordinate information if available
|
|
if ride_location.has_coordinates:
|
|
info.update(
|
|
{
|
|
"latitude": ride_location.latitude,
|
|
"longitude": ride_location.longitude,
|
|
"coordinates": ride_location.coordinates,
|
|
}
|
|
)
|
|
|
|
# Calculate distance to park entrance if park has location
|
|
park_location = getattr(ride_location.ride.park, "location", None)
|
|
if park_location and park_location.point:
|
|
distance_km = ride_location.distance_to_park_location()
|
|
if distance_km is not None:
|
|
info["distance_from_park_entrance_km"] = round(distance_km, 2)
|
|
|
|
return info
|
|
|
|
@classmethod
|
|
def estimate_ride_coordinates_from_park(
|
|
cls,
|
|
ride_location: RideLocation,
|
|
area_offset_meters: Optional[Dict[str, List[float]]] = None,
|
|
) -> Optional[List[float]]:
|
|
"""
|
|
Estimate ride coordinates based on park location and area.
|
|
Useful when exact ride coordinates are not available.
|
|
|
|
Args:
|
|
ride_location: RideLocation instance
|
|
area_offset_meters: Dictionary mapping area names to [north_offset, east_offset] in meters
|
|
|
|
Returns:
|
|
Estimated [latitude, longitude] list or None
|
|
"""
|
|
park_location = getattr(ride_location.ride.park, "location", None)
|
|
if not park_location or not park_location.point:
|
|
return None
|
|
|
|
# Default area offsets (rough estimates for common themed areas)
|
|
default_offsets = {
|
|
"main street": (0, 0), # Usually at entrance
|
|
"fantasyland": (200, 100), # Often north-east
|
|
"tomorrowland": (100, 200), # Often east
|
|
"frontierland": (-100, -200), # Often south-west
|
|
"adventureland": (-200, 100), # Often south-east
|
|
"new orleans square": (-150, -100),
|
|
"critter country": (-200, -200),
|
|
"galaxy's edge": (300, 300), # Often on periphery
|
|
"cars land": (200, -200),
|
|
"pixar pier": (0, 300), # Often waterfront
|
|
}
|
|
|
|
offsets = area_offset_meters or default_offsets
|
|
|
|
# Find matching area offset
|
|
area_lower = ride_location.park_area.lower()
|
|
offset = None
|
|
|
|
for area_name, area_offset in offsets.items():
|
|
if area_name in area_lower:
|
|
offset = area_offset
|
|
break
|
|
|
|
if not offset:
|
|
# Default small random offset if no specific area match
|
|
import random
|
|
|
|
offset = (random.randint(-100, 100), random.randint(-100, 100))
|
|
|
|
# Convert meter offsets to coordinate offsets
|
|
# Rough conversion: 1 degree latitude ≈ 111,000 meters
|
|
# 1 degree longitude varies by latitude, but we'll use a rough approximation
|
|
lat_offset = offset[0] / 111000 # North offset in degrees
|
|
lon_offset = offset[1] / (
|
|
111000 * abs(park_location.latitude) * 0.01
|
|
) # East offset
|
|
|
|
estimated_lat = park_location.latitude + lat_offset
|
|
estimated_lon = park_location.longitude + lon_offset
|
|
|
|
return [estimated_lat, estimated_lon]
|
|
|
|
@classmethod
|
|
def bulk_update_ride_areas_from_osm(cls, park) -> int:
|
|
"""
|
|
Bulk update ride locations for a park using OSM data.
|
|
Attempts to find more precise locations for rides within the park.
|
|
|
|
Args:
|
|
park: Park instance
|
|
|
|
Returns:
|
|
Number of ride locations updated
|
|
"""
|
|
updated_count = 0
|
|
park_location = getattr(park, "location", None)
|
|
|
|
if not park_location or not park_location.point:
|
|
return updated_count
|
|
|
|
# Get all rides in the park that don't have precise coordinates
|
|
ride_locations = RideLocation.objects.filter(
|
|
ride__park=park, point__isnull=True
|
|
).select_related("ride")
|
|
|
|
for ride_location in ride_locations:
|
|
# Try to search for the specific ride within the park area
|
|
search_query = f"{ride_location.ride.name} {park.name}"
|
|
|
|
try:
|
|
# Search for the ride specifically
|
|
params = {
|
|
"q": search_query,
|
|
"format": "json",
|
|
"limit": 5,
|
|
"addressdetails": 1,
|
|
"bounded": 1, # Restrict to viewbox
|
|
# Create a bounding box around the park (roughly 2km radius)
|
|
"viewbox": f"{park_location.longitude - 0.02},{park_location.latitude + 0.02},{park_location.longitude + 0.02},{park_location.latitude - 0.02}",
|
|
}
|
|
|
|
headers = {"User-Agent": cls.USER_AGENT}
|
|
|
|
response = requests.get(
|
|
f"{cls.NOMINATIM_BASE_URL}/search",
|
|
params=params,
|
|
headers=headers,
|
|
timeout=5,
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
results = response.json()
|
|
|
|
# Look for results that might be the ride
|
|
for result in results:
|
|
display_name = result.get("display_name", "").lower()
|
|
if (
|
|
ride_location.ride.name.lower() in display_name
|
|
and park.name.lower() in display_name
|
|
):
|
|
# Update the ride location
|
|
ride_location.set_coordinates(
|
|
float(result["lat"]), float(result["lon"])
|
|
)
|
|
ride_location.save()
|
|
updated_count += 1
|
|
break
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Error updating ride location for {ride_location.ride.name}: {str(e)}"
|
|
)
|
|
continue
|
|
|
|
return updated_count
|
|
|
|
@classmethod
|
|
def generate_park_area_map(cls, park) -> Dict[str, List[str]]:
|
|
"""
|
|
Generate a map of park areas and the rides in each area.
|
|
|
|
Args:
|
|
park: Park instance
|
|
|
|
Returns:
|
|
Dictionary mapping area names to lists of ride names
|
|
"""
|
|
area_map = {}
|
|
|
|
ride_locations = (
|
|
RideLocation.objects.filter(ride__park=park)
|
|
.select_related("ride")
|
|
.order_by("park_area", "ride__name")
|
|
)
|
|
|
|
for ride_location in ride_locations:
|
|
area = ride_location.park_area or "Unknown Area"
|
|
if area not in area_map:
|
|
area_map[area] = []
|
|
area_map[area].append(ride_location.ride.name)
|
|
|
|
return area_map
|