mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
major changes, including tailwind v4
This commit is contained in:
321
memory-bank/documentation/location_model_design.md
Normal file
321
memory-bank/documentation/location_model_design.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user