mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 12:11:14 -05:00
275 lines
7.3 KiB
PHP
275 lines
7.3 KiB
PHP
<?php
|
|
|
|
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',
|
|
'coordinates',
|
|
'latitude', // Legacy field
|
|
'longitude', // Legacy field
|
|
'elevation',
|
|
'timezone',
|
|
'metadata',
|
|
'is_approximate',
|
|
'source',
|
|
'geocoding_data',
|
|
'geocoded_at',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
protected $casts = [
|
|
'coordinates' => 'point',
|
|
'latitude' => 'decimal:6',
|
|
'longitude' => 'decimal:6',
|
|
'elevation' => 'decimal:2',
|
|
'metadata' => 'array',
|
|
'geocoding_data' => 'array',
|
|
'geocoded_at' => 'datetime',
|
|
'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.
|
|
*/
|
|
public function locatable(): MorphTo
|
|
{
|
|
return $this->morphTo();
|
|
}
|
|
|
|
/**
|
|
* Get Google Maps URL for the location.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getMapUrlAttribute(): ?string
|
|
{
|
|
if (!$this->latitude || !$this->longitude) {
|
|
return null;
|
|
}
|
|
|
|
return sprintf(
|
|
'https://www.google.com/maps?q=%f,%f',
|
|
$this->latitude,
|
|
$this->longitude
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update the location's coordinates.
|
|
*
|
|
* @param float $latitude
|
|
* @param float $longitude
|
|
* @param float|null $elevation
|
|
* @return bool
|
|
*/
|
|
public function updateCoordinates(float $latitude, float $longitude, ?float $elevation = null): bool
|
|
{
|
|
return $this->update([
|
|
'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"),
|
|
'elevation' => $elevation,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Set the location's address components.
|
|
*
|
|
* @param array<string, string> $components
|
|
* @return bool
|
|
*/
|
|
public function setAddress(array $components): bool
|
|
{
|
|
return $this->update([
|
|
'address' => $components['address'] ?? $this->address,
|
|
'city' => $components['city'] ?? $this->city,
|
|
'state' => $components['state'] ?? $this->state,
|
|
'country' => $components['country'] ?? $this->country,
|
|
'postal_code' => $components['postal_code'] ?? $this->postal_code,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to find locations within a radius.
|
|
*
|
|
* @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_MakePoint($longitude, $latitude)");
|
|
$distance = $unit === 'mi' ? $radius * 1609.34 : $radius * 1000;
|
|
|
|
return $query->whereRaw(
|
|
"ST_DWithin(coordinates::geography, ?::geography, ?)",
|
|
[$point, $distance]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to find locations 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->whereRaw("ST_Within(coordinates::geometry, ?::geometry)", [$bounds]);
|
|
}
|
|
|
|
/**
|
|
* Calculate the distance to a point.
|
|
*
|
|
* @param float $latitude
|
|
* @param float $longitude
|
|
* @param string $unit
|
|
* @return float|null
|
|
*/
|
|
public function distanceTo(float $latitude, float $longitude, string $unit = 'km'): ?float
|
|
{
|
|
if (!$this->coordinates) {
|
|
return null;
|
|
}
|
|
|
|
$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;
|
|
|
|
return $unit === 'mi' ? $distance * 0.000621371 : $distance / 1000;
|
|
}
|
|
} |