*/ protected $fillable = [ 'address', 'city', 'state', 'country', 'postal_code', 'latitude', 'longitude', 'elevation', 'timezone', 'metadata', 'is_approximate', 'source', 'geocoding_data', 'geocoded_at', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'latitude' => 'decimal:8', 'longitude' => 'decimal:8', 'elevation' => 'decimal:2', 'metadata' => 'array', 'geocoding_data' => 'array', 'geocoded_at' => 'datetime', 'is_approximate' => 'boolean', ]; /** * Get the parent locatable model. */ public function locatable(): MorphTo { return $this->morphTo(); } /** * Get the location's coordinates as an array. * * @return array */ 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. * * @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([ 'latitude' => $latitude, 'longitude' => $longitude, 'elevation' => $elevation, ]); } /** * Set the location's address components. * * @param array $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') { $earthRadius = $unit === 'mi' ? 3959 : 6371; return $query->whereRaw( "($earthRadius * acos( cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)) )) <= ?", [$latitude, $longitude, $latitude, $radius] ); } /** * Scope a query to find locations 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) { return $query->whereBetween('latitude', [$sw['lat'], $ne['lat']]) ->whereBetween('longitude', [$sw['lng'], $ne['lng']]); } /** * 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->latitude || !$this->longitude) { return null; } $earthRadius = $unit === 'mi' ? 3959 : 6371; $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; } }