Refactor Location model to integrate PostGIS for spatial data, add legacy coordinate fields for compatibility, and enhance documentation for geocoding service implementation.

This commit is contained in:
pacnpal
2025-02-23 20:06:27 -05:00
parent 7e5d15eb46
commit f15392806a
8 changed files with 723 additions and 109 deletions

105
.clinerules Normal file
View File

@@ -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

View File

@@ -4,22 +4,28 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\DB;
class Location extends Model class Location extends Model
{ {
use \Spatie\Activitylog\LogsActivity;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *
* @var array<string> * @var array<string>
*/ */
protected $fillable = [ protected $fillable = [
'name',
'location_type',
'address', 'address',
'city', 'city',
'state', 'state',
'country', 'country',
'postal_code', 'postal_code',
'latitude', 'coordinates',
'longitude', 'latitude', // Legacy field
'longitude', // Legacy field
'elevation', 'elevation',
'timezone', 'timezone',
'metadata', 'metadata',
@@ -35,8 +41,9 @@ class Location extends Model
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'latitude' => 'decimal:8', 'coordinates' => 'point',
'longitude' => 'decimal:8', 'latitude' => 'decimal:6',
'longitude' => 'decimal:6',
'elevation' => 'decimal:2', 'elevation' => 'decimal:2',
'metadata' => 'array', 'metadata' => 'array',
'geocoding_data' => 'array', 'geocoding_data' => 'array',
@@ -44,6 +51,97 @@ class Location extends Model
'is_approximate' => 'boolean', '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. * Get the parent locatable model.
*/ */
@@ -52,51 +150,6 @@ class Location extends Model
return $this->morphTo(); return $this->morphTo();
} }
/**
* Get the location's coordinates as an array.
*
* @return array<string, float|null>
*/
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. * 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 public function updateCoordinates(float $latitude, float $longitude, ?float $elevation = null): bool
{ {
return $this->update([ return $this->update([
'latitude' => $latitude, 'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"),
'longitude' => $longitude,
'elevation' => $elevation, 'elevation' => $elevation,
]); ]);
} }
@@ -161,17 +213,12 @@ class Location extends Model
*/ */
public function scopeNearby($query, float $latitude, float $longitude, float $radius, string $unit = 'km') 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( return $query->whereRaw(
"($earthRadius * acos( "ST_DWithin(coordinates::geography, ?::geography, ?)",
cos(radians(?)) * [$point, $distance]
cos(radians(latitude)) *
cos(radians(longitude) - radians(?)) +
sin(radians(?)) *
sin(radians(latitude))
)) <= ?",
[$latitude, $longitude, $latitude, $radius]
); );
} }
@@ -185,8 +232,15 @@ class Location extends Model
*/ */
public function scopeInBounds($query, array $ne, array $sw) public function scopeInBounds($query, array $ne, array $sw)
{ {
return $query->whereBetween('latitude', [$sw['lat'], $ne['lat']]) $bounds = DB::raw(
->whereBetween('longitude', [$sw['lng'], $ne['lng']]); "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 public function distanceTo(float $latitude, float $longitude, string $unit = 'km'): ?float
{ {
if (!$this->latitude || !$this->longitude) { if (!$this->coordinates) {
return null; 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); return $unit === 'mi' ? $distance * 0.000621371 : $distance / 1000;
$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;
} }
} }

178
app/Traits/HasLocation.php Normal file
View File

@@ -0,0 +1,178 @@
<?php
namespace App\Traits;
use App\Models\Location;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Support\Facades\DB;
trait HasLocation
{
/**
* Boot the trait.
*/
public static function bootHasLocation(): void
{
static::deleting(function ($model) {
// Delete associated location when model is deleted
$model->location?->delete();
});
}
/**
* Get the model's location.
*/
public function location(): MorphOne
{
return $this->morphOne(Location::class, 'locatable');
}
/**
* Get the model's coordinates.
*
* @return array<string, float|null>
*/
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<string, mixed> $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<string, string> $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<string, float> $ne Northeast corner [lat, lng]
* @param array<string, float> $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]);
});
}
}

View File

@@ -24,9 +24,19 @@ return new class extends Migration
$table->string('country'); $table->string('country');
$table->string('postal_code')->nullable(); $table->string('postal_code')->nullable();
// Coordinates // Enable PostGIS extension if not enabled
$table->decimal('latitude', 10, 8); DB::statement('CREATE EXTENSION IF NOT EXISTS postgis');
$table->decimal('longitude', 11, 8);
// 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(); $table->decimal('elevation', 8, 2)->nullable();
// Additional details // Additional details
@@ -43,9 +53,10 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
// Indexes // Indexes
$table->index(['latitude', 'longitude']);
$table->index(['country', 'state', 'city']); $table->index(['country', 'state', 'city']);
$table->index('postal_code'); $table->index('postal_code');
$table->index('name');
$table->index('location_type');
}); });
} }

View File

