MapQuest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the Geocoder package.
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. *
  8. * @license MIT License
  9. */
  10. namespace Geocoder\Provider\MapQuest;
  11. use Geocoder\Collection;
  12. use Geocoder\Exception\InvalidArgument;
  13. use Geocoder\Exception\InvalidCredentials;
  14. use Geocoder\Exception\InvalidServerResponse;
  15. use Geocoder\Exception\QuotaExceeded;
  16. use Geocoder\Exception\UnsupportedOperation;
  17. use Geocoder\Http\Provider\AbstractHttpProvider;
  18. use Geocoder\Location;
  19. use Geocoder\Model\Address;
  20. use Geocoder\Model\AddressCollection;
  21. use Geocoder\Model\AdminLevel;
  22. use Geocoder\Model\Bounds;
  23. use Geocoder\Model\Country;
  24. use Geocoder\Query\GeocodeQuery;
  25. use Geocoder\Query\ReverseQuery;
  26. use Geocoder\Provider\Provider;
  27. use Http\Client\HttpClient;
  28. use Psr\Http\Message\ResponseInterface;
  29. /**
  30. * @author William Durand <william.durand1@gmail.com>
  31. */
  32. final class MapQuest extends AbstractHttpProvider implements Provider
  33. {
  34. const DATA_KEY_ADDRESS = 'address';
  35. const KEY_API_KEY = 'key';
  36. const KEY_LOCATION = 'location';
  37. const KEY_OUT_FORMAT = 'outFormat';
  38. const KEY_MAX_RESULTS = 'maxResults';
  39. const KEY_THUMB_MAPS = 'thumbMaps';
  40. const KEY_INTL_MODE = 'intlMode';
  41. const KEY_BOUNDING_BOX = 'boundingBox';
  42. const KEY_LAT = 'lat';
  43. const KEY_LNG = 'lng';
  44. const MODE_5BOX = '5BOX';
  45. const OPEN_BASE_URL = 'https://open.mapquestapi.com/geocoding/v1/';
  46. const LICENSED_BASE_URL = 'https://www.mapquestapi.com/geocoding/v1/';
  47. const GEOCODE_ENDPOINT = 'address';
  48. const DEFAULT_GEOCODE_PARAMS = [
  49. self::KEY_LOCATION => '',
  50. self::KEY_OUT_FORMAT => 'json',
  51. self::KEY_API_KEY => '',
  52. ];
  53. const DEFAULT_GEOCODE_OPTIONS = [
  54. self::KEY_MAX_RESULTS => 3,
  55. self::KEY_THUMB_MAPS => false,
  56. ];
  57. const REVERSE_ENDPOINT = 'reverse';
  58. const ADMIN_LEVEL_STATE = 1;
  59. const ADMIN_LEVEL_COUNTY = 2;
  60. /**
  61. * MapQuest offers two geocoding endpoints one commercial (true) and one open (false)
  62. * More information: http://developer.mapquest.com/web/tools/getting-started/platform/licensed-vs-open.
  63. *
  64. * @var bool
  65. */
  66. private $licensed;
  67. /**
  68. * @var bool
  69. */
  70. private $useRoadPosition;
  71. /**
  72. * @var string
  73. */
  74. private $apiKey;
  75. /**
  76. * @param HttpClient $client an HTTP adapter
  77. * @param string $apiKey an API key
  78. * @param bool $licensed true to use MapQuest's licensed endpoints, default is false to use the open endpoints (optional)
  79. * @param bool $useRoadPosition true to use nearest point on a road for the entrance, false to use map display position
  80. */
  81. public function __construct(HttpClient $client, string $apiKey, bool $licensed = false, bool $useRoadPosition = false)
  82. {
  83. if (empty($apiKey)) {
  84. throw new InvalidCredentials('No API key provided.');
  85. }
  86. $this->apiKey = $apiKey;
  87. $this->licensed = $licensed;
  88. $this->useRoadPosition = $useRoadPosition;
  89. parent::__construct($client);
  90. }
  91. /**
  92. * {@inheritdoc}
  93. */
  94. public function geocodeQuery(GeocodeQuery $query): Collection
  95. {
  96. $params = static::DEFAULT_GEOCODE_PARAMS;
  97. $params[static::KEY_API_KEY] = $this->apiKey;
  98. $options = static::DEFAULT_GEOCODE_OPTIONS;
  99. $options[static::KEY_MAX_RESULTS] = $query->getLimit();
  100. $useGetQuery = true;
  101. $address = $this->extractAddressFromQuery($query);
  102. if ($address instanceof Location) {
  103. $params[static::KEY_LOCATION] = $this->mapAddressToArray($address);
  104. $options[static::KEY_INTL_MODE] = static::MODE_5BOX;
  105. $useGetQuery = false;
  106. } else {
  107. $addressAsText = $query->getText();
  108. if (!$addressAsText) {
  109. throw new InvalidArgument('Cannot geocode empty address');
  110. }
  111. // This API doesn't handle IPs
  112. if (filter_var($addressAsText, FILTER_VALIDATE_IP)) {
  113. throw new UnsupportedOperation('The MapQuest provider does not support IP addresses, only street addresses.');
  114. }
  115. $params[static::KEY_LOCATION] = $addressAsText;
  116. }
  117. $bounds = $query->getBounds();
  118. if ($bounds instanceof Bounds) {
  119. $options[static::KEY_BOUNDING_BOX] = $this->mapBoundsToArray($bounds);
  120. $useGetQuery = false;
  121. }
  122. if ($useGetQuery) {
  123. $params = $this->addOptionsForGetQuery($params, $options);
  124. return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params);
  125. } else {
  126. $params = $this->addOptionsForPostQuery($params, $options);
  127. return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params);
  128. }
  129. }
  130. /**
  131. * {@inheritdoc}
  132. */
  133. public function reverseQuery(ReverseQuery $query): Collection
  134. {
  135. $coordinates = $query->getCoordinates();
  136. $longitude = $coordinates->getLongitude();
  137. $latitude = $coordinates->getLatitude();
  138. $params = [
  139. static::KEY_API_KEY => $this->apiKey,
  140. static::KEY_LAT => $latitude,
  141. static::KEY_LNG => $longitude,
  142. ];
  143. return $this->executeGetQuery(static::REVERSE_ENDPOINT, $params);
  144. }
  145. /**
  146. * {@inheritdoc}
  147. */
  148. public function getName(): string
  149. {
  150. return 'map_quest';
  151. }
  152. private function extractAddressFromQuery(GeocodeQuery $query)
  153. {
  154. return $query->getData(static::DATA_KEY_ADDRESS);
  155. }
  156. private function getUrl($endpoint): string
  157. {
  158. if ($this->licensed) {
  159. $baseUrl = static::LICENSED_BASE_URL;
  160. } else {
  161. $baseUrl = static::OPEN_BASE_URL;
  162. }
  163. return $baseUrl.$endpoint;
  164. }
  165. private function addGetQuery(string $url, array $params): string
  166. {
  167. return $url.'?'.http_build_query($params, '', '&', PHP_QUERY_RFC3986);
  168. }
  169. private function addOptionsForGetQuery(array $params, array $options): array
  170. {
  171. foreach ($options as $key => $value) {
  172. if (false === $value) {
  173. $value = 'false';
  174. } elseif (true === $value) {
  175. $value = 'true';
  176. }
  177. $params[$key] = $value;
  178. }
  179. return $params;
  180. }
  181. private function addOptionsForPostQuery(array $params, array $options): array
  182. {
  183. $params['options'] = $options;
  184. return $params;
  185. }
  186. private function executePostQuery(string $endpoint, array $params)
  187. {
  188. $url = $this->getUrl($endpoint);
  189. $appKey = $params[static::KEY_API_KEY];
  190. unset($params[static::KEY_API_KEY]);
  191. $url .= '?key='.$appKey;
  192. $requestBody = json_encode($params);
  193. $request = $this->getMessageFactory()->createRequest('POST', $url, [], $requestBody);
  194. $response = $this->getHttpClient()->sendRequest($request);
  195. $content = $this->parseHttpResponse($response, $url);
  196. return $this->parseResponseContent($content);
  197. }
  198. /**
  199. * @param string $url
  200. *
  201. * @return AddressCollection
  202. */
  203. private function executeGetQuery(string $endpoint, array $params): AddressCollection
  204. {
  205. $baseUrl = $this->getUrl($endpoint);
  206. $url = $this->addGetQuery($baseUrl, $params);
  207. $content = $this->getUrlContents($url);
  208. return $this->parseResponseContent($content);
  209. }
  210. private function parseResponseContent(string $content): AddressCollection
  211. {
  212. $json = json_decode($content, true);
  213. if (!isset($json['results']) || empty($json['results'])) {
  214. return new AddressCollection([]);
  215. }
  216. $locations = $json['results'][0]['locations'];
  217. if (empty($locations)) {
  218. return new AddressCollection([]);
  219. }
  220. $results = [];
  221. foreach ($locations as $location) {
  222. if ($location['street'] || $location['postalCode'] || $location['adminArea5'] || $location['adminArea4'] || $location['adminArea3']) {
  223. $admins = [];
  224. $state = $location['adminArea3'];
  225. if ($state) {
  226. $code = null;
  227. if (2 == strlen($state)) {
  228. $code = $state;
  229. }
  230. $admins[] = [
  231. 'name' => $state,
  232. 'code' => $code,
  233. 'level' => static::ADMIN_LEVEL_STATE,
  234. ];
  235. }
  236. if ($location['adminArea4']) {
  237. $admins[] = ['name' => $location['adminArea4'], 'level' => static::ADMIN_LEVEL_COUNTY];
  238. }
  239. $position = $location['latLng'];
  240. if (!$this->useRoadPosition) {
  241. if ($location['displayLatLng']) {
  242. $position = $location['displayLatLng'];
  243. }
  244. }
  245. $results[] = Address::createFromArray([
  246. 'providedBy' => $this->getName(),
  247. 'latitude' => $position['lat'],
  248. 'longitude' => $position['lng'],
  249. 'streetName' => $location['street'] ?: null,
  250. 'locality' => $location['adminArea5'] ?: null,
  251. 'subLocality' => $location['adminArea6'] ?: null,
  252. 'postalCode' => $location['postalCode'] ?: null,
  253. 'adminLevels' => $admins,
  254. 'country' => $location['adminArea1'] ?: null,
  255. 'countryCode' => $location['adminArea1'] ?: null,
  256. ]);
  257. }
  258. }
  259. return new AddressCollection($results);
  260. }
  261. private function mapAddressToArray(Location $address): array
  262. {
  263. $location = [];
  264. $streetParts = [
  265. trim($address->getStreetNumber() ?: ''),
  266. trim($address->getStreetName() ?: ''),
  267. ];
  268. $street = implode(' ', array_filter($streetParts));
  269. if ($street) {
  270. $location['street'] = $street;
  271. }
  272. if ($address->getSubLocality()) {
  273. $location['adminArea6'] = $address->getSubLocality();
  274. $location['adminArea6Type'] = 'Neighborhood';
  275. }
  276. if ($address->getLocality()) {
  277. $location['adminArea5'] = $address->getLocality();
  278. $location['adminArea5Type'] = 'City';
  279. }
  280. /** @var AdminLevel $adminLevel */
  281. foreach ($address->getAdminLevels() as $adminLevel) {
  282. switch ($adminLevel->getLevel()) {
  283. case static::ADMIN_LEVEL_STATE:
  284. $state = $adminLevel->getCode();
  285. if (!$state) {
  286. $state = $adminLevel->getName();
  287. }
  288. $location['adminArea3'] = $state;
  289. $location['adminArea3Type'] = 'State';
  290. break;
  291. case static::ADMIN_LEVEL_COUNTY:
  292. $county = $adminLevel->getName();
  293. $location['adminArea4'] = $county;
  294. $location['adminArea4Type'] = 'County';
  295. }
  296. }
  297. $country = $address->getCountry();
  298. if ($country instanceof Country) {
  299. $code = $country->getCode();
  300. if (!$code) {
  301. $code = $country->getName();
  302. }
  303. $location['adminArea1'] = $code;
  304. $location['adminArea1Type'] = 'Country';
  305. }
  306. $postalCode = $address->getPostalCode();
  307. if ($postalCode) {
  308. $location['postalCode'] = $address->getPostalCode();
  309. }
  310. return $location;
  311. }
  312. private function mapBoundsToArray(Bounds $bounds)
  313. {
  314. return [
  315. 'ul' => [static::KEY_LAT => $bounds->getNorth(), static::KEY_LNG => $bounds->getWest()],
  316. 'lr' => [static::KEY_LAT => $bounds->getSouth(), static::KEY_LNG => $bounds->getEast()],
  317. ];
  318. }
  319. protected function parseHttpResponse(ResponseInterface $response, string $url): string
  320. {
  321. $statusCode = $response->getStatusCode();
  322. if (401 === $statusCode || 403 === $statusCode) {
  323. throw new InvalidCredentials();
  324. } elseif (429 === $statusCode) {
  325. throw new QuotaExceeded();
  326. } elseif ($statusCode >= 300) {
  327. throw InvalidServerResponse::create($url, $statusCode);
  328. }
  329. $body = (string) $response->getBody();
  330. if (empty($body)) {
  331. throw InvalidServerResponse::emptyResponse($url);
  332. }
  333. return $body;
  334. }
  335. }