config = config('restaurant-delivery.maps'); $this->provider = $this->config['provider']; } /* |-------------------------------------------------------------------------- | Route Calculation |-------------------------------------------------------------------------- */ /** * Get route between two points */ public function getRoute( float $originLat, float $originLng, float $destLat, float $destLng, ?array $waypoints = null ): ?array { $cacheKey = "route_{$originLat}_{$originLng}_{$destLat}_{$destLng}_".md5(json_encode($waypoints ?? [])); return Cache::remember( $cacheKey, config('restaurant-delivery.cache.ttl.route_calculation'), function () use ($originLat, $originLng, $destLat, $destLng, $waypoints) { return match ($this->provider) { 'google' => $this->getGoogleRoute($originLat, $originLng, $destLat, $destLng, $waypoints), 'mapbox' => $this->getMapboxRoute($originLat, $originLng, $destLat, $destLng, $waypoints), default => $this->getGoogleRoute($originLat, $originLng, $destLat, $destLng, $waypoints), }; } ); } /** * Get route using Google Maps Directions API */ protected function getGoogleRoute( float $originLat, float $originLng, float $destLat, float $destLng, ?array $waypoints = null ): ?array { try { $apiKey = $this->config['google']['api_key']; if (! $apiKey) { Log::warning('Google Maps API key not configured'); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $params = [ 'origin' => "{$originLat},{$originLng}", 'destination' => "{$destLat},{$destLng}", 'key' => $apiKey, 'mode' => 'driving', 'departure_time' => 'now', 'traffic_model' => $this->config['route']['traffic_model'], 'alternatives' => $this->config['route']['alternatives'] ? 'true' : 'false', ]; if ($waypoints) { $params['waypoints'] = implode('|', array_map( fn ($wp) => "{$wp['lat']},{$wp['lng']}", $waypoints )); } $response = Http::get('https://maps.googleapis.com/maps/api/directions/json', $params); if (! $response->successful()) { Log::error('Google Maps API request failed', [ 'status' => $response->status(), ]); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $data = $response->json(); if ($data['status'] !== 'OK' || empty($data['routes'])) { Log::error('Google Maps API returned error', [ 'status' => $data['status'], 'error_message' => $data['error_message'] ?? null, ]); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $route = $data['routes'][0]; $leg = $route['legs'][0]; return [ 'polyline' => $route['overview_polyline']['points'], 'points' => $this->decodePolyline($route['overview_polyline']['points']), 'distance' => $leg['distance']['value'] / 1000, // Convert to km 'distance_text' => $leg['distance']['text'], 'duration' => (int) ceil($leg['duration']['value'] / 60), // Convert to minutes 'duration_text' => $leg['duration']['text'], 'duration_in_traffic' => isset($leg['duration_in_traffic']) ? (int) ceil($leg['duration_in_traffic']['value'] / 60) : null, 'start_address' => $leg['start_address'], 'end_address' => $leg['end_address'], 'steps' => array_map(fn ($step) => [ 'instruction' => strip_tags($step['html_instructions']), 'distance' => $step['distance']['value'], 'duration' => $step['duration']['value'], 'start' => $step['start_location'], 'end' => $step['end_location'], 'maneuver' => $step['maneuver'] ?? null, ], $leg['steps']), 'alternatives' => $this->config['route']['alternatives'] ? $this->parseAlternativeRoutes($data['routes']) : [], ]; } catch (\Exception $e) { Log::error('Google Maps route calculation failed', [ 'error' => $e->getMessage(), ]); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } } /** * Get route using Mapbox Directions API */ protected function getMapboxRoute( float $originLat, float $originLng, float $destLat, float $destLng, ?array $waypoints = null ): ?array { try { $accessToken = $this->config['mapbox']['access_token']; if (! $accessToken) { Log::warning('Mapbox access token not configured'); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $coordinates = "{$originLng},{$originLat}"; if ($waypoints) { foreach ($waypoints as $wp) { $coordinates .= ";{$wp['lng']},{$wp['lat']}"; } } $coordinates .= ";{$destLng},{$destLat}"; $params = [ 'access_token' => $accessToken, 'geometries' => 'polyline', 'overview' => 'full', 'steps' => 'true', 'alternatives' => $this->config['route']['alternatives'] ? 'true' : 'false', ]; $response = Http::get( "https://api.mapbox.com/directions/v5/mapbox/driving/{$coordinates}", $params ); if (! $response->successful()) { Log::error('Mapbox API request failed', [ 'status' => $response->status(), ]); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $data = $response->json(); if (empty($data['routes'])) { Log::error('Mapbox API returned no routes'); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } $route = $data['routes'][0]; return [ 'polyline' => $route['geometry'], 'points' => $this->decodePolyline($route['geometry']), 'distance' => $route['distance'] / 1000, // Convert to km 'distance_text' => $this->formatDistance($route['distance'] / 1000), 'duration' => (int) ceil($route['duration'] / 60), // Convert to minutes 'duration_text' => $this->formatDuration($route['duration']), 'steps' => array_map(fn ($step) => [ 'instruction' => $step['maneuver']['instruction'] ?? '', 'distance' => $step['distance'], 'duration' => $step['duration'], 'maneuver' => $step['maneuver']['type'] ?? null, ], $route['legs'][0]['steps'] ?? []), 'alternatives' => [], ]; } catch (\Exception $e) { Log::error('Mapbox route calculation failed', [ 'error' => $e->getMessage(), ]); return $this->getFallbackRoute($originLat, $originLng, $destLat, $destLng); } } /** * Fallback route calculation using Haversine (straight line) */ protected function getFallbackRoute( float $originLat, float $originLng, float $destLat, float $destLng ): array { $distance = $this->calculateHaversineDistance($originLat, $originLng, $destLat, $destLng); // Estimate duration assuming average speed of 25 km/h $duration = (int) ceil(($distance / 25) * 60); // Create a simple polyline (straight line) $polyline = $this->encodePolyline([ ['lat' => $originLat, 'lng' => $originLng], ['lat' => $destLat, 'lng' => $destLng], ]); return [ 'polyline' => $polyline, 'points' => [ ['lat' => $originLat, 'lng' => $originLng], ['lat' => $destLat, 'lng' => $destLng], ], 'distance' => $distance, 'distance_text' => $this->formatDistance($distance), 'duration' => $duration, 'duration_text' => $this->formatDuration($duration * 60), 'is_fallback' => true, 'steps' => [], 'alternatives' => [], ]; } /** * Parse alternative routes from Google response */ protected function parseAlternativeRoutes(array $routes): array { $alternatives = []; for ($i = 1; $i < count($routes); $i++) { $route = $routes[$i]; $leg = $route['legs'][0]; $alternatives[] = [ 'polyline' => $route['overview_polyline']['points'], 'distance' => $leg['distance']['value'] / 1000, 'duration' => (int) ceil($leg['duration']['value'] / 60), 'summary' => $route['summary'] ?? null, ]; } return $alternatives; } /* |-------------------------------------------------------------------------- | Distance Matrix |-------------------------------------------------------------------------- */ /** * Get distance matrix for multiple origins and destinations */ public function getDistanceMatrix(array $origins, array $destinations): ?array { if ($this->provider !== 'google' || ! $this->config['google']['distance_matrix_api']) { return $this->getFallbackDistanceMatrix($origins, $destinations); } try { $apiKey = $this->config['google']['api_key']; $originStr = implode('|', array_map( fn ($o) => "{$o['lat']},{$o['lng']}", $origins )); $destStr = implode('|', array_map( fn ($d) => "{$d['lat']},{$d['lng']}", $destinations )); $response = Http::get('https://maps.googleapis.com/maps/api/distancematrix/json', [ 'origins' => $originStr, 'destinations' => $destStr, 'key' => $apiKey, 'mode' => 'driving', 'departure_time' => 'now', ]); if (! $response->successful()) { return $this->getFallbackDistanceMatrix($origins, $destinations); } $data = $response->json(); if ($data['status'] !== 'OK') { return $this->getFallbackDistanceMatrix($origins, $destinations); } $matrix = []; foreach ($data['rows'] as $i => $row) { $matrix[$i] = []; foreach ($row['elements'] as $j => $element) { if ($element['status'] === 'OK') { $matrix[$i][$j] = [ 'distance' => $element['distance']['value'] / 1000, 'duration' => (int) ceil($element['duration']['value'] / 60), ]; } else { $matrix[$i][$j] = null; } } } return $matrix; } catch (\Exception $e) { Log::error('Distance matrix calculation failed', [ 'error' => $e->getMessage(), ]); return $this->getFallbackDistanceMatrix($origins, $destinations); } } /** * Fallback distance matrix using Haversine */ protected function getFallbackDistanceMatrix(array $origins, array $destinations): array { $matrix = []; foreach ($origins as $i => $origin) { $matrix[$i] = []; foreach ($destinations as $j => $dest) { $distance = $this->calculateHaversineDistance( $origin['lat'], $origin['lng'], $dest['lat'], $dest['lng'] ); $matrix[$i][$j] = [ 'distance' => $distance, 'duration' => (int) ceil(($distance / 25) * 60), ]; } } return $matrix; } /* |-------------------------------------------------------------------------- | Geocoding |-------------------------------------------------------------------------- */ /** * Geocode an address to coordinates */ public function geocode(string $address): ?array { $cacheKey = 'geocode_'.md5($address); return Cache::remember($cacheKey, 86400, function () use ($address) { if ($this->provider === 'google' && $this->config['google']['geocoding_api']) { return $this->googleGeocode($address); } return null; }); } /** * Reverse geocode coordinates to address */ public function reverseGeocode(float $latitude, float $longitude): ?array { $cacheKey = "reverse_geocode_{$latitude}_{$longitude}"; return Cache::remember($cacheKey, 86400, function () use ($latitude, $longitude) { if ($this->provider === 'google' && $this->config['google']['geocoding_api']) { return $this->googleReverseGeocode($latitude, $longitude); } return null; }); } /** * Google geocoding */ protected function googleGeocode(string $address): ?array { try { $response = Http::get('https://maps.googleapis.com/maps/api/geocode/json', [ 'address' => $address, 'key' => $this->config['google']['api_key'], ]); if (! $response->successful()) { return null; } $data = $response->json(); if ($data['status'] !== 'OK' || empty($data['results'])) { return null; } $result = $data['results'][0]; return [ 'lat' => $result['geometry']['location']['lat'], 'lng' => $result['geometry']['location']['lng'], 'formatted_address' => $result['formatted_address'], 'place_id' => $result['place_id'], 'components' => $this->parseAddressComponents($result['address_components']), ]; } catch (\Exception $e) { Log::error('Geocoding failed', ['error' => $e->getMessage()]); return null; } } /** * Google reverse geocoding */ protected function googleReverseGeocode(float $latitude, float $longitude): ?array { try { $response = Http::get('https://maps.googleapis.com/maps/api/geocode/json', [ 'latlng' => "{$latitude},{$longitude}", 'key' => $this->config['google']['api_key'], ]); if (! $response->successful()) { return null; } $data = $response->json(); if ($data['status'] !== 'OK' || empty($data['results'])) { return null; } $result = $data['results'][0]; return [ 'formatted_address' => $result['formatted_address'], 'place_id' => $result['place_id'], 'components' => $this->parseAddressComponents($result['address_components']), ]; } catch (\Exception $e) { Log::error('Reverse geocoding failed', ['error' => $e->getMessage()]); return null; } } /** * Parse address components from Google response */ protected function parseAddressComponents(array $components): array { $parsed = []; foreach ($components as $component) { foreach ($component['types'] as $type) { $parsed[$type] = $component['long_name']; } } return $parsed; } /* |-------------------------------------------------------------------------- | Utility Methods |-------------------------------------------------------------------------- */ /** * Calculate Haversine distance */ public function calculateHaversineDistance( float $lat1, float $lng1, float $lat2, float $lng2 ): float { $earthRadius = config('restaurant-delivery.distance.earth_radius_km'); $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) * sin($dLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return round($earthRadius * $c, 2); } /** * Decode polyline string to array of points */ public function decodePolyline(string $encoded): array { $points = []; $index = 0; $lat = 0; $lng = 0; while ($index < strlen($encoded)) { $shift = 0; $result = 0; do { $b = ord($encoded[$index++]) - 63; $result |= ($b & 0x1F) << $shift; $shift += 5; } while ($b >= 0x20); $dlat = (($result & 1) ? ~($result >> 1) : ($result >> 1)); $lat += $dlat; $shift = 0; $result = 0; do { $b = ord($encoded[$index++]) - 63; $result |= ($b & 0x1F) << $shift; $shift += 5; } while ($b >= 0x20); $dlng = (($result & 1) ? ~($result >> 1) : ($result >> 1)); $lng += $dlng; $points[] = [ 'lat' => $lat / 1e5, 'lng' => $lng / 1e5, ]; } return $points; } /** * Encode array of points to polyline string */ public function encodePolyline(array $points): string { $encoded = ''; $prevLat = 0; $prevLng = 0; foreach ($points as $point) { $lat = (int) round($point['lat'] * 1e5); $lng = (int) round($point['lng'] * 1e5); $encoded .= $this->encodeNumber($lat - $prevLat); $encoded .= $this->encodeNumber($lng - $prevLng); $prevLat = $lat; $prevLng = $lng; } return $encoded; } /** * Encode a number for polyline */ protected function encodeNumber(int $num): string { $encoded = ''; $num = $num < 0 ? ~($num << 1) : ($num << 1); while ($num >= 0x20) { $encoded .= chr((0x20 | ($num & 0x1F)) + 63); $num >>= 5; } $encoded .= chr($num + 63); return $encoded; } /** * Format distance for display */ protected function formatDistance(float $km): string { if ($km < 1) { return round($km * 1000).' m'; } return round($km, 1).' km'; } /** * Format duration for display */ protected function formatDuration(int $seconds): string { $minutes = (int) ceil($seconds / 60); if ($minutes < 60) { return $minutes.' min'; } $hours = (int) floor($minutes / 60); $mins = $minutes % 60; return $hours.' hr '.$mins.' min'; } /** * Calculate bearing between two points */ public function calculateBearing( float $lat1, float $lng1, float $lat2, float $lng2 ): float { $lat1Rad = deg2rad($lat1); $lat2Rad = deg2rad($lat2); $dLng = deg2rad($lng2 - $lng1); $x = sin($dLng) * cos($lat2Rad); $y = cos($lat1Rad) * sin($lat2Rad) - sin($lat1Rad) * cos($lat2Rad) * cos($dLng); $bearing = atan2($x, $y); $bearing = rad2deg($bearing); return fmod($bearing + 360, 360); } }