fox/app/Http/Controllers/FoxGeocodeController.php

142 lines
4.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Forward-Geocoding via Google: "El Gaucho Wien" → lat/lng + sauberer Adresse.
* Wird genutzt wenn der LLM einen Ort nennt der nicht in der aktuellen POI-Liste ist.
*/
class FoxGeocodeController
{
public function __invoke(Request $request): JsonResponse
{
$request->validate([
'query' => 'required|string|max:200',
'bias_lat' => 'nullable|numeric',
'bias_lng' => 'nullable|numeric',
]);
$key = config('services.google.maps_key');
if (empty($key)) {
return response()->json(['error' => 'Geocoding nicht konfiguriert'], 503);
}
$query = $request->string('query')->toString();
$biasLat = $request->filled('bias_lat') ? (float) $request->input('bias_lat') : null;
$biasLng = $request->filled('bias_lng') ? (float) $request->input('bias_lng') : null;
// 1. Versuch: Places Text Search - findet POIs anhand des Namens
$hit = $this->tryPlacesText($query, $biasLat, $biasLng, $key);
if ($hit) {
return response()->json($hit);
}
// 2. Fallback: Forward Geocoding - falls's eine Adresse ist
$hit = $this->tryGeocode($query, $biasLat, $biasLng, $key);
if ($hit) {
return response()->json($hit);
}
return response()->json(['error' => 'not_found'], 404);
}
protected function tryPlacesText(string $query, ?float $lat, ?float $lng, string $key): ?array
{
try {
$body = [
'textQuery' => $query,
'languageCode' => 'de',
'regionCode' => 'AT',
'maxResultCount' => 1,
];
if ($lat !== null && $lng !== null) {
$body['locationBias'] = [
'circle' => [
'center' => ['latitude' => $lat, 'longitude' => $lng],
'radius' => 20_000.0,
],
];
}
$response = Http::timeout(8)
->withHeaders([
'Content-Type' => 'application/json',
'X-Goog-Api-Key' => $key,
'X-Goog-FieldMask' => 'places.id,places.displayName,places.formattedAddress,places.location',
])
->post('https://places.googleapis.com/v1/places:searchText', $body);
if (! $response->successful()) {
Log::warning('Places-Text HTTP-Fehler', ['status' => $response->status(), 'body' => mb_substr($response->body(), 0, 200)]);
return null;
}
$place = $response->json('places.0');
if (! $place) {
return null;
}
return [
'name' => $place['displayName']['text'] ?? null,
'address' => $place['formattedAddress'] ?? '',
'lat' => $place['location']['latitude'] ?? null,
'lng' => $place['location']['longitude'] ?? null,
'place_id' => $place['id'] ?? null,
'source' => 'places_text',
];
} catch (\Throwable $e) {
Log::warning('Places-Text exception', ['e' => $e->getMessage()]);
return null;
}
}
protected function tryGeocode(string $query, ?float $lat, ?float $lng, string $key): ?array
{
$params = [
'address' => $query,
'language' => 'de',
'region' => 'at',
'key' => $key,
];
if ($lat !== null && $lng !== null) {
$params['bounds'] = sprintf('%f,%f|%f,%f', $lat - 0.1, $lng - 0.15, $lat + 0.1, $lng + 0.15);
}
try {
$response = Http::timeout(8)
->get('https://maps.googleapis.com/maps/api/geocode/json', $params);
if (! $response->successful()) {
return null;
}
$data = $response->json();
if (($data['status'] ?? null) !== 'OK' || empty($data['results'])) {
return null;
}
$r = $data['results'][0];
return [
'name' => $r['address_components'][0]['long_name'] ?? null,
'address' => $r['formatted_address'] ?? '',
'lat' => $r['geometry']['location']['lat'] ?? null,
'lng' => $r['geometry']['location']['lng'] ?? null,
'place_id' => $r['place_id'] ?? null,
'source' => 'geocode',
];
} catch (\Throwable $e) {
Log::warning('Geocode exception', ['e' => $e->getMessage()]);
return null;
}
}
}