http = $http; } /** * Convert an address to coordinates * * @param string $address * @return array|null * @throws GeocodingException|ValidationException */ public function geocode(string $address): ?array { $this->validateAddress($address); $cacheKey = "geocode:" . md5($address); return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($address) { try { $this->enforceRateLimit(); $response = $this->http->withHeaders([ 'User-Agent' => self::USER_AGENT ])->get(self::BASE_URL . '/search', [ 'q' => $address, 'format' => 'json', 'addressdetails' => 1, 'limit' => 1 ]); if (!$response->successful()) { throw new GeocodingException('Geocoding API request failed: ' . $response->status()); } $data = $response->json(); if (empty($data)) { return null; } return [ 'lat' => (float) $data[0]['lat'], 'lon' => (float) $data[0]['lon'], 'display_name' => $data[0]['display_name'], 'address' => $data[0]['address'] ?? [], 'type' => $data[0]['type'] ?? null, ]; } catch (\Exception $e) { Log::error('Geocoding error', [ 'address' => $address, 'error' => $e->getMessage() ]); throw new GeocodingException('Failed to geocode address: ' . $e->getMessage()); } }); } /** * Convert coordinates to an address * * @param float $lat * @param float $lon * @return array|null * @throws GeocodingException|ValidationException */ public function reverseGeocode(float $lat, float $lon): ?array { $this->validateCoordinates($lat, $lon); $cacheKey = "reverse:{$lat},{$lon}"; return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($lat, $lon) { try { $this->enforceRateLimit(); $response = $this->http->withHeaders([ 'User-Agent' => self::USER_AGENT ])->get(self::BASE_URL . '/reverse', [ 'lat' => $lat, 'lon' => $lon, 'format' => 'json', 'addressdetails' => 1 ]); if (!$response->successful()) { throw new GeocodingException('Reverse geocoding API request failed: ' . $response->status()); } $data = $response->json(); if (empty($data)) { return null; } return [ 'display_name' => $data['display_name'], 'address' => $data['address'] ?? [], 'type' => $data['type'] ?? null, ]; } catch (\Exception $e) { Log::error('Reverse geocoding error', [ 'lat' => $lat, 'lon' => $lon, 'error' => $e->getMessage() ]); throw new GeocodingException('Failed to reverse geocode coordinates: ' . $e->getMessage()); } }); } /** * Geocode multiple addresses * * @param array $addresses * @return array */ public function batchGeocode(array $addresses): array { $results = []; foreach ($addresses as $address) { try { $results[$address] = $this->geocode($address); } catch (\Exception $e) { $results[$address] = null; Log::error('Batch geocoding error', [ 'address' => $address, 'error' => $e->getMessage() ]); } } return $results; } /** * Validate coordinates * * @param float $lat * @param float $lon * @return bool * @throws ValidationException */ public function validateCoordinates(float $lat, float $lon): bool { if ($lat < -90 || $lat > 90) { throw new ValidationException('Invalid latitude. Must be between -90 and 90'); } if ($lon < -180 || $lon > 180) { throw new ValidationException('Invalid longitude. Must be between -180 and 180'); } return true; } /** * Clear geocoding cache * * @param string|null $key * @return void */ public function clearCache(?string $key = null): void { if ($key) { Cache::forget($key); } else { // Clear all geocoding related cache $keys = Cache::get('geocoding_cache_keys', []); foreach ($keys as $cacheKey) { Cache::forget($cacheKey); } Cache::forget('geocoding_cache_keys'); } } /** * Warm up cache with common addresses * * @param array $addresses * @return void */ public function warmCache(array $addresses): void { foreach ($addresses as $address) { try { $this->geocode($address); } catch (\Exception $e) { Log::error('Cache warming error', [ 'address' => $address, 'error' => $e->getMessage() ]); } } } /** * Validate address string * * @param string $address * @return bool * @throws ValidationException */ private function validateAddress(string $address): bool { if (strlen($address) < 5) { throw new ValidationException('Address must be at least 5 characters long'); } if (strlen($address) > 200) { throw new ValidationException('Address must not exceed 200 characters'); } return true; } /** * Enforce rate limiting for API requests * * @return void */ private function enforceRateLimit(): void { if ($this->lastRequestTime !== null) { $timeSinceLastRequest = microtime(true) - $this->lastRequestTime; if ($timeSinceLastRequest < self::RATE_LIMIT) { usleep(($this->RATE_LIMIT - $timeSinceLastRequest) * 1000000); } } $this->lastRequestTime = microtime(true); } }