fox/app/Services/RouteService.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,
];
}
}