194 lines
7.0 KiB
PHP
194 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Google Directions API → fertige GeoJSON-Linie + Distanz/Dauer fürs HUD.
|
|
* Der API-Key bleibt im Backend, das Frontend bekommt nur das Ergebnis.
|
|
*/
|
|
class RouteService
|
|
{
|
|
public function route(string $origin, string $destination, string $mode = 'driving'): ?array
|
|
{
|
|
$key = config('services.google.maps_key');
|
|
if (empty($key)) {
|
|
Log::warning('Google Maps Key fehlt - Routing nicht möglich');
|
|
|
|
return null;
|
|
}
|
|
|
|
// Bis zu 2 Versuche bei transienten Fehlern (Timeout, 5xx, ZERO_RESULTS).
|
|
$response = null;
|
|
$data = null;
|
|
$status = null;
|
|
$lastErr = null;
|
|
|
|
for ($attempt = 1; $attempt <= 2; $attempt++) {
|
|
try {
|
|
$response = Http::timeout((int) config('services.google.directions_timeout', 8))
|
|
->retry(0) // wir handeln retry selbst
|
|
->get('https://maps.googleapis.com/maps/api/directions/json', [
|
|
'origin' => $origin,
|
|
'destination' => $destination,
|
|
'mode' => $mode,
|
|
'language' => 'de',
|
|
'region' => 'at',
|
|
'key' => $key,
|
|
]);
|
|
|
|
if (! $response->successful()) {
|
|
$lastErr = 'HTTP '.$response->status();
|
|
Log::warning('Directions API HTTP-Fehler', ['attempt' => $attempt, 'status' => $response->status()]);
|
|
if ($attempt < 2) {
|
|
usleep(400_000);
|
|
continue;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
$data = $response->json();
|
|
$status = $data['status'] ?? null;
|
|
|
|
// ZERO_RESULTS oder UNKNOWN_ERROR sind oft transient bei ÖPNV - retry
|
|
if (in_array($status, ['ZERO_RESULTS', 'UNKNOWN_ERROR', 'OVER_QUERY_LIMIT'], true) && $attempt < 2) {
|
|
Log::info('Directions transient '.$status.', retry');
|
|
usleep(500_000);
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
} catch (\Throwable $e) {
|
|
$lastErr = $e->getMessage();
|
|
Log::warning('Directions exception', ['attempt' => $attempt, 'e' => $lastErr]);
|
|
if ($attempt < 2) {
|
|
usleep(400_000);
|
|
continue;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
if ($status !== 'OK' || empty($data['routes'])) {
|
|
Log::warning('Directions: keine Route nach '.($attempt ?? 1).' Versuchen', [
|
|
'status' => $status,
|
|
'msg' => $data['error_message'] ?? $lastErr,
|
|
'origin' => $origin,
|
|
'destination' => $destination,
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$route = $data['routes'][0];
|
|
$leg = $route['legs'][0];
|
|
|
|
return [
|
|
'distance_text' => $leg['distance']['text'] ?? '',
|
|
'distance_m' => $leg['distance']['value'] ?? 0,
|
|
'duration_text' => $leg['duration']['text'] ?? '',
|
|
'duration_s' => $leg['duration']['value'] ?? 0,
|
|
'start_address' => $leg['start_address'] ?? '',
|
|
'end_address' => $leg['end_address'] ?? '',
|
|
'start_location' => $leg['start_location'] ?? null,
|
|
'end_location' => $leg['end_location'] ?? null,
|
|
'bounds' => $route['bounds'] ?? null,
|
|
'geojson' => $this->decodePolyline($route['overview_polyline']['points'] ?? ''),
|
|
'mode' => $mode,
|
|
'steps' => $this->normalizeSteps($leg['steps'] ?? []),
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::warning('RouteService exception', ['e' => $e->getMessage()]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Google-Steps → schlankeres Format für die Sidebar mit Transit-Details.
|
|
*/
|
|
protected function normalizeSteps(array $steps): array
|
|
{
|
|
$out = [];
|
|
foreach ($steps as $s) {
|
|
$mode = $s['travel_mode'] ?? 'WALKING';
|
|
$instruction = strip_tags(str_replace(
|
|
['<div', '</div>'], [' <div', '</div> '],
|
|
(string) ($s['html_instructions'] ?? ''),
|
|
));
|
|
$instruction = trim((string) preg_replace('/\s+/u', ' ', $instruction));
|
|
|
|
$entry = [
|
|
'mode' => $mode,
|
|
'instruction' => $instruction,
|
|
'distance' => $s['distance']['text'] ?? '',
|
|
'duration' => $s['duration']['text'] ?? '',
|
|
];
|
|
|
|
if ($mode === 'TRANSIT' && ! empty($s['transit_details'])) {
|
|
$td = $s['transit_details'];
|
|
$line = $td['line'] ?? [];
|
|
$entry['transit'] = [
|
|
'line' => $line['short_name'] ?? ($line['name'] ?? ''),
|
|
'line_name' => $line['name'] ?? '',
|
|
'color' => $line['color'] ?? '#666',
|
|
'text_color' => $line['text_color'] ?? '#fff',
|
|
'vehicle' => $line['vehicle']['type'] ?? 'BUS',
|
|
'departure_stop' => $td['departure_stop']['name'] ?? '',
|
|
'arrival_stop' => $td['arrival_stop']['name'] ?? '',
|
|
'departure_time' => $td['departure_time']['text'] ?? '',
|
|
'arrival_time' => $td['arrival_time']['text'] ?? '',
|
|
'num_stops' => (int) ($td['num_stops'] ?? 0),
|
|
'headsign' => $td['headsign'] ?? '',
|
|
];
|
|
}
|
|
|
|
$out[] = $entry;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Google Polyline-Format → GeoJSON LineString-Feature.
|
|
* https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
|
*/
|
|
protected function decodePolyline(string $encoded): array
|
|
{
|
|
$points = [];
|
|
$index = $lat = $lng = 0;
|
|
$len = strlen($encoded);
|
|
|
|
while ($index < $len) {
|
|
$shift = $result = 0;
|
|
do {
|
|
$b = ord($encoded[$index++]) - 63;
|
|
$result |= ($b & 0x1f) << $shift;
|
|
$shift += 5;
|
|
} while ($b >= 0x20 && $index < $len);
|
|
$lat += ($result & 1) ? ~($result >> 1) : ($result >> 1);
|
|
|
|
$shift = $result = 0;
|
|
do {
|
|
$b = ord($encoded[$index++]) - 63;
|
|
$result |= ($b & 0x1f) << $shift;
|
|
$shift += 5;
|
|
} while ($b >= 0x20 && $index < $len);
|
|
$lng += ($result & 1) ? ~($result >> 1) : ($result >> 1);
|
|
|
|
$points[] = [$lng / 1e5, $lat / 1e5];
|
|
}
|
|
|
|
return [
|
|
'type' => 'Feature',
|
|
'geometry' => ['type' => 'LineString', 'coordinates' => $points],
|
|
'properties' => new \stdClass,
|
|
];
|
|
}
|
|
}
|