Files
thrillwiki_laravel/app/Traits/HasLocation.php

178 lines
4.7 KiB
PHP

<?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]);
});
}
}