Completed
Push — master ( 9e9ac3...0d6d33 )
by Tobias
03:50
created

GoogleMaps::parseCoordinates()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.3244

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 16
cts 22
cp 0.7272
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 21
nc 4
nop 2
crap 4.3244
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\GoogleMaps;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidCredentials;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\QuotaExceeded;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Model\AddressCollection;
21
use Geocoder\Model\AddressBuilder;
22
use Geocoder\Query\GeocodeQuery;
23
use Geocoder\Query\ReverseQuery;
24
use Geocoder\Http\Provider\AbstractHttpProvider;
25
use Geocoder\Provider\GoogleMaps\Model\GoogleAddress;
26
use Geocoder\Provider\Provider;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author William Durand <[email protected]>
31
 */
32
final class GoogleMaps extends AbstractHttpProvider implements Provider
33
{
34
    /**
35
     * @var string
36
     */
37
    const GEOCODE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s';
38
39
    /**
40
     * @var string
41
     */
42
    const REVERSE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%F,%F';
43
44
    /**
45
     * @var string|null
46
     */
47
    private $region;
48
49
    /**
50
     * @var string|null
51
     */
52
    private $apiKey;
53
54
    /**
55
     * @var string
56
     */
57
    private $clientId;
58
59
    /**
60
     * @var string|null
61
     */
62
    private $privateKey;
63
64
    /**
65
     * Google Maps for Business
66
     * https://developers.google.com/maps/documentation/business/.
67
     *
68
     * @param HttpClient $client     An HTTP adapter
69
     * @param string     $clientId   Your Client ID
70
     * @param string     $privateKey Your Private Key (optional)
71
     * @param string     $region     Region biasing (optional)
72
     * @param string     $apiKey     Google Geocoding API key (optional)
73
     *
74
     * @return GoogleMaps
75
     */
76 4
    public static function business(HttpClient $client, string  $clientId, string $privateKey = null, string $region = null, string $apiKey = null)
77
    {
78 4
        $provider = new self($client, $region, $apiKey);
79 4
        $provider->clientId = $clientId;
80 4
        $provider->privateKey = $privateKey;
81
82 4
        return $provider;
83
    }
84
85
    /**
86
     * @param HttpClient $client An HTTP adapter
87
     * @param string     $region Region biasing (optional)
88
     * @param string     $apiKey Google Geocoding API key (optional)
89
     */
90 39
    public function __construct(HttpClient $client, string $region = null, string $apiKey = null)
91
    {
92 39
        parent::__construct($client);
93
94 39
        $this->region = $region;
95 39
        $this->apiKey = $apiKey;
96 39
    }
97
98 28
    public function geocodeQuery(GeocodeQuery $query): Collection
99
    {
100
        // Google API returns invalid data if IP address given
101
        // This API doesn't handle IPs
102 28
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
103 3
            throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.');
104
        }
105
106 25
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, rawurlencode($query->getText()));
107 25
        if (null !== $bounds = $query->getBounds()) {
108
            $url .= sprintf('&bounds=%s,%s|%s,%s', $bounds->getSouth(), $bounds->getWest(), $bounds->getNorth(), $bounds->getEast());
109
        }
110
111 25
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
112
    }
113
114 10
    public function reverseQuery(ReverseQuery $query): Collection
115
    {
116 10
        $coordinate = $query->getCoordinates();
117 10
        $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude());
118
119 10
        if (null !== $locationType = $query->getData('location_type')) {
120
            $url .= '&location_type='.urlencode($locationType);
121
        }
122
123 10
        if (null !== $resultType = $query->getData('result_type')) {
124
            $url .= '&result_type='.urlencode($resultType);
125
        }
126
127 10
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 16
    public function getName(): string
134
    {
135 16
        return 'google_maps';
136
    }
137
138
    /**
139
     * @param string $url
140
     * @param string $locale
141
     *
142
     * @return string query with extra params
143
     */
144 35
    private function buildQuery(string $url, string $locale = null, string $region = null)
