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