geocoder-php /
mapbox-provider
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | /* |
||
| 6 | * This file is part of the Geocoder package. |
||
| 7 | * For the full copyright and license information, please view the LICENSE |
||
| 8 | * file that was distributed with this source code. |
||
| 9 | * |
||
| 10 | * @license MIT License |
||
| 11 | */ |
||
| 12 | |||
| 13 | namespace Geocoder\Provider\Mapbox; |
||
| 14 | |||
| 15 | use Geocoder\Collection; |
||
| 16 | use Geocoder\Exception\InvalidArgument; |
||
| 17 | use Geocoder\Exception\InvalidServerResponse; |
||
| 18 | use Geocoder\Exception\UnsupportedOperation; |
||
| 19 | use Geocoder\Model\AddressCollection; |
||
| 20 | use Geocoder\Model\AddressBuilder; |
||
| 21 | use Geocoder\Query\GeocodeQuery; |
||
| 22 | use Geocoder\Query\ReverseQuery; |
||
| 23 | use Geocoder\Http\Provider\AbstractHttpProvider; |
||
| 24 | use Geocoder\Provider\Mapbox\Model\MapboxAddress; |
||
| 25 | use Geocoder\Provider\Provider; |
||
| 26 | use Http\Client\HttpClient; |
||
| 27 | |||
| 28 | final class Mapbox extends AbstractHttpProvider implements Provider |
||
| 29 | { |
||
| 30 | /** |
||
| 31 | * @var string |
||
| 32 | */ |
||
| 33 | const GEOCODE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%s.json'; |
||
| 34 | |||
| 35 | /** |
||
| 36 | * @var string |
||
| 37 | */ |
||
| 38 | const REVERSE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%F,%F.json'; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * @var string |
||
| 42 | */ |
||
| 43 | const GEOCODING_MODE_PLACES = 'mapbox.places'; |
||
| 44 | |||
| 45 | /** |
||
| 46 | * @var string |
||
| 47 | */ |
||
| 48 | const GEOCODING_MODE_PLACES_PERMANENT = 'mapbox.places-permanent'; |
||
| 49 | |||
| 50 | /** |
||
| 51 | * @var array |
||
| 52 | */ |
||
| 53 | const GEOCODING_MODES = [ |
||
| 54 | self::GEOCODING_MODE_PLACES, |
||
| 55 | self::GEOCODING_MODE_PLACES_PERMANENT, |
||
| 56 | ]; |
||
| 57 | |||
| 58 | /** |
||
| 59 | * @var string |
||
| 60 | */ |
||
| 61 | const TYPE_COUNTRY = 'country'; |
||
| 62 | |||
| 63 | /** |
||
| 64 | * @var string |
||
| 65 | */ |
||
| 66 | const TYPE_REGION = 'region'; |
||
| 67 | |||
| 68 | /** |
||
| 69 | * @var string |
||
| 70 | */ |
||
| 71 | const TYPE_POSTCODE = 'postcode'; |
||
| 72 | |||
| 73 | /** |
||
| 74 | * @var string |
||
| 75 | */ |
||
| 76 | const TYPE_DISTRICT = 'district'; |
||
| 77 | |||
| 78 | /** |
||
| 79 | * @var string |
||
| 80 | */ |
||
| 81 | const TYPE_PLACE = 'place'; |
||
| 82 | |||
| 83 | /** |
||
| 84 | * @var string |
||
| 85 | */ |
||
| 86 | const TYPE_LOCALITY = 'locality'; |
||
| 87 | |||
| 88 | /** |
||
| 89 | * @var string |
||
| 90 | */ |
||
| 91 | const TYPE_NEIGHBORHOOD = 'neighborhood'; |
||
| 92 | |||
| 93 | /** |
||
| 94 | * @var string |
||
| 95 | */ |
||
| 96 | const TYPE_ADDRESS = 'address'; |
||
| 97 | |||
| 98 | /** |
||
| 99 | * @var string |
||
| 100 | */ |
||
| 101 | const TYPE_POI = 'poi'; |
||
| 102 | |||
| 103 | /** |
||
| 104 | * @var string |
||
| 105 | */ |
||
| 106 | const TYPE_POI_LANDMARK = 'poi.landmark'; |
||
| 107 | |||
| 108 | /** |
||
| 109 | * @var array |
||
| 110 | */ |
||
| 111 | const TYPES = [ |
||
| 112 | self::TYPE_COUNTRY, |
||
| 113 | self::TYPE_REGION, |
||
| 114 | self::TYPE_POSTCODE, |
||
| 115 | self::TYPE_DISTRICT, |
||
| 116 | self::TYPE_PLACE, |
||
| 117 | self::TYPE_LOCALITY, |
||
| 118 | self::TYPE_NEIGHBORHOOD, |
||
| 119 | self::TYPE_ADDRESS, |
||
| 120 | self::TYPE_POI, |
||
| 121 | self::TYPE_POI_LANDMARK, |
||
| 122 | ]; |
||
| 123 | |||
| 124 | const DEFAULT_TYPE = self::TYPE_ADDRESS; |
||
| 125 | |||
| 126 | /** |
||
| 127 | * @var HttpClient |
||
| 128 | */ |
||
| 129 | private $client; |
||
| 130 | |||
| 131 | /** |
||
| 132 | * @var string |
||
| 133 | */ |
||
| 134 | private $accessToken; |
||
| 135 | |||
| 136 | /** |
||
| 137 | * @var string|null |
||
| 138 | */ |
||
| 139 | private $country; |
||
| 140 | |||
| 141 | /** |
||
| 142 | * @var string |
||
| 143 | */ |
||
| 144 | private $geocodingMode; |
||
| 145 | |||
| 146 | /** |
||
| 147 | * @param HttpClient $client An HTTP adapter |
||
| 148 | * @param string $accessToken Your Mapbox access token |
||
| 149 | * @param string|null $country |
||
| 150 | * @param string $geocodingMode |
||
| 151 | */ |
||
| 152 | public function __construct( |
||
| 153 | HttpClient $client, |
||
| 154 | string $accessToken, |
||
| 155 | string $country = null, |
||
| 156 | string $geocodingMode = self::GEOCODING_MODE_PLACES |
||
| 157 | ) { |
||
| 158 | parent::__construct($client); |
||
| 159 | |||
| 160 | if (!in_array($geocodingMode, self::GEOCODING_MODES)) { |
||
| 161 | throw new InvalidArgument('The Mapbox geocoding mode should be either mapbox.places or mapbox.places-permanent.'); |
||
| 162 | } |
||
| 163 | |||
| 164 | $this->client = $client; |
||
| 165 | $this->accessToken = $accessToken; |
||
| 166 | $this->country = $country; |
||
| 167 | $this->geocodingMode = $geocodingMode; |
||
| 168 | } |
||
| 169 | |||
| 170 | public function geocodeQuery(GeocodeQuery $query): Collection |
||
| 171 | { |
||
| 172 | // Mapbox API returns invalid data if IP address given |
||
| 173 | // This API doesn't handle IPs |
||
| 174 | if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { |
||
| 175 | throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.'); |
||
| 176 | } |
||
| 177 | |||
| 178 | $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->geocodingMode, rawurlencode($query->getText())); |
||
| 179 | |||
| 180 | $urlParameters = []; |
||
| 181 | if ($query->getBounds()) { |
||
| 182 | $urlParameters['bbox'] = sprintf( |
||
| 183 | '%s,%s|%s,%s', |
||
| 184 | $query->getBounds()->getWest(), |
||
| 185 | $query->getBounds()->getSouth(), |
||
| 186 | $query->getBounds()->getEast(), |
||
| 187 | $query->getBounds()->getNorth() |
||
| 188 | ); |
||
| 189 | } |
||
| 190 | |||
| 191 | if (null !== $locationType = $query->getData('location_type')) { |
||
| 192 | $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType; |
||
| 193 | } else { |
||
| 194 | $urlParameters['types'] = self::DEFAULT_TYPE; |
||
| 195 | } |
||
| 196 | |||
| 197 | if ($urlParameters) { |
||
| 198 | $url .= '?'.http_build_query($urlParameters); |
||
| 199 | } |
||
| 200 | |||
| 201 | return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country)); |
||
| 202 | } |
||
| 203 | |||
| 204 | public function reverseQuery(ReverseQuery $query): Collection |
||
| 205 | { |
||
| 206 | $coordinate = $query->getCoordinates(); |
||
| 207 | $url = sprintf( |
||
| 208 | self::REVERSE_ENDPOINT_URL_SSL, |
||
| 209 | $this->geocodingMode, |
||
| 210 | $coordinate->getLongitude(), |
||
| 211 | $coordinate->getLatitude() |
||
| 212 | ); |
||
| 213 | |||
| 214 | if (null !== $locationType = $query->getData('location_type')) { |
||
| 215 | $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType; |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
| 216 | } else { |
||
| 217 | $urlParameters['types'] = self::DEFAULT_TYPE; |
||
| 218 | } |
||
| 219 | |||
| 220 | if ($urlParameters) { |
||
| 221 | $url .= '?'.http_build_query($urlParameters); |
||
| 222 | } |
||
| 223 | |||
| 224 | return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country)); |
||
| 225 | } |
||
| 226 | |||
| 227 | /** |
||
| 228 | * {@inheritdoc} |
||
| 229 | */ |
||
| 230 | public function getName(): string |
||
| 231 | { |
||
| 232 | return 'mapbox'; |
||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * @param string $url |
||
| 237 | * @param int $limit |
||
| 238 | * @param string|null $locale |
||
| 239 | * @param string|null $country |
||
| 240 | * |
||
| 241 | * @return string query with extra params |
||
| 242 | */ |
||
| 243 | private function buildQuery(string $url, int $limit, string $locale = null, string $country = null): string |
||
| 244 | { |
||
| 245 | $parameters = array_filter([ |
||
| 246 | 'country' => $country, |
||
| 247 | 'language' => $locale, |
||
| 248 | 'limit' => $limit, |
||
| 249 | 'access_token' => $this->accessToken, |
||
| 250 | ]); |
||
| 251 | |||
| 252 | $separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?'; |
||
| 253 | |||
| 254 | return $url.$separator.http_build_query($parameters); |
||
| 255 | } |
||
| 256 | |||
| 257 | /** |
||
| 258 | * @param string $url |
||
| 259 | * @param int $limit |
||
| 260 | * @param string|null $locale |
||
| 261 | * @param string|null $country |
||
| 262 | * |
||
| 263 | * @return AddressCollection |
||
| 264 | */ |
||
| 265 | private function fetchUrl(string $url, int $limit, string $locale = null, string $country = null): AddressCollection |
||
| 266 | { |
||
| 267 | $url = $this->buildQuery($url, $limit, $locale, $country); |
||
| 268 | $content = $this->getUrlContents($url); |
||
| 269 | $json = $this->validateResponse($url, $content); |
||
| 270 | |||
| 271 | // no result |
||
| 272 | if (!isset($json['features']) || !count($json['features'])) { |
||
| 273 | return new AddressCollection([]); |
||
| 274 | } |
||
| 275 | |||
| 276 | $results = []; |
||
| 277 | foreach ($json['features'] as $result) { |
||
| 278 | if (!array_key_exists('context', $result)) { |
||
| 279 | break; |
||
| 280 | } |
||
| 281 | |||
| 282 | $builder = new AddressBuilder($this->getName()); |
||
| 283 | $this->parseCoordinates($builder, $result); |
||
| 284 | |||
| 285 | // set official Mapbox place id |
||
| 286 | if (isset($result['id'])) { |
||
| 287 | $builder->setValue('id', $result['id']); |
||
| 288 | } |
||
| 289 | |||
| 290 | // set official Mapbox place id |
||
| 291 | if (isset($result['text'])) { |
||
| 292 | $builder->setValue('street_name', $result['text']); |
||
| 293 | } |
||
| 294 | |||
| 295 | // update address components |
||
| 296 | foreach ($result['context'] as $component) { |
||
| 297 | $this->updateAddressComponent($builder, $component['id'], $component); |
||
| 298 | } |
||
| 299 | |||
| 300 | /** @var MapboxAddress $address */ |
||
| 301 | $address = $builder->build(MapboxAddress::class); |
||
| 302 | $address = $address->withId($builder->getValue('id')); |
||
| 303 | if (isset($result['address'])) { |
||
| 304 | $address = $address->withStreetNumber($result['address']); |
||
| 305 | } |
||
| 306 | if (isset($result['place_type'])) { |
||
| 307 | $address = $address->withResultType($result['place_type']); |
||
| 308 | } |
||
| 309 | if (isset($result['place_name'])) { |
||
| 310 | $address = $address->withFormattedAddress($result['place_name']); |
||
| 311 | } |
||
| 312 | $address = $address->withStreetName($builder->getValue('street_name')); |
||
| 313 | $address = $address->withNeighborhood($builder->getValue('neighborhood')); |
||
| 314 | $results[] = $address; |
||
| 315 | |||
| 316 | if (count($results) >= $limit) { |
||
| 317 | break; |
||
| 318 | } |
||
| 319 | } |
||
| 320 | |||
| 321 | return new AddressCollection($results); |
||
| 322 | } |
||
| 323 | |||
| 324 | /** |
||
| 325 | * Update current resultSet with given key/value. |
||
| 326 | * |
||
| 327 | * @param AddressBuilder $builder |
||
| 328 | * @param string $type Component type |
||
| 329 | * @param array $value The component value |
||
| 330 | */ |
||
| 331 | private function updateAddressComponent(AddressBuilder $builder, string $type, array $value) |
||
| 332 | { |
||
| 333 | $typeParts = explode('.', $type); |
||
| 334 | $type = reset($typeParts); |
||
| 335 | |||
| 336 | switch ($type) { |
||
| 337 | case 'postcode': |
||
| 338 | $builder->setPostalCode($value['text']); |
||
| 339 | |||
| 340 | break; |
||
| 341 | |||
| 342 | case 'locality': |
||
| 343 | $builder->setLocality($value['text']); |
||
| 344 | |||
| 345 | break; |
||
| 346 | |||
| 347 | case 'country': |
||
| 348 | $builder->setCountry($value['text']); |
||
| 349 | $builder->setCountryCode(strtoupper($value['short_code'])); |
||
| 350 | |||
| 351 | break; |
||
| 352 | |||
| 353 | case 'neighborhood': |
||
| 354 | $builder->setValue($type, $value['text']); |
||
| 355 | |||
| 356 | break; |
||
| 357 | |||
| 358 | case 'place': |
||
| 359 | $builder->addAdminLevel(1, $value['text']); |
||
| 360 | $builder->setLocality($value['text']); |
||
| 361 | |||
| 362 | break; |
||
| 363 | |||
| 364 | case 'region': |
||
| 365 | $code = null; |
||
| 366 | if (!empty($value['short_code']) && preg_match('/[A-z]{2}-/', $value['short_code'])) { |
||
| 367 | $code = preg_replace('/[A-z]{2}-/', '', $value['short_code']); |
||
| 368 | } |
||
| 369 | $builder->addAdminLevel(2, $value['text'], $code); |
||
| 370 | |||
| 371 | break; |
||
| 372 | |||
| 373 | default: |
||
| 374 | } |
||
| 375 | } |
||
| 376 | |||
| 377 | /** |
||
| 378 | * Decode the response content and validate it to make sure it does not have any errors. |
||
| 379 | * |
||
| 380 | * @param string $url |
||
| 381 | * @param string $content |
||
| 382 | * |
||
| 383 | * @return array |
||
| 384 | */ |
||
| 385 | private function validateResponse(string $url, $content): array |
||
| 386 | { |
||
| 387 | $json = json_decode($content, true); |
||
| 388 | |||
| 389 | // API error |
||
| 390 | if (!isset($json) || JSON_ERROR_NONE !== json_last_error()) { |
||
| 391 | throw InvalidServerResponse::create($url); |
||
| 392 | } |
||
| 393 | |||
| 394 | return $json; |
||
| 395 | } |
||
| 396 | |||
| 397 | /** |
||
| 398 | * Parse coordinats and bounds. |
||
| 399 | * |
||
| 400 | * @param AddressBuilder $builder |
||
| 401 | * @param array $result |
||
| 402 | */ |
||
| 403 | private function parseCoordinates(AddressBuilder $builder, array $result) |
||
| 404 | { |
||
| 405 | $coordinates = $result['geometry']['coordinates']; |
||
| 406 | $builder->setCoordinates($coordinates[1], $coordinates[0]); |
||
| 407 | |||
| 408 | if (isset($result['bbox'])) { |
||
| 409 | $builder->setBounds( |
||
| 410 | $result['bbox'][1], |
||
| 411 | $result['bbox'][0], |
||
| 412 | $result['bbox'][3], |
||
| 413 | $result['bbox'][2] |
||
| 414 | ); |
||
| 415 | } |
||
| 416 | } |
||
| 417 | } |
||
| 418 |