163 lines
4.9 KiB
PHP
163 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Location;
|
|
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class Geocoder
|
|
{
|
|
public function __construct(
|
|
private array $cfg = []
|
|
) {
|
|
$this->cfg = config('services.geocoding');
|
|
}
|
|
|
|
/**
|
|
* Forward geocode a free-form string.
|
|
* Returns a normalized array for your `locations` table or null.
|
|
*/
|
|
public function forward(string $query): ?array
|
|
{
|
|
$provider = $this->cfg['provider'] ?? 'arcgis';
|
|
|
|
// Treat obvious non-address labels as non-geocodable (e.g., "Home", "Office")
|
|
if (mb_strlen(trim($query)) < 4) {
|
|
return null;
|
|
}
|
|
|
|
return match ($provider) {
|
|
'arcgis' => $this->forwardArcgis($query),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reverse geocode lon/lat → address. (Optional)
|
|
*/
|
|
public function reverse(float $lat, float $lon): ?array
|
|
{
|
|
$provider = $this->cfg['provider'] ?? 'arcgis';
|
|
return match ($provider) {
|
|
'arcgis' => $this->reverseArcgis($lat, $lon),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/* ---------------- ArcGIS World Geocoding ---------------- */
|
|
|
|
private function http()
|
|
{
|
|
return Http::retry(3, 500)
|
|
->timeout((int) ($this->cfg['timeout'] ?? 20))
|
|
->withHeaders([
|
|
'User-Agent' => $this->cfg['user_agent'] ?? 'Kithkin/Geocoder',
|
|
]);
|
|
}
|
|
|
|
private function arcgisBase(): string
|
|
{
|
|
return rtrim($this->cfg['arcgis']['endpoint'], '/');
|
|
}
|
|
|
|
private function forwardArcgis(string $query): ?array
|
|
{
|
|
$params = [
|
|
'singleLine' => $query,
|
|
'outFields' => $this->cfg['arcgis']['out_fields'] ?? '*',
|
|
'maxLocations'=> $this->cfg['arcgis']['max_results'] ?? 1,
|
|
'f' => 'pjson',
|
|
'token' => $this->cfg['arcgis']['api_key'],
|
|
];
|
|
|
|
// If your plan permits/requests it:
|
|
if (!empty($this->cfg['arcgis']['store'])) {
|
|
$params['forStorage'] = 'true';
|
|
}
|
|
|
|
$res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params);
|
|
|
|
if (!$res->ok()) {
|
|
Log::warning('ArcGIS forward geocode failed', ['status' => $res->status(), 'q' => $query]);
|
|
return null;
|
|
}
|
|
|
|
$json = $res->json();
|
|
$cand = Arr::first($json['candidates'] ?? []);
|
|
if (!$cand) {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeArcgisCandidate($cand, $query);
|
|
}
|
|
|
|
private function reverseArcgis(float $lat, float $lon): ?array
|
|
{
|
|
$params = [
|
|
// ArcGIS expects x=lon, y=lat
|
|
'location' => "{$lon},{$lat}",
|
|
'f' => 'pjson',
|
|
'token' => $this->cfg['arcgis']['api_key'],
|
|
];
|
|
|
|
if (!empty($this->cfg['arcgis']['store'])) {
|
|
$params['forStorage'] = 'true';
|
|
}
|
|
|
|
$res = $this->http()->get($this->arcgisBase().'/reverseGeocode', $params);
|
|
|
|
if (!$res->ok()) {
|
|
Log::warning('ArcGIS reverse geocode failed', ['status' => $res->status()]);
|
|
return null;
|
|
}
|
|
|
|
$j = $res->json();
|
|
$addr = $j['address'] ?? null;
|
|
if (!$addr) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'display_name' => $addr['LongLabel'] ?? $addr['Match_addr'] ?? null,
|
|
'raw_address' => $addr['Match_addr'] ?? null,
|
|
'street' => $addr['Address'] ?? null,
|
|
'city' => $addr['City'] ?? null,
|
|
'state' => $addr['Region'] ?? null,
|
|
'postal' => $addr['Postal'] ?? null,
|
|
'country' => $addr['CountryCode'] ?? null,
|
|
// reverseGeocode returns location as x/y too:
|
|
'lat' => Arr::get($j, 'location.y'),
|
|
'lon' => Arr::get($j, 'location.x'),
|
|
];
|
|
}
|
|
|
|
private function normalizeArcgisCandidate(array $c, string $query): array
|
|
{
|
|
$loc = $c['location'] ?? [];
|
|
$attr = $c['attributes'] ?? [];
|
|
|
|
// Prefer LongLabel → address → Place_addr
|
|
$display = $attr['LongLabel']
|
|
?? $c['address']
|
|
?? $attr['Place_addr']
|
|
?? $query;
|
|
|
|
// ArcGIS often returns both 'Address' and 'Place_addr'; use either.
|
|
$street = $attr['Address'] ?? $attr['Place_addr'] ?? null;
|
|
|
|
return [
|
|
'display_name' => $display,
|
|
'raw_address' => $query,
|
|
'street' => $street,
|
|
'city' => $attr['City'] ?? null,
|
|
'state' => $attr['Region'] ?? null,
|
|
'postal' => $attr['Postal'] ?? null,
|
|
'country' => $attr['CountryCode'] ?? null,
|
|
// location.x = lon, location.y = lat
|
|
'lat' => $loc['y'] ?? null,
|
|
'lon' => $loc['x'] ?? null,
|
|
];
|
|
}
|
|
}
|