# Domain-Specific Location Models Design - ThrillWiki ## Executive Summary This design document outlines the complete transition from ThrillWiki's generic location system to domain-specific location models. The design builds upon existing partial implementations (ParkLocation, RideLocation, CompanyHeadquarters) and addresses the requirements for road trip planning, spatial queries, and clean domain boundaries. ## 1. Model Specifications ### 1.1 ParkLocation Model #### Purpose Primary location model for theme parks, optimized for road trip planning and visitor navigation. #### Field Specifications ```python class ParkLocation(models.Model): # Relationships park = models.OneToOneField( 'parks.Park', on_delete=models.CASCADE, related_name='park_location' # Changed from 'location' to avoid conflicts ) # Spatial Data (PostGIS) point = gis_models.PointField( srid=4326, # WGS84 coordinate system db_index=True, help_text="Geographic coordinates for mapping and distance calculations" ) # Core Address Fields street_address = models.CharField( max_length=255, blank=True, help_text="Street number and name for the main entrance" ) city = models.CharField( max_length=100, db_index=True, help_text="City where the park is located" ) state = models.CharField( max_length=100, db_index=True, help_text="State/Province/Region" ) country = models.CharField( max_length=100, default='USA', db_index=True, help_text="Country code or full name" ) postal_code = models.CharField( max_length=20, blank=True, help_text="ZIP or postal code" ) # Road Trip Metadata highway_exit = models.CharField( max_length=100, blank=True, help_text="Nearest highway exit information (e.g., 'I-75 Exit 234')" ) parking_notes = models.TextField( blank=True, help_text="Parking tips, costs, and preferred lots" ) best_arrival_time = models.TimeField( null=True, blank=True, help_text="Recommended arrival time to minimize crowds" ) seasonal_notes = models.TextField( blank=True, help_text="Seasonal considerations for visiting (weather, crowds, events)" ) # Navigation Helpers main_entrance_notes = models.TextField( blank=True, help_text="Specific directions to main entrance from parking" ) gps_accuracy_notes = models.CharField( max_length=255, blank=True, help_text="Notes about GPS accuracy or common navigation issues" ) # OpenStreetMap Integration osm_id = models.BigIntegerField( null=True, blank=True, db_index=True, help_text="OpenStreetMap ID for data synchronization" ) osm_last_sync = models.DateTimeField( null=True, blank=True, help_text="Last time data was synchronized with OSM" ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) verified_date = models.DateField( null=True, blank=True, help_text="Date location was last verified as accurate" ) verified_by = models.ForeignKey( 'accounts.User', null=True, blank=True, on_delete=models.SET_NULL, related_name='verified_park_locations' ) ``` #### Properties and Methods ```python @property def latitude(self): """Returns latitude for backward compatibility""" return self.point.y if self.point else None @property def longitude(self): """Returns longitude for backward compatibility""" return self.point.x if self.point else None @property def formatted_address(self): """Returns a formatted address string""" components = [] if self.street_address: components.append(self.street_address) if self.city: components.append(self.city) if self.state: components.append(self.state) if self.postal_code: components.append(self.postal_code) if self.country and self.country != 'USA': components.append(self.country) return ", ".join(components) @property def short_address(self): """Returns city, state for compact display""" parts = [] if self.city: parts.append(self.city) if self.state: parts.append(self.state) return ", ".join(parts) if parts else "Location Unknown" def distance_to(self, other_location): """Calculate distance to another ParkLocation in miles""" if not self.point or not hasattr(other_location, 'point') or not other_location.point: return None # Use PostGIS distance calculation and convert to miles from django.contrib.gis.measure import D return self.point.distance(other_location.point) * 69.0 # Rough conversion def nearby_parks(self, distance_miles=50): """Find other parks within specified distance""" if not self.point: return ParkLocation.objects.none() from django.contrib.gis.measure import D return ParkLocation.objects.filter( point__distance_lte=(self.point, D(mi=distance_miles)) ).exclude(pk=self.pk).select_related('park') def get_directions_url(self): """Generate Google Maps directions URL""" if self.point: return f"https://www.google.com/maps/dir/?api=1&destination={self.latitude},{self.longitude}" return None ``` #### Meta Options ```python class Meta: verbose_name = "Park Location" verbose_name_plural = "Park Locations" indexes = [ models.Index(fields=['city', 'state']), models.Index(fields=['country']), models.Index(fields=['osm_id']), GistIndex(fields=['point']), # Spatial index for PostGIS ] constraints = [ models.UniqueConstraint( fields=['park'], name='unique_park_location' ) ] ``` ### 1.2 RideLocation Model #### Purpose Optional lightweight location tracking for individual rides within parks. #### Field Specifications ```python class RideLocation(models.Model): # Relationships ride = models.OneToOneField( 'rides.Ride', on_delete=models.CASCADE, related_name='ride_location' ) # Optional Spatial Data entrance_point = gis_models.PointField( srid=4326, null=True, blank=True, help_text="Specific coordinates for ride entrance" ) exit_point = gis_models.PointField( srid=4326, null=True, blank=True, help_text="Specific coordinates for ride exit (if different)" ) # Park Area Information park_area = models.CharField( max_length=100, blank=True, db_index=True, help_text="Themed area or land within the park" ) level = models.CharField( max_length=50, blank=True, help_text="Floor or level if in multi-story area" ) # Accessibility accessible_entrance_point = gis_models.PointField( srid=4326, null=True, blank=True, help_text="Coordinates for accessible entrance if different" ) accessible_entrance_notes = models.TextField( blank=True, help_text="Directions to accessible entrance" ) # Queue and Navigation queue_entrance_notes = models.TextField( blank=True, help_text="How to find the queue entrance" ) fastpass_entrance_notes = models.TextField( blank=True, help_text="Location of FastPass/Express entrance" ) single_rider_entrance_notes = models.TextField( blank=True, help_text="Location of single rider entrance if available" ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) ``` #### Properties and Methods ```python @property def has_coordinates(self): """Check if any coordinates are set""" return bool(self.entrance_point or self.exit_point or self.accessible_entrance_point) @property def primary_point(self): """Returns the primary location point (entrance preferred)""" return self.entrance_point or self.exit_point or self.accessible_entrance_point def get_park_location(self): """Get the parent park's location""" return self.ride.park.park_location if hasattr(self.ride.park, 'park_location') else None ``` #### Meta Options ```python class Meta: verbose_name = "Ride Location" verbose_name_plural = "Ride Locations" indexes = [ models.Index(fields=['park_area']), GistIndex(fields=['entrance_point'], condition=Q(entrance_point__isnull=False)), ] ``` ### 1.3 CompanyHeadquarters Model #### Purpose Simple address storage for company headquarters without coordinate tracking. #### Field Specifications ```python class CompanyHeadquarters(models.Model): # Relationships company = models.OneToOneField( 'parks.Company', on_delete=models.CASCADE, related_name='headquarters' ) # Address Fields (No coordinates needed) street_address = models.CharField( max_length=255, blank=True, help_text="Mailing address if publicly available" ) city = models.CharField( max_length=100, db_index=True, help_text="Headquarters city" ) state = models.CharField( max_length=100, blank=True, db_index=True, help_text="State/Province/Region" ) country = models.CharField( max_length=100, default='USA', db_index=True ) postal_code = models.CharField( max_length=20, blank=True ) # Contact Information (Optional) phone = models.CharField( max_length=30, blank=True, help_text="Corporate phone number" ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) ``` #### Properties and Methods ```python @property def formatted_address(self): """Returns a formatted address string""" components = [] if self.street_address: components.append(self.street_address) if self.city: components.append(self.city) if self.state: components.append(self.state) if self.postal_code: components.append(self.postal_code) if self.country and self.country != 'USA': components.append(self.country) return ", ".join(components) if components else f"{self.city}, {self.country}" @property def location_display(self): """Simple city, country display""" parts = [self.city] if self.state: parts.append(self.state) if self.country != 'USA': parts.append(self.country) return ", ".join(parts) ``` #### Meta Options ```python class Meta: verbose_name = "Company Headquarters" verbose_name_plural = "Company Headquarters" indexes = [ models.Index(fields=['city', 'country']), ] ``` ## 2. Shared Functionality Design ### 2.1 Address Formatting Utilities Create a utility module `location/utils.py`: ```python class AddressFormatter: """Utility class for consistent address formatting across models""" @staticmethod def format_full(street=None, city=None, state=None, postal=None, country=None): """Format a complete address""" components = [] if street: components.append(street) if city: components.append(city) if state: components.append(state) if postal: components.append(postal) if country and country != 'USA': components.append(country) return ", ".join(components) @staticmethod def format_short(city=None, state=None, country=None): """Format a short location display""" parts = [] if city: parts.append(city) if state: parts.append(state) elif country and country != 'USA': parts.append(country) return ", ".join(parts) if parts else "Unknown Location" ``` ### 2.2 Geocoding Service Create `location/services.py`: ```python class GeocodingService: """Service for geocoding addresses using OpenStreetMap Nominatim""" @staticmethod def geocode_address(street, city, state, country='USA'): """Convert address to coordinates""" # Implementation using Nominatim API pass @staticmethod def reverse_geocode(latitude, longitude): """Convert coordinates to address""" # Implementation using Nominatim API pass @staticmethod def validate_coordinates(latitude, longitude): """Validate coordinate ranges""" return (-90 <= latitude <= 90) and (-180 <= longitude <= 180) ``` ### 2.3 Distance Calculation Mixin ```python class DistanceCalculationMixin: """Mixin for models with point fields to calculate distances""" def distance_to_point(self, point): """Calculate distance to a point in miles""" if not self.point or not point: return None # Use PostGIS for calculation return self.point.distance(point) * 69.0 # Rough miles conversion def within_radius(self, radius_miles): """Get queryset of objects within radius""" if not self.point: return self.__class__.objects.none() from django.contrib.gis.measure import D return self.__class__.objects.filter( point__distance_lte=(self.point, D(mi=radius_miles)) ).exclude(pk=self.pk) ``` ## 3. Data Flow Design ### 3.1 Location Data Entry Flow ```mermaid graph TD A[User Creates/Edits Park] --> B[Park Form] B --> C{Has Address?} C -->|Yes| D[Geocoding Service] C -->|No| E[Manual Coordinate Entry] D --> F[Validate Coordinates] E --> F F --> G[Create/Update ParkLocation] G --> H[Update OSM Fields] H --> I[Save to Database] ``` ### 3.2 Location Search Flow ```mermaid graph TD A[User Searches Location] --> B[Search View] B --> C[Check Cache] C -->|Hit| D[Return Cached Results] C -->|Miss| E[Query OSM Nominatim] E --> F[Process Results] F --> G[Filter by Park Existence] G --> H[Cache Results] H --> D ``` ### 3.3 Road Trip Planning Flow ```mermaid graph TD A[User Plans Road Trip] --> B[Select Starting Point] B --> C[Query Nearby Parks] C --> D[Calculate Distances] D --> E[Sort by Distance/Route] E --> F[Display with Highway Exits] F --> G[Show Parking/Arrival Info] ``` ## 4. Query Patterns ### 4.1 Common Spatial Queries ```python # Find parks within radius ParkLocation.objects.filter( point__distance_lte=(origin_point, D(mi=50)) ).select_related('park') # Find nearest park ParkLocation.objects.annotate( distance=Distance('point', origin_point) ).order_by('distance').first() # Parks along a route (bounding box) from django.contrib.gis.geos import Polygon bbox = Polygon.from_bbox((min_lng, min_lat, max_lng, max_lat)) ParkLocation.objects.filter(point__within=bbox) # Group parks by state ParkLocation.objects.values('state').annotate( count=Count('id'), parks=ArrayAgg('park__name') ) ``` ### 4.2 Performance Optimizations ```python # Prefetch related data for park listings Park.objects.select_related( 'park_location', 'operator', 'property_owner' ).prefetch_related('rides') # Use database functions for formatting from django.db.models import Value, F from django.db.models.functions import Concat ParkLocation.objects.annotate( display_address=Concat( F('city'), Value(', '), F('state') ) ) ``` ### 4.3 Caching Strategy ```python # Cache frequently accessed location data CACHE_KEYS = { 'park_location': 'park_location_{park_id}', 'nearby_parks': 'nearby_parks_{park_id}_{radius}', 'state_parks': 'state_parks_{state}', } # Cache timeout in seconds CACHE_TIMEOUTS = { 'park_location': 3600, # 1 hour 'nearby_parks': 1800, # 30 minutes 'state_parks': 7200, # 2 hours } ``` ## 5. Integration Points ### 5.1 Model Integration ```python # Park model integration class Park(models.Model): # Remove GenericRelation to Location # location = GenericRelation(Location) # REMOVE THIS @property def location(self): """Backward compatibility property""" return self.park_location if hasattr(self, 'park_location') else None @property def coordinates(self): """Quick access to coordinates""" if hasattr(self, 'park_location') and self.park_location: return (self.park_location.latitude, self.park_location.longitude) return None ``` ### 5.2 Form Integration ```python # Park forms will need location inline class ParkLocationForm(forms.ModelForm): class Meta: model = ParkLocation fields = [ 'street_address', 'city', 'state', 'country', 'postal_code', 'highway_exit', 'parking_notes', 'best_arrival_time', 'seasonal_notes', 'point' ] widgets = { 'point': LeafletWidget(), # Map widget for coordinate selection } class ParkForm(forms.ModelForm): # Include location fields as nested form location = ParkLocationForm() ``` ### 5.3 API Serialization ```python # Django REST Framework serializers class ParkLocationSerializer(serializers.ModelSerializer): latitude = serializers.ReadOnlyField() longitude = serializers.ReadOnlyField() formatted_address = serializers.ReadOnlyField() class Meta: model = ParkLocation fields = [ 'latitude', 'longitude', 'formatted_address', 'city', 'state', 'country', 'highway_exit', 'parking_notes', 'best_arrival_time' ] class ParkSerializer(serializers.ModelSerializer): location = ParkLocationSerializer(source='park_location', read_only=True) ``` ### 5.4 Template Integration ```django {# Park detail template #} {% if park.park_location %}
{{ park.park_location.formatted_address }}
{% if park.park_location.highway_exit %}Highway Exit: {{ park.park_location.highway_exit }}
{% endif %} {% if park.park_location.parking_notes %}Parking: {{ park.park_location.parking_notes }}
{% endif %}