123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of the Geocoder package.
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- *
- * @license MIT License
- */
- namespace Geocoder\Provider\MapQuest;
- use Geocoder\Collection;
- use Geocoder\Exception\InvalidArgument;
- use Geocoder\Exception\InvalidCredentials;
- use Geocoder\Exception\InvalidServerResponse;
- use Geocoder\Exception\QuotaExceeded;
- use Geocoder\Exception\UnsupportedOperation;
- use Geocoder\Http\Provider\AbstractHttpProvider;
- use Geocoder\Location;
- use Geocoder\Model\Address;
- use Geocoder\Model\AddressCollection;
- use Geocoder\Model\AdminLevel;
- use Geocoder\Model\Bounds;
- use Geocoder\Model\Country;
- use Geocoder\Query\GeocodeQuery;
- use Geocoder\Query\ReverseQuery;
- use Geocoder\Provider\Provider;
- use Http\Client\HttpClient;
- use Psr\Http\Message\ResponseInterface;
- /**
- * @author William Durand <william.durand1@gmail.com>
- */
- final class MapQuest extends AbstractHttpProvider implements Provider
- {
- const DATA_KEY_ADDRESS = 'address';
- const KEY_API_KEY = 'key';
- const KEY_LOCATION = 'location';
- const KEY_OUT_FORMAT = 'outFormat';
- const KEY_MAX_RESULTS = 'maxResults';
- const KEY_THUMB_MAPS = 'thumbMaps';
- const KEY_INTL_MODE = 'intlMode';
- const KEY_BOUNDING_BOX = 'boundingBox';
- const KEY_LAT = 'lat';
- const KEY_LNG = 'lng';
- const MODE_5BOX = '5BOX';
- const OPEN_BASE_URL = 'https://open.mapquestapi.com/geocoding/v1/';
- const LICENSED_BASE_URL = 'https://www.mapquestapi.com/geocoding/v1/';
- const GEOCODE_ENDPOINT = 'address';
- const DEFAULT_GEOCODE_PARAMS = [
- self::KEY_LOCATION => '',
- self::KEY_OUT_FORMAT => 'json',
- self::KEY_API_KEY => '',
- ];
- const DEFAULT_GEOCODE_OPTIONS = [
- self::KEY_MAX_RESULTS => 3,
- self::KEY_THUMB_MAPS => false,
- ];
- const REVERSE_ENDPOINT = 'reverse';
- const ADMIN_LEVEL_STATE = 1;
- const ADMIN_LEVEL_COUNTY = 2;
- /**
- * MapQuest offers two geocoding endpoints one commercial (true) and one open (false)
- * More information: http://developer.mapquest.com/web/tools/getting-started/platform/licensed-vs-open.
- *
- * @var bool
- */
- private $licensed;
- /**
- * @var bool
- */
- private $useRoadPosition;
- /**
- * @var string
- */
- private $apiKey;
- /**
- * @param HttpClient $client an HTTP adapter
- * @param string $apiKey an API key
- * @param bool $licensed true to use MapQuest's licensed endpoints, default is false to use the open endpoints (optional)
- * @param bool $useRoadPosition true to use nearest point on a road for the entrance, false to use map display position
- */
- public function __construct(HttpClient $client, string $apiKey, bool $licensed = false, bool $useRoadPosition = false)
- {
- if (empty($apiKey)) {
- throw new InvalidCredentials('No API key provided.');
- }
- $this->apiKey = $apiKey;
- $this->licensed = $licensed;
- $this->useRoadPosition = $useRoadPosition;
- parent::__construct($client);
- }
- /**
- * {@inheritdoc}
- */
- public function geocodeQuery(GeocodeQuery $query): Collection
- {
- $params = static::DEFAULT_GEOCODE_PARAMS;
- $params[static::KEY_API_KEY] = $this->apiKey;
- $options = static::DEFAULT_GEOCODE_OPTIONS;
- $options[static::KEY_MAX_RESULTS] = $query->getLimit();
- $useGetQuery = true;
- $address = $this->extractAddressFromQuery($query);
- if ($address instanceof Location) {
- $params[static::KEY_LOCATION] = $this->mapAddressToArray($address);
- $options[static::KEY_INTL_MODE] = static::MODE_5BOX;
- $useGetQuery = false;
- } else {
- $addressAsText = $query->getText();
- if (!$addressAsText) {
- throw new InvalidArgument('Cannot geocode empty address');
- }
- // This API doesn't handle IPs
- if (filter_var($addressAsText, FILTER_VALIDATE_IP)) {
- throw new UnsupportedOperation('The MapQuest provider does not support IP addresses, only street addresses.');
- }
- $params[static::KEY_LOCATION] = $addressAsText;
- }
- $bounds = $query->getBounds();
- if ($bounds instanceof Bounds) {
- $options[static::KEY_BOUNDING_BOX] = $this->mapBoundsToArray($bounds);
- $useGetQuery = false;
- }
- if ($useGetQuery) {
- $params = $this->addOptionsForGetQuery($params, $options);
- return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params);
- } else {
- $params = $this->addOptionsForPostQuery($params, $options);
- return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function reverseQuery(ReverseQuery $query): Collection
- {
- $coordinates = $query->getCoordinates();
- $longitude = $coordinates->getLongitude();
- $latitude = $coordinates->getLatitude();
- $params = [
- static::KEY_API_KEY => $this->apiKey,
- static::KEY_LAT => $latitude,
- static::KEY_LNG => $longitude,
- ];
- return $this->executeGetQuery(static::REVERSE_ENDPOINT, $params);
- }
- /**
- * {@inheritdoc}
- */
- public function getName(): string
- {
- return 'map_quest';
- }
- private function extractAddressFromQuery(GeocodeQuery $query)
- {
- return $query->getData(static::DATA_KEY_ADDRESS);
- }
- private function getUrl($endpoint): string
- {
- if ($this->licensed) {
- $baseUrl = static::LICENSED_BASE_URL;
- } else {
- $baseUrl = static::OPEN_BASE_URL;
- }
- return $baseUrl.$endpoint;
- }
- private function addGetQuery(string $url, array $params): string
- {
- return $url.'?'.http_build_query($params, '', '&', PHP_QUERY_RFC3986);
- }
- private function addOptionsForGetQuery(array $params, array $options): array
- {
- foreach ($options as $key => $value) {
- if (false === $value) {
- $value = 'false';
- } elseif (true === $value) {
- $value = 'true';
- }
- $params[$key] = $value;
- }
- return $params;
- }
- private function addOptionsForPostQuery(array $params, array $options): array
- {
- $params['options'] = $options;
- return $params;
- }
- private function executePostQuery(string $endpoint, array $params)
- {
- $url = $this->getUrl($endpoint);
- $appKey = $params[static::KEY_API_KEY];
- unset($params[static::KEY_API_KEY]);
- $url .= '?key='.$appKey;
- $requestBody = json_encode($params);
- $request = $this->getMessageFactory()->createRequest('POST', $url, [], $requestBody);
- $response = $this->getHttpClient()->sendRequest($request);
- $content = $this->parseHttpResponse($response, $url);
- return $this->parseResponseContent($content);
- }
- /**
- * @param string $url
- *
- * @return AddressCollection
- */
- private function executeGetQuery(string $endpoint, array $params): AddressCollection
- {
- $baseUrl = $this->getUrl($endpoint);
- $url = $this->addGetQuery($baseUrl, $params);
- $content = $this->getUrlContents($url);
- return $this->parseResponseContent($content);
- }
- private function parseResponseContent(string $content): AddressCollection
- {
- $json = json_decode($content, true);
- if (!isset($json['results']) || empty($json['results'])) {
- return new AddressCollection([]);
- }
- $locations = $json['results'][0]['locations'];
- if (empty($locations)) {
- return new AddressCollection([]);
- }
- $results = [];
- foreach ($locations as $location) {
- if ($location['street'] || $location['postalCode'] || $location['adminArea5'] || $location['adminArea4'] || $location['adminArea3']) {
- $admins = [];
- $state = $location['adminArea3'];
- if ($state) {
- $code = null;
- if (2 == strlen($state)) {
- $code = $state;
- }
- $admins[] = [
- 'name' => $state,
- 'code' => $code,
- 'level' => static::ADMIN_LEVEL_STATE,
- ];
- }
- if ($location['adminArea4']) {
- $admins[] = ['name' => $location['adminArea4'], 'level' => static::ADMIN_LEVEL_COUNTY];
- }
- $position = $location['latLng'];
- if (!$this->useRoadPosition) {
- if ($location['displayLatLng']) {
- $position = $location['displayLatLng'];
- }
- }
- $results[] = Address::createFromArray([
- 'providedBy' => $this->getName(),
- 'latitude' => $position['lat'],
- 'longitude' => $position['lng'],
- 'streetName' => $location['street'] ?: null,
- 'locality' => $location['adminArea5'] ?: null,
- 'subLocality' => $location['adminArea6'] ?: null,
- 'postalCode' => $location['postalCode'] ?: null,
- 'adminLevels' => $admins,
- 'country' => $location['adminArea1'] ?: null,
- 'countryCode' => $location['adminArea1'] ?: null,
- ]);
- }
- }
- return new AddressCollection($results);
- }
- private function mapAddressToArray(Location $address): array
- {
- $location = [];
- $streetParts = [
- trim($address->getStreetNumber() ?: ''),
- trim($address->getStreetName() ?: ''),
- ];
- $street = implode(' ', array_filter($streetParts));
- if ($street) {
- $location['street'] = $street;
- }
- if ($address->getSubLocality()) {
- $location['adminArea6'] = $address->getSubLocality();
- $location['adminArea6Type'] = 'Neighborhood';
- }
- if ($address->getLocality()) {
- $location['adminArea5'] = $address->getLocality();
- $location['adminArea5Type'] = 'City';
- }
- /** @var AdminLevel $adminLevel */
- foreach ($address->getAdminLevels() as $adminLevel) {
- switch ($adminLevel->getLevel()) {
- case static::ADMIN_LEVEL_STATE:
- $state = $adminLevel->getCode();
- if (!$state) {
- $state = $adminLevel->getName();
- }
- $location['adminArea3'] = $state;
- $location['adminArea3Type'] = 'State';
- break;
- case static::ADMIN_LEVEL_COUNTY:
- $county = $adminLevel->getName();
- $location['adminArea4'] = $county;
- $location['adminArea4Type'] = 'County';
- }
- }
- $country = $address->getCountry();
- if ($country instanceof Country) {
- $code = $country->getCode();
- if (!$code) {
- $code = $country->getName();
- }
- $location['adminArea1'] = $code;
- $location['adminArea1Type'] = 'Country';
- }
- $postalCode = $address->getPostalCode();
- if ($postalCode) {
- $location['postalCode'] = $address->getPostalCode();
- }
- return $location;
- }
- private function mapBoundsToArray(Bounds $bounds)
- {
- return [
- 'ul' => [static::KEY_LAT => $bounds->getNorth(), static::KEY_LNG => $bounds->getWest()],
- 'lr' => [static::KEY_LAT => $bounds->getSouth(), static::KEY_LNG => $bounds->getEast()],
- ];
- }
- protected function parseHttpResponse(ResponseInterface $response, string $url): string
- {
- $statusCode = $response->getStatusCode();
- if (401 === $statusCode || 403 === $statusCode) {
- throw new InvalidCredentials();
- } elseif (429 === $statusCode) {
- throw new QuotaExceeded();
- } elseif ($statusCode >= 300) {
- throw InvalidServerResponse::create($url, $statusCode);
- }
- $body = (string) $response->getBody();
- if (empty($body)) {
- throw InvalidServerResponse::emptyResponse($url);
- }
- return $body;
- }
- }
|