145
    {
146 35
        if (null !== $locale) {
147 4
            $url = sprintf('%s&language=%s', $url, $locale);
148
        }
149
150 35
        if (null !== $region) {
151 1
            $url = sprintf('%s&region=%s', $url, $region);
152
        }
153
154 35
        if (null !== $this->apiKey) {
155 2
            $url = sprintf('%s&key=%s', $url, $this->apiKey);
156
        }
157
158 35
        if (null !== $this->clientId) {
159 4
            $url = sprintf('%s&client=%s', $url, $this->clientId);
160
161 4
            if (null !== $this->privateKey) {
162 3
                $url = $this->signQuery($url);
163
            }
164
        }
165
166 35
        return $url;
167
    }
168
169
    /**
170
     * @param string $url
171
     * @param string $locale
172
     * @param int    $limit
173
     * @param string $region
174
     *
175
     * @return AddressCollection
176
     *
177
     * @throws InvalidServerResponse
178
     * @throws InvalidCredentials
179
     */
180 35
    private function fetchUrl(string $url, string $locale = null, int $limit, string $region = null): AddressCollection
181
    {
182 35
        $url = $this->buildQuery($url, $locale, $region);
183 35
        $content = $this->getUrlContents($url);
184 22
        $json = $this->validateResponse($url, $content);
185
186
        // no result
187 17
        if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
188 2
            return new AddressCollection([]);
189
        }
190
191 15
        $results = [];
192 15
        foreach ($json->results as $result) {
193 15
            $builder = new AddressBuilder($this->getName());
194 15
            $this->parseCoordinates($builder, $result);
195
196
            // update address components
197 15
            foreach ($result->address_components as $component) {
198 15
                foreach ($component->types as $type) {
199 15
                    $this->updateAddressComponent($builder, $type, $component);
200
                }
201
            }
202
203
            /** @var GoogleAddress $address */
204 15
            $address = $builder->build(GoogleAddress::class);
205 15
            if (isset($result->geometry->location_type)) {
206 15
                $address = $address->withLocationType($result->geometry->location_type);
207
            }
208 15
            if (isset($result->types)) {
209 15
                $address = $address->withResultType($result->types);
210
            }
211 15
            if (isset($result->formatted_address)) {
212 15
                $address = $address->withFormattedAddress($result->formatted_address);
213
            }
214 15
            $address = $address->withStreetAddress($builder->getValue('street_address'));
215 15
            $address = $address->withIntersection($builder->getValue('intersection'));
216 15
            $address = $address->withPolitical($builder->getValue('political'));
217 15
            $address = $address->withColloquialArea($builder->getValue('colloquial_area'));
218 15
            $address = $address->withWard($builder->getValue('ward'));
219 15
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
220 15
            $address = $address->withPremise($builder->getValue('premise'));
221 15
            $address = $address->withSubpremise($builder->getValue('subpremise'));
222 15
            $address = $address->withNaturalFeature($builder->getValue('natural_feature'));
223 15
            $address = $address->withAirport($builder->getValue('airport'));
224 15
            $address = $address->withPark($builder->getValue('park'));
225 15
            $address = $address->withPointOfInterest($builder->getValue('point_of_interest'));
226 15
            $address = $address->withEstablishment($builder->getValue('establishment'));
227 15
            $results[] = $address;
228
229 15
            if (count($results) >= $limit) {
230 15
                break;
231
            }
232
        }
233
234 15
        return new AddressCollection($results);
235
    }
236
237
    /**
238
     * Update current resultSet with given key/value.
239
     *
240
     * @param AddressBuilder $builder
241
     * @param string         $type    Component type
242
     * @param object         $values  The component values
243
     */
244 15
    private function updateAddressComponent(AddressBuilder $builder, string $type, $values)
