diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..cedcb33 --- /dev/null +++ b/.clinerules @@ -0,0 +1,105 @@ +# Project Rules + +## Feature Parity Requirements + +IMPORTANT: This Laravel/Livewire project must maintain strict feature parity with the original Django project at '/Users/talor/thrillwiki_django_no_react'. + +### Core Requirements + +1. Feature-to-Feature Matching + - Every Django app must have an equivalent Laravel implementation + - All functionality must be replicated without loss of capability + - New features must not break existing feature parity + - Document any deviations or Laravel-specific enhancements + +2. Function-to-Function Equivalence + - Methods and functions must maintain the same business logic + - API endpoints must provide identical responses + - Database operations must maintain data integrity + - Performance characteristics should be equivalent or better + +3. Design Consistency + - UI/UX must match the original Django implementation + - Component structure should mirror Django templates + - Maintain consistent naming conventions + - Preserve user interaction patterns + +4. Project Structure Reference + - Original Django apps to maintain parity with: + * accounts + * analytics + * autocomplete + * companies + * core + * designers + * email_service + * history + * history_tracking + * location + * moderation + * parks + * reviews + * rides + * search + * wiki + +### Implementation Guidelines + +1. Framework Usage + - Use Laravel and Livewire native implementations whenever possible + - Avoid custom code unless absolutely necessary + - Leverage built-in Laravel features and conventions + - Utilize Livewire's reactive components and lifecycle hooks + - Only create custom solutions when framework features don't meet requirements + +2. Before implementing new features: + - Review corresponding Django implementation + - Document feature requirements + - Plan Laravel/Livewire approach + - Verify feature parity coverage + +2. During development: + - Test against Django behavior + - Maintain identical data structures + - Preserve business logic flow + - Document any technical differences + +3. After implementation: + - Verify feature completeness + - Test edge cases match Django + - Update documentation + - Mark feature as parity-verified + +### Quality Assurance + +1. Testing Requirements + - Test cases must cover Django functionality + - Behavior must match Django implementation + - Performance metrics must be comparable + - Document any differences in approach + +2. Documentation Requirements + - Reference original Django features + - Explain implementation differences + - Document Laravel-specific enhancements + - Maintain feature parity tracking + +3. Review Process + - Compare against Django source + - Verify feature completeness + - Test user workflows + - Document verification results + +### Exceptions + +1. Allowed Deviations + - Performance improvements + - Security enhancements + - Framework-specific optimizations + - Modern browser capabilities + +2. Required Documentation + - Justify any deviations + - Document improvements + - Explain technical decisions + - Track affected features \ No newline at end of file diff --git a/app/Models/Location.php b/app/Models/Location.php index e3de4d8..7c97a62 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -4,22 +4,28 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Support\Facades\DB; class Location extends Model { + use \Spatie\Activitylog\LogsActivity; + /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ + 'name', + 'location_type', 'address', 'city', 'state', 'country', 'postal_code', - 'latitude', - 'longitude', + 'coordinates', + 'latitude', // Legacy field + 'longitude', // Legacy field 'elevation', 'timezone', 'metadata', @@ -35,8 +41,9 @@ class Location extends Model * @var array */ protected $casts = [ - 'latitude' => 'decimal:8', - 'longitude' => 'decimal:8', + 'coordinates' => 'point', + 'latitude' => 'decimal:6', + 'longitude' => 'decimal:6', 'elevation' => 'decimal:2', 'metadata' => 'array', 'geocoding_data' => 'array', @@ -44,6 +51,97 @@ class Location extends Model 'is_approximate' => 'boolean', ]; + /** + * The attributes that should be logged. + * + * @var array + */ + protected static $logAttributes = [ + 'name', + 'location_type', + 'address', + 'city', + 'state', + 'country', + 'postal_code', + 'coordinates', + 'latitude', + 'longitude', + ]; + + /** + * Boot the model. + */ + protected static function boot() + { + parent::boot(); + + static::saving(function ($model) { + // Sync point field with lat/lon fields for backward compatibility + if ($model->latitude && $model->longitude && !$model->coordinates) { + $model->coordinates = DB::raw( + "ST_SetSRID(ST_MakePoint($model->longitude, $model->latitude), 4326)" + ); + } elseif ($model->coordinates && (!$model->latitude || !$model->longitude)) { + $point = json_decode($model->coordinates); + $model->longitude = $point->lng; + $model->latitude = $point->lat; + } + }); + } + + /** + * Get the location's string representation. + */ + public function __toString(): string + { + $locationParts = []; + if ($this->city) { + $locationParts[] = $this->city; + } + if ($this->country) { + $locationParts[] = $this->country; + } + $locationStr = $locationParts ? implode(', ', $locationParts) : 'Unknown location'; + + return "{$this->name} ({$locationStr})"; + } + + /** + * Get the formatted address. + */ + public function getFormattedAddressAttribute(): string + { + $components = array_filter([ + $this->address, + $this->city, + $this->state, + $this->postal_code, + $this->country + ]); + + return implode(', ', $components); + } + + /** + * Normalize a coordinate value. + */ + protected function normalizeCoordinate($value, int $decimalPlaces = 6): ?float + { + if ($value === null) { + return null; + } + + try { + // Convert to string first to handle both float and string inputs + $value = (string) $value; + // Use BC Math for precise decimal handling + return round((float) $value, $decimalPlaces); + } catch (\Exception $e) { + return null; + } + } + /** * Get the parent locatable model. */ @@ -52,51 +150,6 @@ class Location extends Model return $this->morphTo(); } - /** - * Get the location's coordinates as an array. - * - * @return array - */ - public function getCoordinatesAttribute(): array - { - return [ - 'lat' => $this->latitude, - 'lng' => $this->longitude, - ]; - } - - /** - * Get the formatted address. - * - * @return string - */ - public function getFormattedAddressAttribute(): string - { - $parts = []; - - if ($this->address) { - $parts[] = $this->address; - } - - if ($this->city) { - $parts[] = $this->city; - } - - if ($this->state) { - $parts[] = $this->state; - } - - if ($this->postal_code) { - $parts[] = $this->postal_code; - } - - if ($this->country) { - $parts[] = $this->country; - } - - return implode(', ', $parts); - } - /** * Get Google Maps URL for the location. * @@ -126,8 +179,7 @@ class Location extends Model public function updateCoordinates(float $latitude, float $longitude, ?float $elevation = null): bool { return $this->update([ - 'latitude' => $latitude, - 'longitude' => $longitude, + 'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"), 'elevation' => $elevation, ]); } @@ -161,17 +213,12 @@ class Location extends Model */ public function scopeNearby($query, float $latitude, float $longitude, float $radius, string $unit = 'km') { - $earthRadius = $unit === 'mi' ? 3959 : 6371; + $point = DB::raw("ST_MakePoint($longitude, $latitude)"); + $distance = $unit === 'mi' ? $radius * 1609.34 : $radius * 1000; return $query->whereRaw( - "($earthRadius * acos( - cos(radians(?)) * - cos(radians(latitude)) * - cos(radians(longitude) - radians(?)) + - sin(radians(?)) * - sin(radians(latitude)) - )) <= ?", - [$latitude, $longitude, $latitude, $radius] + "ST_DWithin(coordinates::geography, ?::geography, ?)", + [$point, $distance] ); } @@ -185,8 +232,15 @@ class Location extends Model */ public function scopeInBounds($query, array $ne, array $sw) { - return $query->whereBetween('latitude', [$sw['lat'], $ne['lat']]) - ->whereBetween('longitude', [$sw['lng'], $ne['lng']]); + $bounds = DB::raw( + "ST_MakeEnvelope( + {$sw['lng']}, {$sw['lat']}, + {$ne['lng']}, {$ne['lat']}, + 4326 + )" + ); + + return $query->whereRaw("ST_Within(coordinates::geometry, ?::geometry)", [$bounds]); } /** @@ -199,23 +253,23 @@ class Location extends Model */ public function distanceTo(float $latitude, float $longitude, string $unit = 'km'): ?float { - if (!$this->latitude || !$this->longitude) { + if (!$this->coordinates) { return null; } - $earthRadius = $unit === 'mi' ? 3959 : 6371; + $point = DB::raw("ST_MakePoint($longitude, $latitude)"); + $distance = DB::selectOne( + "SELECT ST_Distance( + ST_GeomFromText(?), + ST_GeomFromText(?), + true + ) as distance", + [ + "POINT({$this->longitude} {$this->latitude})", + "POINT($longitude $latitude)" + ] + )->distance; - $latFrom = deg2rad($this->latitude); - $lonFrom = deg2rad($this->longitude); - $latTo = deg2rad($latitude); - $lonTo = deg2rad($longitude); - - $latDelta = $latTo - $latFrom; - $lonDelta = $lonTo - $lonFrom; - - $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) + - cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2))); - - return $angle * $earthRadius; + return $unit === 'mi' ? $distance * 0.000621371 : $distance / 1000; } } \ No newline at end of file diff --git a/app/Traits/HasLocation.php b/app/Traits/HasLocation.php new file mode 100644 index 0000000..cd832b4 --- /dev/null +++ b/app/Traits/HasLocation.php @@ -0,0 +1,178 @@ +location?->delete(); + }); + } + + /** + * Get the model's location. + */ + public function location(): MorphOne + { + return $this->morphOne(Location::class, 'locatable'); + } + + /** + * Get the model's coordinates. + * + * @return array + */ + public function getCoordinatesAttribute(): array + { + if (!$this->location?->coordinates) { + return [ + 'lat' => null, + 'lng' => null, + ]; + } + + return [ + 'lat' => $this->location->coordinates->getLat(), + 'lng' => $this->location->coordinates->getLng(), + ]; + } + + /** + * Get the model's formatted address. + * + * @return string|null + */ + public function getFormattedAddressAttribute(): ?string + { + return $this->location?->formatted_address; + } + + /** + * Get the model's map URL. + * + * @return string|null + */ + public function getMapUrlAttribute(): ?string + { + return $this->location?->map_url; + } + + /** + * Update or create the model's location. + * + * @param array $attributes + * @return \App\Models\Location + */ + public function updateLocation(array $attributes): Location + { + if ($this->location) { + $this->location->update($attributes); + return $this->location; + } + + return $this->location()->create($attributes); + } + + /** + * Set the model's coordinates. + * + * @param float $latitude + * @param float $longitude + * @param float|null $elevation + * @return \App\Models\Location + */ + public function setCoordinates(float $latitude, float $longitude, ?float $elevation = null): Location + { + return $this->updateLocation([ + 'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"), + 'elevation' => $elevation, + ]); + } + + /** + * Set the model's address. + * + * @param array $components + * @return \App\Models\Location + */ + public function setAddress(array $components): Location + { + return $this->updateLocation($components); + } + + /** + * Calculate distance to another model with location. + * + * @param mixed $model Model using HasLocation trait + * @param string $unit 'km' or 'mi' + * @return float|null + */ + public function distanceTo($model, string $unit = 'km'): ?float + { + if (!$this->location || !$model->location) { + return null; + } + + return $this->location->distanceTo( + $model->location->latitude, + $model->location->longitude, + $unit + ); + } + + /** + * Scope a query to find models near coordinates. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param float $latitude + * @param float $longitude + * @param float $radius + * @param string $unit + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeNearby($query, float $latitude, float $longitude, float $radius, string $unit = 'km') + { + $point = DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"); + $distance = $unit === 'mi' ? $radius * 1609.34 : $radius * 1000; + + return $query->whereHas('location', function ($query) use ($point, $distance) { + $query->whereRaw( + "ST_DWithin(coordinates::geography, ?::geography, ?)", + [$point, $distance] + ); + }); + } + + /** + * Scope a query to find models within bounds. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $ne Northeast corner [lat, lng] + * @param array $sw Southwest corner [lat, lng] + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeInBounds($query, array $ne, array $sw) + { + $bounds = DB::raw( + "ST_MakeEnvelope( + {$sw['lng']}, {$sw['lat']}, + {$ne['lng']}, {$ne['lat']}, + 4326 + )" + ); + + return $query->whereHas('location', function ($query) use ($bounds) { + $query->whereRaw("ST_Within(coordinates::geometry, ?::geometry)", [$bounds]); + }); + } +} \ No newline at end of file diff --git a/database/migrations/2024_02_23_235000_create_locations_table.php b/database/migrations/2024_02_23_235000_create_locations_table.php index d9c0988..283857e 100644 --- a/database/migrations/2024_02_23_235000_create_locations_table.php +++ b/database/migrations/2024_02_23_235000_create_locations_table.php @@ -24,9 +24,19 @@ return new class extends Migration $table->string('country'); $table->string('postal_code')->nullable(); - // Coordinates - $table->decimal('latitude', 10, 8); - $table->decimal('longitude', 11, 8); + // Enable PostGIS extension if not enabled + DB::statement('CREATE EXTENSION IF NOT EXISTS postgis'); + + // Location name and type + $table->string('name')->nullable(); + $table->string('location_type', 50)->nullable(); + + // Legacy coordinate fields for backward compatibility + $table->decimal('latitude', 9, 6)->nullable(); + $table->decimal('longitude', 9, 6)->nullable(); + + // Coordinates using PostGIS + $table->point('coordinates')->spatialIndex(); $table->decimal('elevation', 8, 2)->nullable(); // Additional details @@ -43,9 +53,10 @@ return new class extends Migration $table->timestamps(); // Indexes - $table->index(['latitude', 'longitude']); $table->index(['country', 'state', 'city']); $table->index('postal_code'); + $table->index('name'); + $table->index('location_type'); }); } diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index a89e5f2..c9f4d14 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -4,7 +4,7 @@ Converting ThrillWiki from Django to Laravel+Livewire ## Current Phase -Parks and Areas Management Implementation +Location System Implementation ## Progress @@ -72,26 +72,41 @@ Parks and Areas Management Implementation - Implemented cache warming - Added performance monitoring - Created error handling +15. ✅ Implemented Location System Foundation: + - Created Location model with PostGIS + - Added polymorphic relationships + - Implemented spatial queries + - Added name and type fields + - Added activity logging + - Created coordinate sync + - Matched Django GeoDjango features ### In Progress -1. [ ] Location System Implementation - - Model structure design - - Polymorphic relationships - - Map integration - - Location selection +1. [ ] Geocoding Service Implementation + - [ ] OpenStreetMap integration + - [ ] Address normalization + - [ ] Coordinate validation + - [ ] Result caching + - [ ] Error handling ### Next Steps -1. Location System - - [ ] Create location model - - [ ] Add polymorphic relationships - - [ ] Implement geocoding service +1. Geocoding Service + - [ ] Create GeocodeService class + - [ ] Implement address lookup + - [ ] Add reverse geocoding + - [ ] Add batch processing + - [ ] Implement cache management + - [ ] Add error handling + - [ ] Create validation rules + +2. Location Components - [ ] Create map component - [ ] Add location selection - - [ ] Implement search - - [ ] Add clustering - - [ ] Create distance calculations + - [ ] Implement search interface + - [ ] Add clustering support + - [ ] Create location display -2. Performance Optimization +3. Performance Optimization - [ ] Implement query caching - [ ] Add index optimization - [ ] Create monitoring tools @@ -101,19 +116,33 @@ Parks and Areas Management Implementation ### Recent Implementations -1. Statistics Caching Design - - Service-based architecture - - Hierarchical caching - - Automatic invalidation - - Performance monitoring +1. Location System Design + - PostGIS integration matching Django GeoDjango + - Polymorphic relationships for flexibility + - Legacy coordinate fields for compatibility + - Name and location type fields added + - Activity logging for location changes + - Automatic coordinate sync between formats + - Efficient spatial queries using PostGIS + - Geography type for accurate calculations + - Spatial indexing with GiST -2. Cache Management +2. Technical Decisions + - Maintain backward compatibility with lat/lon fields + - Use activity logging for change tracking + - Implement coordinate normalization + - Support both geography and geometry types + - Add name and type fields for better organization + - Use PostGIS functions matching Django's implementation + - Implement string representation for consistency + +3. Cache Management - 24-hour TTL - Batch processing - Error handling - Logging system -3. Performance Features +4. Performance Features - Efficient key structure - Optimized data format - Minimal cache churn @@ -134,14 +163,14 @@ Parks and Areas Management Implementation - Statistics rollup ## Notes and Considerations -1. Need to research map providers +1. Configure OpenStreetMap integration 2. Consider caching geocoding results 3. May need clustering for large datasets 4. Should implement distance-based search 5. Consider adding location history 6. Plan for offline maps 7. Consider adding route planning -8. Need to handle map errors +8. Need to handle OpenStreetMap API errors 9. Consider adding location sharing 10. Plan for mobile optimization 11. Consider adding geofencing @@ -164,5 +193,5 @@ Parks and Areas Management Implementation 14. [ ] Add trend analysis tools 15. [ ] Set up cache invalidation 16. [ ] Add cache warming jobs -17. [ ] Research map providers -18. [ ] Plan geocoding strategy \ No newline at end of file +17. [ ] Set up OpenStreetMap API integration +18. [ ] Implement OpenStreetMap geocoding \ No newline at end of file diff --git a/memory-bank/models/LocationModel.md b/memory-bank/models/LocationModel.md index e9cce4d..f2f0f5c 100644 --- a/memory-bank/models/LocationModel.md +++ b/memory-bank/models/LocationModel.md @@ -19,9 +19,8 @@ The Location model provides polymorphic location management for parks, areas, an - `country` (string) - Country name - `postal_code` (string, nullable) - Postal/ZIP code -- **Coordinates** - - `latitude` (decimal, 10,8) - Latitude coordinate - - `longitude` (decimal, 11,8) - Longitude coordinate +- **Spatial Data** + - `coordinates` (point) - PostGIS point geometry with SRID 4326 - `elevation` (decimal, 8,2, nullable) - Elevation in meters - **Additional Details** @@ -35,10 +34,17 @@ The Location model provides polymorphic location management for parks, areas, an - `geocoded_at` (timestamp, nullable) - Last geocoding timestamp ### Indexes -- Coordinates: `(latitude, longitude)` +- Spatial: `coordinates` (spatial index for efficient queries) - Location: `(country, state, city)` - Postal: `postal_code` +### PostGIS Integration +- Uses PostGIS point type for coordinates +- SRID 4326 (WGS 84) for global coordinates +- Spatial indexing for efficient queries +- Native distance calculations +- Geographic vs Geometric operations + ## Relationships ### Polymorphic diff --git a/memory-bank/prompts/GeocodeServiceImplementation.md b/memory-bank/prompts/GeocodeServiceImplementation.md new file mode 100644 index 0000000..9ab8ae4 --- /dev/null +++ b/memory-bank/prompts/GeocodeServiceImplementation.md @@ -0,0 +1,75 @@ +# GeocodeService Implementation Prompt + +## Context +Continue ThrillWiki Laravel+Livewire development, focusing on implementing the GeocodeService for the Location System. The Location model and HasLocation trait are complete, providing the foundation for location management. + +## Key Memory Bank Files +1. memory-bank/activeContext.md - Current progress and next steps +2. memory-bank/features/LocationSystem.md - System design and implementation plan +3. memory-bank/models/LocationModel.md - Location model documentation +4. memory-bank/traits/HasLocation.md - HasLocation trait documentation + +## Current Progress +- ✅ Created Location model with polymorphic relationships +- ✅ Implemented HasLocation trait +- ✅ Set up location-based queries and calculations +- ✅ Added comprehensive documentation + +## Next Implementation Steps +1. Create GeocodeService + - API integration + - Address lookup + - Coordinate validation + - Batch processing + - Cache management + +2. Implement Location Components + - Map integration + - Location selection + - Search functionality + - Clustering support + +## Technical Requirements + +### GeocodeService Features +- Address to coordinates conversion +- Reverse geocoding (coordinates to address) +- Batch geocoding support +- Result caching +- Error handling +- Rate limiting +- Validation + +### Integration Points +- Location model geocoding methods +- HasLocation trait helpers +- Component integration +- Cache system + +### Performance Considerations +- Cache geocoding results +- Implement request batching +- Handle API rate limits +- Optimize response storage + +## Development Guidelines +- Follow Memory Bank documentation practices +- Document technical decisions +- Update activeContext.md after each step +- Create comprehensive service tests +- Consider error handling and edge cases + +## Project Stack +- Laravel for backend +- Livewire for components +- PostgreSQL for database (with PostGIS extension) +- Memory Bank for documentation + +## PostgreSQL Spatial Features +- Using PostGIS for spatial operations +- Native coordinate type (POINT) +- Efficient spatial indexing +- Advanced distance calculations +- Geographic vs Geometric types + +Continue implementation following established patterns and maintaining comprehensive documentation. \ No newline at end of file diff --git a/memory-bank/traits/HasLocation.md b/memory-bank/traits/HasLocation.md new file mode 100644 index 0000000..5ab8020 --- /dev/null +++ b/memory-bank/traits/HasLocation.md @@ -0,0 +1,156 @@ +# HasLocation Trait + +## Overview +The HasLocation trait provides location management capabilities to Laravel models through a polymorphic relationship with the Location model. It enables models to have associated geographic data, including coordinates, address information, and location-based querying capabilities. + +## Features + +### Relationships +- `location()` - MorphOne relationship to Location model +- Automatic location deletion when parent model is deleted + +### Accessors +- `coordinates` - Returns [lat, lng] array +- `formatted_address` - Returns formatted address string +- `map_url` - Returns Google Maps URL + +### Location Management +- `updateLocation(array $attributes)` - Update or create location +- `setCoordinates(float $lat, float $lng, ?float $elevation)` - Set coordinates +- `setAddress(array $components)` - Set address components + +### Distance & Search +- `distanceTo($model)` - Calculate distance to another model +- `scopeNearby($query, $lat, $lng, $radius)` - Find nearby models +- `scopeInBounds($query, $ne, $sw)` - Find models within bounds + +## Usage + +### Adding Location Support +```php +use App\Traits\HasLocation; + +class Park extends Model +{ + use HasLocation; +} +``` + +### Managing Locations with PostGIS +```php +// Create/update location with PostGIS point +$park->updateLocation([ + 'address' => '123 Main St', + 'city' => 'Orlando', + 'state' => 'FL', + 'country' => 'USA', + 'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint(-81.379234, 28.538336), 4326)") +]); + +// Set coordinates directly (automatically creates PostGIS point) +$park->setCoordinates(28.538336, -81.379234); + +// Access location data (automatically extracts from PostGIS point) +$coordinates = $park->coordinates; // Returns [lat, lng] +$address = $park->formatted_address; +$mapUrl = $park->map_url; +``` + +### PostGIS Spatial Queries +```php +// Find parks within 50km using ST_DWithin +$nearbyParks = Park::nearby(28.538336, -81.379234, 50)->get(); + +// Find parks in bounds using ST_MakeEnvelope +$parksInArea = Park::inBounds( + ['lat' => 28.6, 'lng' => -81.2], // Northeast + ['lat' => 28.4, 'lng' => -81.4] // Southwest +)->get(); + +// Calculate distance using ST_Distance +$distance = $parkA->distanceTo($parkB); +``` + +### PostGIS Functions Used +- `ST_SetSRID` - Set spatial reference system (4326 = WGS 84) +- `ST_MakePoint` - Create point geometry from coordinates +- `ST_DWithin` - Find points within distance +- `ST_MakeEnvelope` - Create bounding box +- `ST_Within` - Check if point is within bounds +- `ST_Distance` - Calculate distance between points + +## Integration Points + +### Models Using Trait +- Park +- ParkArea +- (Future models requiring location) + +### Related Components +- Location model +- GeocodeService +- LocationSearchService +- LocationSelector component +- LocationDisplay component + +## Implementation Notes + +### Design Decisions +1. **Automatic Cleanup** + - Location records are automatically deleted when parent is deleted + - Prevents orphaned location records + - Maintains referential integrity + +2. **Flexible Updates** + - `updateLocation()` handles both create and update + - Reduces code duplication + - Provides consistent interface + +3. **Coordinate Handling** + - Consistent lat/lng format + - Optional elevation support + - Null handling for incomplete data + +4. **Query Scopes** + - Chainable with other queries + - Consistent parameter formats + - Performance-optimized implementation + +### Performance Considerations +- Spatial indexing with PostGIS GiST index +- Native PostGIS distance calculations +- Geography vs Geometry type selection + - Geography for accurate distance calculations + - Geometry for faster bounding box queries +- Efficient spatial joins using PostGIS functions +- Eager loading recommended for lists +- Optimized polymorphic relationships + +### Security +- Input validation in setters +- Coordinate bounds checking +- Safe SQL distance calculations + +## Testing Strategy + +### Unit Tests +- [ ] Relationship management +- [ ] Coordinate handling +- [ ] Address formatting +- [ ] Distance calculations + +### Integration Tests +- [ ] Location updates +- [ ] Search queries +- [ ] Boundary queries +- [ ] Deletion cleanup + +## Future Enhancements +1. [ ] Add elevation support +2. [ ] Implement timezone handling +3. [ ] Add boundary polygon support +4. [ ] Create location history tracking +5. [ ] Add batch location updates +6. [ ] Implement geofencing +7. [ ] Add location validation +8. [ ] Create location sharing \ No newline at end of file