fox/app/Services/PlacesService.php

107 lines
3.9 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Google Places (New) - Nearby Search.
* Wird als Fallback aufgerufen wenn Overpass-Mirrors alle tot sind.
* https://developers.google.com/maps/documentation/places/web-service/nearby-search
*/
class PlacesService
{
/**
* Map unsere internen Kategorien auf Google-Places-Types.
* Multi-Type möglich, Google nimmt OR-Logik.
*/
protected const CATEGORY_TYPES = [
'restaurant' => ['restaurant'],
'cafe' => ['cafe'],
'bar' => ['bar', 'night_club'],
'fast_food' => ['fast_food_restaurant', 'meal_takeaway'],
'pharmacy' => ['pharmacy'],
'hospital' => ['hospital'],
'doctor' => ['doctor'],
'bank' => ['bank', 'atm'],
'fuel' => ['gas_station'],
'supermarket' => ['supermarket'],
'hotel' => ['hotel', 'lodging'],
'government' => ['city_hall', 'government_office'],
'police' => ['police'],
'parking' => ['parking'],
];
public function nearby(string $category, float $lat, float $lng, int $radius = 2000, ?string $cuisine = null): array
{
$key = config('services.google.maps_key');
if (empty($key)) {
Log::warning('Places: kein Google-Key, Fallback nicht möglich');
return [];
}
$types = self::CATEGORY_TYPES[$category] ?? ['restaurant'];
try {
$response = Http::timeout(10)
->withHeaders([
'Content-Type' => 'application/json',
'X-Goog-Api-Key' => $key,
'X-Goog-FieldMask' => 'places.id,places.displayName,places.formattedAddress,places.location,places.types,places.rating',
])
->post('https://places.googleapis.com/v1/places:searchNearby', [
'includedTypes' => $types,
'maxResultCount' => 20,
'languageCode' => 'de',
'regionCode' => 'AT',
'locationRestriction' => [
'circle' => [
'center' => ['latitude' => $lat, 'longitude' => $lng],
'radius' => min((float) $radius, 50_000.0),
],
],
]);
if (! $response->successful()) {
Log::warning('Places API HTTP-Fehler', [
'status' => $response->status(),
'body' => mb_substr($response->body(), 0, 200),
]);
return [];
}
$places = $response->json('places') ?? [];
// Cuisine-Filter clientseitig: Google hat keine cuisine-Tags, aber wir
// können nach displayName + types filtern (z.B. "steak" enthalten).
if ($cuisine !== null) {
$needle = mb_strtolower($cuisine);
$places = array_values(array_filter($places, function ($p) use ($needle) {
$name = mb_strtolower($p['displayName']['text'] ?? '');
return str_contains($name, $needle) || str_contains($name, str_replace('_', ' ', $needle));
}));
}
return array_map(function ($p) use ($category) {
return [
'id' => 'gp-'.($p['id'] ?? uniqid()),
'name' => $p['displayName']['text'] ?? '?',
'address' => $p['formattedAddress'] ?? '',
'lat' => $p['location']['latitude'] ?? null,
'lng' => $p['location']['longitude'] ?? null,
'rating' => $p['rating'] ?? null,
'category' => $category,
];
}, $places);
} catch (\Throwable $e) {
Log::warning('Places exception', ['e' => $e->getMessage()]);
return [];
}
}
}