Completed
Push — master ( c5af65...93d8cc )
by Tobias
05:03
created

MapQuest::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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\MapQuest;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidCredentials;
18
use Geocoder\Exception\InvalidServerResponse;
19
use Geocoder\Exception\QuotaExceeded;
20
use Geocoder\Exception\UnsupportedOperation;
21
use Geocoder\Http\Provider\AbstractHttpProvider;
22
use Geocoder\Location;
23
use Geocoder\Model\Address;
24
use Geocoder\Model\AddressCollection;
25
use Geocoder\Model\AdminLevel;
26
use Geocoder\Model\Bounds;
27
use Geocoder\Model\Country;
28
use Geocoder\Query\GeocodeQuery;
29
use Geocoder\Query\ReverseQuery;
30
use Geocoder\Provider\Provider;
31
use Http\Client\HttpClient;
32
use Psr\Http\Message\ResponseInterface;
33
34
/**
35
 * @author William Durand <[email protected]>
36
 */
37
final class MapQuest extends AbstractHttpProvider implements Provider
38
{
39
    const DATA_KEY_ADDRESS = 'address';
40
41
    const KEY_API_KEY = 'key';
42
    const KEY_LOCATION = 'location';
43
    const KEY_OUT_FORMAT = 'outFormat';
44
    const KEY_MAX_RESULTS = 'maxResults';
45
    const KEY_THUMB_MAPS = 'thumbMaps';
46
    const KEY_INTL_MODE = 'intlMode';
47
    const KEY_BOUNDING_BOX = 'boundingBox';
48
    const KEY_LAT = 'lat';
49
    const KEY_LNG = 'lng';
50
    const MODE_5BOX = '5BOX';
51
    const OPEN_BASE_URL = 'https://open.mapquestapi.com/geocoding/v1/';
52
    const LICENSED_BASE_URL = 'https://www.mapquestapi.com/geocoding/v1/';
53
    const GEOCODE_ENDPOINT = 'address';
54
    const DEFAULT_GEOCODE_PARAMS = [
55
        self::KEY_LOCATION => '',
56
        self::KEY_OUT_FORMAT => 'json',
57
        self::KEY_API_KEY => '',
58
    ];
59
    const DEFAULT_GEOCODE_OPTIONS = [
60
        self::KEY_MAX_RESULTS => 3,
61
        self::KEY_THUMB_MAPS => false,
62
    ];
63
    const REVERSE_ENDPOINT = 'reverse';
64
    const ADMIN_LEVEL_STATE = 1;
65
    const ADMIN_LEVEL_COUNTY = 2;
66
67
    /**
68
     * MapQuest offers two geocoding endpoints one commercial (true) and one open (false)
69
     * More information: http://developer.mapquest.com/web/tools/getting-started/platform/licensed-vs-open.
70
     *
71
     * @var bool
72
     */
73
    private $licensed;
74
75
    /**
76
     * @var bool
77
     */
78
    private $useRoadPosition;
79
80
    /**
81
     * @var string
82
     */
83
    private $apiKey;
84
85
    /**
86
     * @param HttpClient $client          an HTTP adapter
87
     * @param string     $apiKey          an API key
88
     * @param bool       $licensed        true to use MapQuest's licensed endpoints, default is false to use the open endpoints (optional)
89
     * @param bool       $useRoadPosition true to use nearest point on a road for the entrance, false to use map display position
90
     */
91 28
    public function __construct(HttpClient $client, string $apiKey, bool $licensed = false, bool $useRoadPosition = false)
92
    {
93 28
        if (empty($apiKey)) {
94
            throw new InvalidCredentials('No API key provided.');
95
        }
96
97 28
        $this->apiKey = $apiKey;
98 28
        $this->licensed = $licensed;
99 28
        $this->useRoadPosition = $useRoadPosition;
100 28
        parent::__construct($client);
101 28
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 18
    public function geocodeQuery(GeocodeQuery $query): Collection
107
    {
108 18
        $params = static::DEFAULT_GEOCODE_PARAMS;
109 18
        $params[static::KEY_API_KEY] = $this->apiKey;
110
111 18
        $options = static::DEFAULT_GEOCODE_OPTIONS;
112 18
        $options[static::KEY_MAX_RESULTS] = $query->getLimit();
113
114 18
        $useGetQuery = true;
115
116 18
        $address = $this->extractAddressFromQuery($query);
117 18
        if ($address instanceof Location) {
118 3
            $params[static::KEY_LOCATION] = $this->mapAddressToArray($address);
119 3
            $options[static::KEY_INTL_MODE] = static::MODE_5BOX;
120 3
            $useGetQuery = false;
121
        } else {
122 15
            $addressAsText = $query->getText();
123
124 15
            if (!$addressAsText) {
125
                throw new InvalidArgument('Cannot geocode empty address');
126
            }
127
128
            // This API doesn't handle IPs
129 15
            if (filter_var($addressAsText, FILTER_VALIDATE_IP)) {
130 4
                throw new UnsupportedOperation('The MapQuest provider does not support IP addresses, only street addresses.');
131
            }
132
133 11
            $params[static::KEY_LOCATION] = $addressAsText;
134
        }
135
136 14
        $bounds = $query->getBounds();
137 14
        if ($bounds instanceof Bounds) {
138 1
            $options[static::KEY_BOUNDING_BOX] = $this->mapBoundsToArray($bounds);
139 1
            $useGetQuery = false;
140
        }
141
142 14
        if ($useGetQuery) {
143 11
            $params = $this->addOptionsForGetQuery($params, $options);
144
145 11
            return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params);
146
        } else {
147 3
            $params = $this->addOptionsForPostQuery($params, $options);
148
149 3
            return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params);
150
        }
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156 9
    public function reverseQuery(ReverseQuery $query): Collection
157
    {
158 9
        $coordinates = $query->getCoordinates();
159 9
        $longitude = $coordinates->getLongitude();
160 9
        $latitude = $coordinates->getLatitude();
161
162
        $params = [
163 9
            static::KEY_API_KEY => $this->apiKey,
164 9
            static::KEY_LAT => $latitude,
165 9
            static::KEY_LNG => $longitude,
166
        ];
167
168 9
        return $this->executeGetQuery(static::REVERSE_ENDPOINT, $params);
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 10
    public function getName(): string
175
    {
176 10
        return 'map_quest';
177
    }
178
179 18
    private function extractAddressFromQuery(GeocodeQuery $query)
180
    {
181 18
        return $query->getData(static::DATA_KEY_ADDRESS);
182
    }
183
184 23
    private function getUrl($endpoint): string
185
    {
186 23
        if ($this->licensed) {
187
            $baseUrl = static::LICENSED_BASE_URL;
188
        } else {
189 23
            $baseUrl = static::OPEN_BASE_URL;
190
        }
191
192 23
        return $baseUrl.$endpoint;
193
    }
194
195 20
    private function addGetQuery(string $url, array $params): string
196
    {
197 20
        return $url.'?'.http_build_query($params, '', '&', PHP_QUERY_RFC3986);
198
    }
199
200 11
    private function addOptionsForGetQuery(array $params, array $options): array
201
    {
202 11
        foreach ($options as $key => $value) {
203 11
            if (false === $value) {
204 11
                $value = 'false';
205 11
            } elseif (true === $value) {
206
                $value = 'true';
207
            }
208 11
            $params[$key] = $value;
209
        }
210
211 11
        return $params;
212
    }
213
214 3
    private function addOptionsForPostQuery(array $params, array $options): array
215
    {
216 3
        $params['options'] = $options;
217
218 3
        return $params;
219
    }
220
221 3
    private function executePostQuery(string $endpoint, array $params)
222
    {
223 3
        $url = $this->getUrl($endpoint);
224
225 3
        $appKey = $params[static::KEY_API_KEY];
226 3
        unset($params[static::KEY_API_KEY]);
227 3
        $url .= '?key='.$appKey;
228
229 3
        $requestBody = json_encode($params);
230 3
        $request = $this->getMessageFactory()->createRequest('POST', $url, [], $requestBody);
231
232 3
        $response = $this->getHttpClient()->sendRequest($request);
233 3
        $content = $this->parseHttpResponse($response, $url);
234
235 3
        return $this->parseResponseContent($content);
236
    }
237
238
    /**
239
     * @param string $url
0 ignored issues
show
Bug introduced by
There is no parameter named $url. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
240
     *
241
     * @return AddressCollection
242
     */
243 20
    private function executeGetQuery(string $endpoint, array $params): AddressCollection
244
    {
245 20
        $baseUrl = $this->getUrl($endpoint);
246 20
        $url = $this->addGetQuery($baseUrl, $params);
247
248 20
        $content = $this->getUrlContents($url);
249
250 10
        return $this->parseResponseContent($content);
251
    }
252
253 13
    private function parseResponseContent(string $content): AddressCollection
254
    {
255 13
        $json = json_decode($content, true);
256
257 13
        if (!isset($json['results']) || empty($json['results'])) {
258 1
            return new AddressCollection([]);
259
        }
260
261 12
        $locations = $json['results'][0]['locations'];
262
263 12
        if (empty($locations)) {
264
            return new AddressCollection([]);
265
        }
266
267 12
        $results = [];
268 12
        foreach ($locations as $location) {
269 12
            if ($location['street'] || $location['postalCode'] || $location['adminArea5'] || $location['adminArea4'] || $location['adminArea3']) {
270 9
                $admins = [];
271
272 9
                $state = $location['adminArea3'];
273 9
                if ($state) {
274 9
                    $code = null;
275 9
                    if (2 == strlen($state)) {
276 4
                        $code = $state;
277
                    }
278 9
                    $admins[] = [
279 9
                        'name' => $state,
280 9
                        'code' => $code,
281 9
                        'level' => static::ADMIN_LEVEL_STATE,
282
                    ];
283
                }
284
285 9
                if ($location['adminArea4']) {
286 6
                    $admins[] = ['name' => $location['adminArea4'], 'level' => static::ADMIN_LEVEL_COUNTY];
287
                }
288
289 9
                $position = $location['latLng'];
290 9
                if (!$this->useRoadPosition) {
291 9
                    if ($location['displayLatLng']) {
292 9
                        $position = $location['displayLatLng'];
293
                    }
294
                }
295
296 9
                $results[] = Address::createFromArray([
297 12
                    'providedBy' => $this->getName(),
298 9
                    'latitude' => $position['lat'],
299 9
                    'longitude' => $position['lng'],
300 9
                    'streetName' => $location['street'] ?: null,
301 9
                    'locality' => $location['adminArea5'] ?: null,
302 9
                    'subLocality' => $location['adminArea6'] ?: null,
303 9
                    'postalCode' => $location['postalCode'] ?: null,
304 9
                    'adminLevels' => $admins,
305 9
                    'country' => $location['adminArea1'] ?: null,
306 9
                    'countryCode' => $location['adminArea1'] ?: null,
307
                ]);
308
            }
309
        }
310
311 12
        return new AddressCollection($results);
312
    }
313
314 3
    private function mapAddressToArray(Location $address): array
315
    {
316 3
        $location = [];
317
318
        $streetParts = [
319 3
            trim($address->getStreetNumber() ?: ''),
320 3
            trim($address->getStreetName() ?: ''),
321
        ];
322 3
        $street = implode(' ', array_filter($streetParts));
323 3
        if ($street) {
324 1
            $location['street'] = $street;
325
        }
326
327 3
        if ($address->getSubLocality()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $address->getSubLocality() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
328 1
            $location['adminArea6'] = $address->getSubLocality();
329 1
            $location['adminArea6Type'] = 'Neighborhood';
330
        }
331
332 3
        if ($address->getLocality()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $address->getLocality() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
333 3
            $location['adminArea5'] = $address->getLocality();
334 3
            $location['adminArea5Type'] = 'City';
335
        }
336
337
        /** @var AdminLevel $adminLevel */
338 3
        foreach ($address->getAdminLevels() as $adminLevel) {
339 1
            switch ($adminLevel->getLevel()) {
340 1
                case static::ADMIN_LEVEL_STATE:
341 1
                    $state = $adminLevel->getCode();
342 1
                    if (!$state) {
343
                        $state = $adminLevel->getName();
344
                    }
345 1
                    $location['adminArea3'] = $state;
346 1
                    $location['adminArea3Type'] = 'State';
347
348 1
                    break;
349
                case static::ADMIN_LEVEL_COUNTY:
350
                    $county = $adminLevel->getName();
351
                    $location['adminArea4'] = $county;
352 1
                    $location['adminArea4Type'] = 'County';
353
            }
354
        }
355
356 3
        $country = $address->getCountry();
357 3
        if ($country instanceof Country) {
358 1
            $code = $country->getCode();
359 1
            if (!$code) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $code of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
360
                $code = $country->getName();
361
            }
362 1
            $location['adminArea1'] = $code;
363 1
            $location['adminArea1Type'] = 'Country';
364
        }
365
366 3
        $postalCode = $address->getPostalCode();
367 3
        if ($postalCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postalCode of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
368 1
            $location['postalCode'] = $address->getPostalCode();
369
        }
370
371 3
        return $location;
372
    }
373
374 1
    private function mapBoundsToArray(Bounds $bounds)
375
    {
376
        return [
377 1
            'ul' => [static::KEY_LAT => $bounds->getNorth(), static::KEY_LNG => $bounds->getWest()],
378 1
            'lr' => [static::KEY_LAT => $bounds->getSouth(), static::KEY_LNG => $bounds->getEast()],
379
        ];
380
    }
381
382 3
    protected function parseHttpResponse(ResponseInterface $response, string $url): string
383
    {
384 3
        $statusCode = $response->getStatusCode();
385 3
        if (401 === $statusCode || 403 === $statusCode) {
386
            throw new InvalidCredentials();
387 3
        } elseif (429 === $statusCode) {
388
            throw new QuotaExceeded();
389 3
        } elseif ($statusCode >= 300) {
390
            throw InvalidServerResponse::create($url, $statusCode);
391
        }
392
393 3
        $body = (string) $response->getBody();
394 3
        if (empty($body)) {
395
            throw InvalidServerResponse::emptyResponse($url);
396
        }
397
398 3
        return $body;
399
    }
400
}
401