677 lines
21 KiB
PHP
677 lines
21 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Modules\RestaurantDelivery\Services\Maps;
|
||
|
|
|
||
|
|
use Illuminate\Support\Facades\Cache;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
|
||
|
|
class MapsService
|
||
|
|
{
|
||
|
|
protected array $config;
|
||
|
|
|
||
|
|
protected string $provider;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->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);
|
||
|
|
}
|
||
|
|
}
|