location?->delete(); }); } /** * Get the model's location. */ public function location(): MorphOne { return $this->morphOne(Location::class, 'locatable'); } /** * Get the model's coordinates. * * @return array */ 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 $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 $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 $ne Northeast corner [lat, lng] * @param array $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]); }); } }