# Location Model Design Document ## ParkLocation Model ```python from django.contrib.gis.db import models as gis_models from django.db import models from parks.models import Park class ParkLocation(models.Model): park = models.OneToOneField( Park, on_delete=models.CASCADE, related_name='location' ) # Geographic coordinates point = gis_models.PointField( srid=4326, # WGS84 coordinate system null=True, blank=True, help_text="Geographic coordinates as a Point" ) # Address components street_address = models.CharField(max_length=255, blank=True, null=True) city = models.CharField(max_length=100, blank=True, null=True) state = models.CharField(max_length=100, blank=True, null=True, help_text="State/Region/Province") country = models.CharField(max_length=100, blank=True, null=True) postal_code = models.CharField(max_length=20, blank=True, null=True) # Road trip metadata highway_exit = models.CharField( max_length=100, blank=True, null=True, help_text="Nearest highway exit (e.g., 'Exit 42')" ) parking_notes = models.TextField( blank=True, null=True, help_text="Parking information and tips" ) # OSM integration osm_id = models.BigIntegerField( blank=True, null=True, help_text="OpenStreetMap ID for this location" ) osm_data = models.JSONField( blank=True, null=True, help_text="Raw OSM data snapshot" ) class Meta: indexes = [ models.Index(fields=['city']), models.Index(fields=['state']), models.Index(fields=['country']), models.Index(fields=['city', 'state']), ] # Spatial index will be created automatically by PostGIS def __str__(self): return f"{self.park.name} Location" @property def coordinates(self): """Returns coordinates as a tuple (latitude, longitude)""" if self.point: return (self.point.y, self.point.x) return None def get_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: components.append(self.country) return ", ".join(components) if components else "" ``` ## RideLocation Model ```python from django.contrib.gis.db import models as gis_models from django.db import models from parks.models import ParkArea from rides.models import Ride class RideLocation(models.Model): ride = models.OneToOneField( Ride, on_delete=models.CASCADE, related_name='location' ) # Optional coordinates point = gis_models.PointField( srid=4326, null=True, blank=True, help_text="Precise ride location within park" ) # Park area reference park_area = models.ForeignKey( ParkArea, on_delete=models.SET_NULL, null=True, blank=True, related_name='ride_locations' ) class Meta: indexes = [ models.Index(fields=['park_area']), ] def __str__(self): return f"{self.ride.name} Location" @property def coordinates(self): """Returns coordinates as a tuple (latitude, longitude) if available""" if self.point: return (self.point.y, self.point.x) return None ``` ## CompanyHeadquarters Model ```python from django.db import models from parks.models import Company class CompanyHeadquarters(models.Model): company = models.OneToOneField( Company, on_delete=models.CASCADE, related_name='headquarters' ) city = models.CharField(max_length=100) state = models.CharField(max_length=100, help_text="State/Region/Province") class Meta: verbose_name_plural = "Company headquarters" indexes = [ models.Index(fields=['city']), models.Index(fields=['state']), models.Index(fields=['city', 'state']), ] def __str__(self): return f"{self.company.name} Headquarters" ``` ## Shared Functionality Protocol ```python from typing import Protocol, Optional, Tuple class LocationProtocol(Protocol): def get_coordinates(self) -> Optional[Tuple[float, float]]: """Get coordinates as (latitude, longitude) tuple""" ... def get_location_name(self) -> str: """Get human-readable location name""" ... def distance_to(self, other: 'LocationProtocol') -> Optional[float]: """Calculate distance to another location in meters""" ... ``` ## Index Strategy 1. **ParkLocation**: - Spatial index on `point` (PostGIS GiST index) - Standard indexes on `city`, `state`, `country` - Composite index on (`city`, `state`) for common queries - Index on `highway_exit` for road trip searches 2. **RideLocation**: - Spatial index on `point` (PostGIS GiST index) - Index on `park_area` for area-based queries 3. **CompanyHeadquarters**: - Index on `city` - Index on `state` - Composite index on (`city`, `state`) ## OSM Integration Plan 1. **Data Collection**: - Store OSM ID in `ParkLocation.osm_id` - Cache raw OSM data in `ParkLocation.osm_data` 2. **Geocoding**: - Implement Nominatim geocoding service - Create management command to geocode existing parks - Add geocoding on ParkLocation save 3. **Road Trip Metadata**: - Map OSM highway data to `highway_exit` field - Extract parking information to `parking_notes` ## Migration Strategy ### Phase 1: Add New Models 1. Create new models (ParkLocation, RideLocation, CompanyHeadquarters) 2. Generate migrations 3. Deploy to production ### Phase 2: Data Migration 1. Migrate existing Location data: ```python for park in Park.objects.all(): if park.location.exists(): loc = park.location.first() ParkLocation.objects.create( park=park, point=loc.point, street_address=loc.street_address, city=loc.city, state=loc.state, country=loc.country, postal_code=loc.postal_code ) ``` 2. Migrate company headquarters: ```python for company in Company.objects.exclude(headquarters=''): city, state = parse_headquarters(company.headquarters) CompanyHeadquarters.objects.create( company=company, city=city, state=state ) ``` ### Phase 3: Update References 1. Update Park model to use ParkLocation 2. Update Ride model to use RideLocation 3. Update Company model to use CompanyHeadquarters 4. Remove old Location model ### Phase 4: OSM Integration 1. Implement geocoding command 2. Run geocoding for all ParkLocations 3. Extract road trip metadata from OSM data ## Relationship Diagram ```mermaid classDiagram Park "1" --> "1" ParkLocation Ride "1" --> "1" RideLocation Company "1" --> "1" CompanyHeadquarters RideLocation "1" --> "0..1" ParkArea class Park { +name: str } class ParkLocation { +point: Point +street_address: str +city: str +state: str +country: str +postal_code: str +highway_exit: str +parking_notes: str +osm_id: int +get_coordinates() +get_formatted_address() } class Ride { +name: str } class RideLocation { +point: Point +get_coordinates() } class Company { +name: str } class CompanyHeadquarters { +city: str +state: str } class ParkArea { +name: str } ``` ## Rollout Timeline 1. **Week 1**: Implement models and migrations 2. **Week 2**: Migrate data in staging environment 3. **Week 3**: Deploy to production, migrate data 4. **Week 4**: Implement OSM integration 5. **Week 5**: Optimize queries and indexes