mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 07:11:09 -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:
@@ -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<string>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
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<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.
|
||||
*
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user