Completed
Push — master ( fef860...0ac8a8 )
by Tobias
03:53
created

OpenCage::parseAdminsLevels()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.9
c 0
b 0
f 0
cc 4
nc 6
nop 2
crap 4
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 27
    public function __construct(HttpClient $client, string $apiKey)
49
    {
50 27
        if (empty($apiKey)) {
51
            throw new InvalidCredentials('No API key provided.');
52
        }
53
54 27
        $this->apiKey = $apiKey;
55 27
        parent::__construct($client);
56 27
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61 26
    public function geocodeQuery(GeocodeQuery $query): Collection
62
    {
63 26
        $address = $query->getText();
64
65
        // This API doesn't handle IPs
66 26
        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 22
        $url = sprintf(self::GEOCODE_ENDPOINT_URL, $this->apiKey, urlencode($address), $query->getLimit());
71
72 22
        return $this->executeQuery($url, $query->getLocale());
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78 8
    public function reverseQuery(ReverseQuery $query): Collection
79
    {
80 8
        $coordinates = $query->getCoordinates();
81 8
        $address = sprintf('%f, %f', $coordinates->getLatitude(), $coordinates->getLongitude());
82
83 8
        $geocodeQuery = GeocodeQuery::create($address);
84 8
        if (null !== $locale = $query->getLocale()) {
85 1
            $geocodeQuery = $geocodeQuery->withLocale($query->getLocale());
86
        }
87
88 8
        return $this->geocodeQuery($geocodeQuery);
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 9
    public function getName(): string
95
    {
96 9
        return 'opencage';
97
    }
98
99
    /**
100
     * @param string      $url
101
     * @param string|null $locale
102
     *
103
     * @return AddressCollection
104
     *
105
     * @throws \Geocoder\Exception\Exception
106
     */
107 22
    private function executeQuery(string $url, string $locale = null): AddressCollection
108
    {
109 22
        if (null !== $locale) {
110 4
            $url = sprintf('%s&language=%s', $url, $locale);
111
        }
112
113 22
        $content = $this->getUrlContents($url);
114 12
        $json = json_decode($content, true);
115
116
        // https://geocoder.opencagedata.com/api#codes
117 12
        if (isset($json['status'])) {
118 11
            switch ($json['status']['code']) {
119 11
                case 400:
120
                    throw new InvalidArgument('Invalid request (a required parameter is missing).');
121 11
                case 402:
122 1
                    throw new QuotaExceeded('Valid request but quota exceeded.');
123 10
                case 403:
124 1
                    throw new InvalidCredentials('Invalid or missing api key.');
125
            }
126
        }
127
128 10
        if (!isset($json['total_results']) || 0 == $json['total_results']) {
129 2
            return new AddressCollection([]);
130
        }
131
132 8
        $locations = $json['results'];
133
134 8
        if (empty($locations)) {
135
            return new AddressCollection([]);
136
        }
137
138 8
        $results = [];
139 8
        foreach ($locations as $location) {
140 8
            $builder = new AddressBuilder($this->getName());
141 8
            $this->parseCoordinates($builder, $location);
142
143 8
            $components = $location['components'];
144 8
            $annotations = $location['annotations'];
145
146 8
            $this->parseAdminsLevels($builder, $components);
147 8
            $this->parseCountry($builder, $components);
148 8
            $builder->setLocality($this->guessLocality($components));
149 8
            $builder->setSubLocality($this->guessSubLocality($components));
150 8
            $builder->setStreetNumber(isset($components['house_number']) ? $components['house_number'] : null);
151 8
            $builder->setStreetName($this->guessStreetName($components));
152 8
            $builder->setPostalCode(isset($components['postcode']) ? $components['postcode'] : null);
153 8
            $builder->setTimezone(isset($annotations['timezone']['name']) ? $annotations['timezone']['name'] : null);
154
155
            /** @var OpenCageAddress $address */
156 8
            $address = $builder->build(OpenCageAddress::class);
157 8
            $address = $address->withMGRS(isset($annotations['MGRS']) ? $annotations['MGRS'] : null);
158 8
            $address = $address->withMaidenhead(isset($annotations['Maidenhead']) ? $annotations['Maidenhead'] : null);
159 8
            $address = $address->withGeohash(isset($annotations['geohash']) ? $annotations['geohash'] : null);
160 8
            $address = $address->withWhat3words(isset($annotations['what3words'], $annotations['what3words']['words']) ? $annotations['what3words']['words'] : null);
161 8
            $address = $address->withFormattedAddress($location['formatted']);
162
163 8
            $results[] = $address;
164
        }
165
166 8
        return new AddressCollection($results);
167
    }
168
169
    /**
170
     * @param AddressBuilder $builder
171
     * @param array          $location
172
     */
173 8
    private function parseCoordinates(AddressBuilder $builder, array $location)
174
    {
175 8
        $builder->setCoordinates($location['geometry']['lat'], $location['geometry']['lng']);
176
177
        $bounds = [
178 8
            'south' => null,
179
            'west' => null,
180
            'north' => null,
181
            'east' => null,
182
        ];
183
184 8
        if (isset($location['bounds'])) {
185
            $bounds = [
186 8
                'south' => $location['bounds']['southwest']['lat'],
187 8
                'west' => $location['bounds']['southwest']['lng'],
188 8
                'north' => $location['bounds']['northeast']['lat'],
189 8
                'east' => $location['bounds']['northeast']['lng'],
190
            ];
191
        }
192
193 8
        $builder->setBounds(
194 8
            $bounds['south'],
195 8
            $bounds['west'],
196 8
            $bounds['north'],
197 8
            $bounds['east']
198
        );
199 8
    }
200
201
    /**
202
     * @param AddressBuilder $builder
203
     * @param array          $components
204
     */
205 8
    private function parseAdminsLevels(AddressBuilder $builder, array $components)
206
    {
207 8
        if (isset($components['state'])) {
208 8
            $stateCode = isset($components['state_code']) ? $components['state_code'] : null;
209 8
            $builder->addAdminLevel(1, $components['state'], $stateCode);
210
        }
211
212 8
        if (isset($components['county'])) {
213 6
            $builder->addAdminLevel(2, $components['county']);
214
        }
215 8
    }
216
217
    /**
218
     * @param AddressBuilder $builder
219
     * @param array          $components
220
     */
221 8
    private function parseCountry(AddressBuilder $builder, array $components)
222
    {
223 8
        if (isset($components['country'])) {
224 8
            $builder->setCountry($components['country']);
225
        }
226
227 8
        if (isset($components['country_code'])) {
228 8
            $builder->setCountryCode(\strtoupper($components['country_code']));
229
        }
230 8
    }
231
232
    /**
233
     * @param array $components
234
     *
235
     * @return null|string
236
     */
237 8
    protected function guessLocality(array $components)
238
    {
239 8
        $localityKeys = ['city', 'town', 'village', 'hamlet'];
240
241 8
        return $this->guessBestComponent($components, $localityKeys);
242
    }
243
244
    /**
245
     * @param array $components
246
     *
247
     * @return null|string
248
     */
249 8
    protected function guessStreetName(array $components)
250
    {
251 8
        $streetNameKeys = ['road', 'street', 'street_name', 'residential'];
252
253 8
        return $this->guessBestComponent($components, $streetNameKeys);
254
    }
255
256
    /**
257
     * @param array $components
258
     *
259
     * @return null|string
260
     */
261 8
    protected function guessSubLocality(array $components)
262
    {
263 8
        $subLocalityKeys = ['suburb', 'neighbourhood', 'city_district'];
264
265 8
        return $this->guessBestComponent($components, $subLocalityKeys);
266
    }
267
268
    /**
269
     * @param array $components
270
     * @param array $keys
271
     *
272
     * @return null|string
273
     */
274 8
    protected function guessBestComponent(array $components, array $keys)
275
    {
276 8
        foreach ($keys as $key) {
277 8
            if (isset($components[$key]) && !empty($components[$key])) {
278 8
                return $components[$key];
279
            }
280
        }
281
282 7
        return null;
283
    }
284
}
285