PageRenderTime 44ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Provider/OpenCage/OpenCage.php

http://github.com/willdurand/Geocoder
PHP | 293 lines | 176 code | 44 blank | 73 comment | 20 complexity | 7c5676e57f5a55151f4f00e3e6b83131 MD5 | raw file
Possible License(s): MIT
  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\OpenCage;
  11. use Geocoder\Exception\InvalidArgument;
  12. use Geocoder\Exception\InvalidCredentials;
  13. use Geocoder\Exception\QuotaExceeded;
  14. use Geocoder\Exception\UnsupportedOperation;
  15. use Geocoder\Collection;
  16. use Geocoder\Model\AddressBuilder;
  17. use Geocoder\Model\AddressCollection;
  18. use Geocoder\Provider\OpenCage\Model\OpenCageAddress;
  19. use Geocoder\Query\GeocodeQuery;
  20. use Geocoder\Query\ReverseQuery;
  21. use Geocoder\Http\Provider\AbstractHttpProvider;
  22. use Geocoder\Provider\Provider;
  23. use Http\Client\HttpClient;
  24. /**
  25. * @author mtm <mtm@opencagedata.com>
  26. */
  27. final class OpenCage extends AbstractHttpProvider implements Provider
  28. {
  29. /**
  30. * @var string
  31. */
  32. const GEOCODE_ENDPOINT_URL = 'https://api.opencagedata.com/geocode/v1/json?key=%s&query=%s&limit=%d&pretty=1';
  33. /**
  34. * @var string
  35. */
  36. private $apiKey;
  37. /**
  38. * @param HttpClient $client an HTTP adapter
  39. * @param string $apiKey an API key
  40. */
  41. public function __construct(HttpClient $client, string $apiKey)
  42. {
  43. if (empty($apiKey)) {
  44. throw new InvalidCredentials('No API key provided.');
  45. }
  46. $this->apiKey = $apiKey;
  47. parent::__construct($client);
  48. }
  49. /**
  50. * {@inheritdoc}
  51. */
  52. public function geocodeQuery(GeocodeQuery $query): Collection
  53. {
  54. $address = $query->getText();
  55. // This API doesn't handle IPs
  56. if (filter_var($address, FILTER_VALIDATE_IP)) {
  57. throw new UnsupportedOperation('The OpenCage provider does not support IP addresses, only street addresses.');
  58. }
  59. $url = sprintf(self::GEOCODE_ENDPOINT_URL, $this->apiKey, urlencode($address), $query->getLimit());
  60. if (null !== $countryCode = $query->getData('countrycode')) {
  61. $url = sprintf('%s&countrycode=%s', $url, $countryCode);
  62. }
  63. if (null !== $bounds = $query->getBounds()) {
  64. $url = sprintf('%s&bounds=%s,%s,%s,%s', $url, $bounds->getWest(), $bounds->getSouth(), $bounds->getEast(), $bounds->getNorth());
  65. }
  66. if (null !== $proximity = $query->getData('proximity')) {
  67. $url = sprintf('%s&proximity=%s', $url, $proximity);
  68. }
  69. return $this->executeQuery($url, $query->getLocale());
  70. }
  71. /**
  72. * {@inheritdoc}
  73. */
  74. public function reverseQuery(ReverseQuery $query): Collection
  75. {
  76. $coordinates = $query->getCoordinates();
  77. $address = sprintf('%f, %f', $coordinates->getLatitude(), $coordinates->getLongitude());
  78. $geocodeQuery = GeocodeQuery::create($address);
  79. if (null !== $locale = $query->getLocale()) {
  80. $geocodeQuery = $geocodeQuery->withLocale($query->getLocale());
  81. }
  82. return $this->geocodeQuery($geocodeQuery);
  83. }
  84. /**
  85. * {@inheritdoc}
  86. */
  87. public function getName(): string
  88. {
  89. return 'opencage';
  90. }
  91. /**
  92. * @param string $url
  93. * @param string|null $locale
  94. *
  95. * @return AddressCollection
  96. *
  97. * @throws \Geocoder\Exception\Exception
  98. */
  99. private function executeQuery(string $url, string $locale = null): AddressCollection
  100. {
  101. if (null !== $locale) {
  102. $url = sprintf('%s&language=%s', $url, $locale);
  103. }
  104. $content = $this->getUrlContents($url);
  105. $json = json_decode($content, true);
  106. // https://geocoder.opencagedata.com/api#codes
  107. if (isset($json['status'])) {
  108. switch ($json['status']['code']) {
  109. case 400:
  110. throw new InvalidArgument('Invalid request (a required parameter is missing).');
  111. case 402:
  112. throw new QuotaExceeded('Valid request but quota exceeded.');
  113. case 403:
  114. throw new InvalidCredentials('Invalid or missing api key.');
  115. }
  116. }
  117. if (!isset($json['total_results']) || 0 == $json['total_results']) {
  118. return new AddressCollection([]);
  119. }
  120. $locations = $json['results'];
  121. if (empty($locations)) {
  122. return new AddressCollection([]);
  123. }
  124. $results = [];
  125. foreach ($locations as $location) {
  126. $builder = new AddressBuilder($this->getName());
  127. $this->parseCoordinates($builder, $location);
  128. $components = $location['components'];
  129. $annotations = $location['annotations'];
  130. $this->parseAdminsLevels($builder, $components);
  131. $this->parseCountry($builder, $components);
  132. $builder->setLocality($this->guessLocality($components));
  133. $builder->setSubLocality($this->guessSubLocality($components));
  134. $builder->setStreetNumber(isset($components['house_number']) ? $components['house_number'] : null);
  135. $builder->setStreetName($this->guessStreetName($components));
  136. $builder->setPostalCode(isset($components['postcode']) ? $components['postcode'] : null);
  137. $builder->setTimezone(isset($annotations['timezone']['name']) ? $annotations['timezone']['name'] : null);
  138. /** @var OpenCageAddress $address */
  139. $address = $builder->build(OpenCageAddress::class);
  140. $address = $address->withMGRS(isset($annotations['MGRS']) ? $annotations['MGRS'] : null);
  141. $address = $address->withMaidenhead(isset($annotations['Maidenhead']) ? $annotations['Maidenhead'] : null);
  142. $address = $address->withGeohash(isset($annotations['geohash']) ? $annotations['geohash'] : null);
  143. $address = $address->withWhat3words(isset($annotations['what3words'], $annotations['what3words']['words']) ? $annotations['what3words']['words'] : null);
  144. $address = $address->withFormattedAddress($location['formatted']);
  145. $results[] = $address;
  146. }
  147. return new AddressCollection($results);
  148. }
  149. /**
  150. * @param AddressBuilder $builder
  151. * @param array $location
  152. */
  153. private function parseCoordinates(AddressBuilder $builder, array $location)
  154. {
  155. $builder->setCoordinates($location['geometry']['lat'], $location['geometry']['lng']);
  156. $bounds = [
  157. 'south' => null,
  158. 'west' => null,
  159. 'north' => null,
  160. 'east' => null,
  161. ];
  162. if (isset($location['bounds'])) {
  163. $bounds = [
  164. 'south' => $location['bounds']['southwest']['lat'],
  165. 'west' => $location['bounds']['southwest']['lng'],
  166. 'north' => $location['bounds']['northeast']['lat'],
  167. 'east' => $location['bounds']['northeast']['lng'],
  168. ];
  169. }
  170. $builder->setBounds(
  171. $bounds['south'],
  172. $bounds['west'],
  173. $bounds['north'],
  174. $bounds['east']
  175. );
  176. }
  177. /**
  178. * @param AddressBuilder $builder
  179. * @param array $components
  180. */
  181. private function parseAdminsLevels(AddressBuilder $builder, array $components)
  182. {
  183. if (isset($components['state'])) {
  184. $stateCode = isset($components['state_code']) ? $components['state_code'] : null;
  185. $builder->addAdminLevel(1, $components['state'], $stateCode);
  186. }
  187. if (isset($components['county'])) {
  188. $builder->addAdminLevel(2, $components['county']);
  189. }
  190. }
  191. /**
  192. * @param AddressBuilder $builder
  193. * @param array $components
  194. */
  195. private function parseCountry(AddressBuilder $builder, array $components)
  196. {
  197. if (isset($components['country'])) {
  198. $builder->setCountry($components['country']);
  199. }
  200. if (isset($components['country_code'])) {
  201. $builder->setCountryCode(\strtoupper($components['country_code']));
  202. }
  203. }
  204. /**
  205. * @param array $components
  206. *
  207. * @return string|null
  208. */
  209. protected function guessLocality(array $components)
  210. {
  211. $localityKeys = ['city', 'town', 'municipality', 'village', 'hamlet', 'locality', 'croft'];
  212. return $this->guessBestComponent($components, $localityKeys);
  213. }
  214. /**
  215. * @param array $components
  216. *
  217. * @return string|null
  218. */
  219. protected function guessStreetName(array $components)
  220. {
  221. $streetNameKeys = ['road', 'footway', 'street', 'street_name', 'residential', 'path', 'pedestrian', 'road_reference', 'road_reference_intl'];
  222. return $this->guessBestComponent($components, $streetNameKeys);
  223. }
  224. /**
  225. * @param array $components
  226. *
  227. * @return string|null
  228. */
  229. protected function guessSubLocality(array $components)
  230. {
  231. $subLocalityKeys = ['neighbourhood', 'suburb', 'city_district', 'district', 'quarter', 'houses', 'subdivision'];
  232. return $this->guessBestComponent($components, $subLocalityKeys);
  233. }
  234. /**
  235. * @param array $components
  236. * @param array $keys
  237. *
  238. * @return string|null
  239. */
  240. protected function guessBestComponent(array $components, array $keys)
  241. {
  242. foreach ($keys as $key) {
  243. if (isset($components[$key]) && !empty($components[$key])) {
  244. return $components[$key];
  245. }
  246. }
  247. return null;
  248. }
  249. }