245
    {
246
        switch ($type) {
247 15
            case 'postal_code':
248 13
                $builder->setPostalCode($values->long_name);
249 13
                break;
250
251 15
            case 'locality':
252 15
            case 'postal_town':
253 15
                $builder->setLocality($values->long_name);
254 15
                break;
255
256 15
            case 'administrative_area_level_1':
257 15
            case 'administrative_area_level_2':
258 15
            case 'administrative_area_level_3':
259 15
            case 'administrative_area_level_4':
260 15
            case 'administrative_area_level_5':
261 14
                $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name);
262 14
                break;
263
264 15
            case 'country':
265 15
                $builder->setCountry($values->long_name);
266 15
                $builder->setCountryCode($values->short_name);
267 15
                break;
268
269 15
            case 'street_number':
270 7
                $builder->setStreetNumber($values->long_name);
271 7
                break;
272
273 15
            case 'route':
274 8
                $builder->setStreetName($values->long_name);
275 8
                break;
276
277 15
            case 'sublocality':
278 5
                $builder->setSubLocality($values->long_name);
279 5
                break;
280
281 15
            case 'street_address':
282 15
            case 'intersection':
283 15
            case 'political':
284 13
            case 'colloquial_area':
285 12
            case 'ward':
286 12
            case 'neighborhood':
287 11
            case 'premise':
288 10
            case 'subpremise':
289 9
            case 'natural_feature':
290 9
            case 'airport':
291 9
            case 'park':
292 9
            case 'point_of_interest':
293 9
            case 'establishment':
294 15
                $builder->setValue($type, $values->long_name);
295 15
                break;
296
297
            default:
298
        }
299 15
    }
300
301
    /**
302
     * Sign a URL with a given crypto key
303
     * Note that this URL must be properly URL-encoded
304
     * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source.
305
     *
306
     * @param string $query Query to be signed
307
     *
308
     * @return string $query query with signature appended
309
     */
310 3
    private function signQuery(string $query): string
311
    {
312 3
        $url = parse_url($query);
313
314 3
        $urlPartToSign = $url['path'].'?'.$url['query'];
315
316
        // Decode the private key into its binary format
317 3
        $decodedKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $this->privateKey));
318
319
        // Create a signature using the private key and the URL-encoded
320
        // string using HMAC SHA1. This signature will be binary.
321 3
        $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true);
322
323 3
        $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature));
324
325 3
        return sprintf('%s&signature=%s', $query, $encodedSignature);
326
    }
327
328
    /**
329
     * Decode the response content and validate it to make sure it does not have any errors.
330
     *
331
     * @param string $url
332
     * @param string $content
333
     *
334
     * @return mixed result form json_decode()
335
     *
336
     * @throws InvalidCredentials
337
     * @throws InvalidServerResponse
338
     * @throws QuotaExceeded
339
     */
340 22
    private function validateResponse(string $url, $content)
341
    {
342
        // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
343 22
        if (strpos($content, "Provided 'signature' is not valid for the provided client ID") !== false) {
344 2
            throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url));
345
        }
346
347 20
        $json = json_decode($content);
348
349
        // API error
350 20
        if (!isset($json)) {
351
            throw InvalidServerResponse::create($url);
352
        }
353
354 20
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
355 2
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
356
        }
357
358 18
        if ('REQUEST_DENIED' === $json->status) {
359
            throw new InvalidServerResponse(
360
                sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_message)
361
            );
362
        }
363
364
        // you are over your quota
365 18
        if ('OVER_QUERY_LIMIT' === $json->status) {
366 1
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
367
        }
368
369 17
        return $json;
370
    }
371
372
    /**
373
     * Parse coordinats and bounds.
374
     *
375
     * @param AddressBuilder $builder
376
     * @param $result
377
     */
378 15
    private function parseCoordinates(AddressBuilder $builder, $result)
379
    {
380 15
        $coordinates = $result->geometry->location;
381 15
        $builder->setCoordinates($coordinates->lat, $coordinates->lng);
382
383 15
        if (isset($result->geometry->bounds)) {
384 8
            $builder->setBounds(
385 8
                $result->geometry->bounds->southwest->lat,
386 8
                $result->geometry->bounds->southwest->lng,
387 8
                $result->geometry->bounds->northeast->lat,
388 8
                $result->geometry->bounds->northeast->lng
389
            );
390 10
        } elseif (isset($result->geometry->viewport)) {
391 10
            $builder->setBounds(
392 10
                $result->geometry->viewport->southwest->lat,
393 10
                $result->geometry->viewport->southwest->lng,
394 10
                $result->geometry->viewport->northeast->lat,
395 10
                $result->geometry->viewport->northeast->lng
396
            );
397
        } elseif ('ROOFTOP' === $result->geometry->location_type) {
398
            // Fake bounds
399
            $builder->setBounds(
400
                $coordinates->lat,
401
                $coordinates->lng,
402
                $coordinates->lat,
403
                $coordinates->lng
404
            );
405
        }
406 15
    }
407
}
408