@@ -4,7 +4,7 @@
Converting ThrillWiki from Django to Laravel+Livewire Converting ThrillWiki from Django to Laravel+Livewire
## Current Phase ## Current Phase
Parks and Areas Management Implementation Location System Implementation
## Progress ## Progress
@@ -72,26 +72,41 @@ Parks and Areas Management Implementation
- Implemented cache warming - Implemented cache warming
- Added performance monitoring - Added performance monitoring
- Created error handling - 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 ### In Progress
1. [ ] Location System Implementation 1. [ ] Geocoding Service Implementation
- Model structure design - [ ] OpenStreetMap integration
- Polymorphic relationships - [ ] Address normalization
- Map integration - [ ] Coordinate validation
- Location selection - [ ] Result caching
- [ ] Error handling
### Next Steps ### Next Steps
1. Location System 1. Geocoding Service
- [ ] Create location model - [ ] Create GeocodeService class
- [ ] Add polymorphic relationships - [ ] Implement address lookup
- [ ] Implement geocoding service - [ ] Add reverse geocoding
- [ ] Add batch processing
- [ ] Implement cache management
- [ ] Add error handling
- [ ] Create validation rules
2. Location Components
- [ ] Create map component - [ ] Create map component
- [ ] Add location selection - [ ] Add location selection
- [ ] Implement search - [ ] Implement search interface
- [ ] Add clustering - [ ] Add clustering support
- [ ] Create distance calculations - [ ] Create location display
2. Performance Optimization 3. Performance Optimization
- [ ] Implement query caching - [ ] Implement query caching
- [ ] Add index optimization - [ ] Add index optimization
- [ ] Create monitoring tools - [ ] Create monitoring tools
@@ -101,19 +116,33 @@ Parks and Areas Management Implementation
### Recent Implementations ### Recent Implementations
1. Statistics Caching Design 1. Location System Design
- Service-based architecture - PostGIS integration matching Django GeoDjango
- Hierarchical caching - Polymorphic relationships for flexibility
- Automatic invalidation - Legacy coordinate fields for compatibility
- Performance monitoring - 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 - 24-hour TTL
- Batch processing - Batch processing
- Error handling - Error handling
- Logging system - Logging system
3. Performance Features 4. Performance Features
- Efficient key structure - Efficient key structure
- Optimized data format - Optimized data format
- Minimal cache churn - Minimal cache churn
@@ -134,14 +163,14 @@ Parks and Areas Management Implementation
- Statistics rollup - Statistics rollup
## Notes and Considerations ## Notes and Considerations
1. Need to research map providers 1. Configure OpenStreetMap integration
2. Consider caching geocoding results 2. Consider caching geocoding results
3. May need clustering for large datasets 3. May need clustering for large datasets
4. Should implement distance-based search 4. Should implement distance-based search
5. Consider adding location history 5. Consider adding location history
6. Plan for offline maps 6. Plan for offline maps
7. Consider adding route planning 7. Consider adding route planning
8. Need to handle map errors 8. Need to handle OpenStreetMap API errors
9. Consider adding location sharing 9. Consider adding location sharing
10. Plan for mobile optimization 10. Plan for mobile optimization
11. Consider adding geofencing 11. Consider adding geofencing
@@ -164,5 +193,5 @@ Parks and Areas Management Implementation
14. [ ] Add trend analysis tools 14. [ ] Add trend analysis tools
15. [ ] Set up cache invalidation 15. [ ] Set up cache invalidation
16. [ ] Add cache warming jobs 16. [ ] Add cache warming jobs
17. [ ] Research map providers 17. [ ] Set up OpenStreetMap API integration
18. [ ] Plan geocoding strategy 18. [ ] Implement OpenStreetMap geocoding

View File

@@ -19,9 +19,8 @@ The Location model provides polymorphic location management for parks, areas, an
- `country` (string) - Country name - `country` (string) - Country name
- `postal_code` (string, nullable) - Postal/ZIP code - `postal_code` (string, nullable) - Postal/ZIP code
- **Coordinates** - **Spatial Data**
- `latitude` (decimal, 10,8) - Latitude coordinate - `coordinates` (point) - PostGIS point geometry with SRID 4326
- `longitude` (decimal, 11,8) - Longitude coordinate
- `elevation` (decimal, 8,2, nullable) - Elevation in meters - `elevation` (decimal, 8,2, nullable) - Elevation in meters
- **Additional Details** - **Additional Details**
@@ -35,10 +34,17 @@ The Location model provides polymorphic location management for parks, areas, an
- `geocoded_at` (timestamp, nullable) - Last geocoding timestamp - `geocoded_at` (timestamp, nullable) - Last geocoding timestamp
### Indexes ### Indexes
- Coordinates: `(latitude, longitude)` - Spatial: `coordinates` (spatial index for efficient queries)
- Location: `(country, state, city)` - Location: `(country, state, city)`
- Postal: `postal_code` - 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 ## Relationships
### Polymorphic ### Polymorphic

View File

@@ -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.

View File

@@ -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