mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 06:51:10 -05:00
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:
105
.clinerules
Normal file
105
.clinerules
Normal 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
|
||||||
@@ -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
178
app/Traits/HasLocation.php
Normal 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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
75
memory-bank/prompts/GeocodeServiceImplementation.md
Normal file
75
memory-bank/prompts/GeocodeServiceImplementation.md
Normal 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.
|
||||||
156
memory-bank/traits/HasLocation.md
Normal file
156
memory-bank/traits/HasLocation.md
Normal 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
|
||||||
Reference in New Issue
Block a user