Code

< 40 %
40-60 %
> 60 %
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\Provider\Provider;
29
use Geocoder\Query\GeocodeQuery;
30
use Geocoder\Query\ReverseQuery;
31
use Psr\Http\Client\ClientInterface;
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
    public const DATA_KEY_ADDRESS = 'address';
40
41
    public const KEY_API_KEY = 'key';
42
43
    public const KEY_LOCATION = 'location';
44
45
    public const KEY_OUT_FORMAT = 'outFormat';
46
47
    public const KEY_MAX_RESULTS = 'maxResults';
48
49
    public const KEY_THUMB_MAPS = 'thumbMaps';
50
51
    public const KEY_INTL_MODE = 'intlMode';
52
53
    public const KEY_BOUNDING_BOX = 'boundingBox';
54
55
    public const KEY_LAT = 'lat';
56
57
    public const KEY_LNG = 'lng';
58
59
    public const MODE_5BOX = '5BOX';
60
61
    public const OPEN_BASE_URL = 'https://open.mapquestapi.com/geocoding/v1/';
62
63
    public const LICENSED_BASE_URL = 'https://www.mapquestapi.com/geocoding/v1/';
64
65
    public const GEOCODE_ENDPOINT = 'address';
66
67
    public const DEFAULT_GEOCODE_PARAMS = [
68
        self::KEY_LOCATION => '',
69
        self::KEY_OUT_FORMAT => 'json',
70
        self::KEY_API_KEY => '',
71
    ];
72
73
    public const DEFAULT_GEOCODE_OPTIONS = [
74
        self::KEY_MAX_RESULTS => 3,
75
        self::KEY_THUMB_MAPS => false,
76
    ];
77
78
    public const REVERSE_ENDPOINT = 'reverse';
79
80
    public const ADMIN_LEVEL_STATE = 1;
81
82
    public const ADMIN_LEVEL_COUNTY = 2;
83
84
    /**
85
     * MapQuest offers two geocoding endpoints one commercial (true) and one open (false)
86
     * More information: http://developer.mapquest.com/web/tools/getting-started/platform/licensed-vs-open.
87
     *
88
     * @var bool
89
     */
90
    private $licensed;
91
92
    /**
93
     * @var bool
94
     */
95
    private $useRoadPosition;
96
97
    /**
98
     * @var string
99
     */
100
    private $apiKey;
101
102
    /**
103
     * @param ClientInterface $client          an HTTP adapter
104
     * @param string          $apiKey          an API key
105
     * @param bool            $licensed        true to use MapQuest's licensed endpoints, default is false to use the open endpoints (optional)
106
     * @param bool            $useRoadPosition true to use nearest point on a road for the entrance, false to use map display position
107
     */
108 28
    public function __construct(ClientInterface $client, string $apiKey, bool $licensed = false, bool $useRoadPosition = false)
109
    {
110 28
        if (empty($apiKey)) {
111
            throw new InvalidCredentials('No API key provided.');
112
        }
113
114 28
        $this->apiKey = $apiKey;
115 28
        $this->licensed = $licensed;
116 28
        $this->useRoadPosition = $useRoadPosition;
117 28
        parent::__construct($client);
118
    }
119
120 18
    public function geocodeQuery(GeocodeQuery $query): Collection
121
    {
122 18
        $params = static::DEFAULT_GEOCODE_PARAMS;
123 18
        $params[static::KEY_API_KEY] = $this->apiKey;
124
125 18
        $options = static::DEFAULT_GEOCODE_OPTIONS;
126 18
        $options[static::KEY_MAX_RESULTS] = $query->getLimit();
127
128 18
        $useGetQuery = true;
129
130 18
        $address = $this->extractAddressFromQuery($query);
131 18
        if ($address instanceof Location) {
132 3
            $params[static::KEY_LOCATION] = $this->mapAddressToArray($address);
133 3
            $options[static::KEY_INTL_MODE] = static::MODE_5BOX;
134 3
            $useGetQuery = false;
135
        } else {
136 15
            $addressAsText = $query->getText();
137
138 15
            if (!$addressAsText) {
139
                throw new InvalidArgument('Cannot geocode empty address');
140
            }
141
142
            // This API doesn't handle IPs
143 15
            if (filter_var($addressAsText, FILTER_VALIDATE_IP)) {
144 4
                throw new UnsupportedOperation('The MapQuest provider does not support IP addresses, only street addresses.');
145
            }
146
147 11
            $params[static::KEY_LOCATION] = $addressAsText;
148
        }
149
150 14
        $bounds = $query->getBounds();
151 14
        if ($bounds instanceof Bounds) {
152 1
            $options[static::KEY_BOUNDING_BOX] = $this->mapBoundsToArray($bounds);
153 1
            $useGetQuery = false;
154
        }
155
156 14
        if ($useGetQuery) {
157 11
            $params = $this->addOptionsForGetQuery($params, $options);
158
159 11
            return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params);
160
        } else {
161 3
            $params = $this->addOptionsForPostQuery($params, $options);
162
163 3
            return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params);
164
        }
165
    }
166
167 9
    public function reverseQuery(ReverseQuery $query): Collection
168
    {
169 9
        $coordinates = $query->getCoordinates();
170 9
        $longitude = $coordinates->getLongitude();
171 9
        $latitude = $coordinates->getLatitude();
172
173 9
        $params = [
174 9
            static::KEY_API_KEY => $this->apiKey,
175 9
            static::KEY_LAT => $latitude,
176 9
            static::KEY_LNG => $longitude,
177 9
        ];
178
179 9
        return $this->executeGetQuery(static::REVERSE_ENDPOINT, $params);
180
    }
181
182 10
    public function getName(): string
183
    {
184 10
        return 'map_quest';
185
    }
186
187 18
    private function extractAddressFromQuery(GeocodeQuery $query)
188
    {
189 18
        return $query->getData(static::DATA_KEY_ADDRESS);
190
    }
191
192 23
    private function getUrl($endpoint): string
193
    {
194 23
        if ($this->licensed) {
195
            $baseUrl = static::LICENSED_BASE_URL;
196
        } else {
197 23
            $baseUrl = static::OPEN_BASE_URL;
198
        }
199
200 23
        return $baseUrl.$endpoint;
201
    }
202
203 20
    private function addGetQuery(string $url, array $params): string
204
    {
205 20
        return $url.'?'.http_build_query($params, '', '&', PHP_QUERY_RFC3986);
206
    }
207
208 11
    private function addOptionsForGetQuery(array $params, array $options): array
209
    {
210 11
        foreach ($options as $key => $value) {
211 11
            if (false === $value) {
212 11
                $value = 'false';
213 11
            } elseif (true === $value) {
214
                $value = 'true';
215
            }
216 11
            $params[$key] = $value;
217
        }
218
219 11
        return $params;
220
    }
221
222 3
    private function addOptionsForPostQuery(array $params, array $options): array
223
    {
224 3
        $params['options'] = $options;
225
226 3
        return $params;
227
    }
228
229 3
    private function executePostQuery(string $endpoint, array $params)
230
    {
231 3
        $url = $this->getUrl($endpoint);
232
233 3
        $appKey = $params[static::KEY_API_KEY];
234 3
        unset($params[static::KEY_API_KEY]);
235 3
        $url .= '?key='.$appKey;
236
237 3
        $requestBody = json_encode($params);
238 3
        $request = $this->getMessageFactory()->createRequest('POST', $url, [], $requestBody);
239
240 3
        $response = $this->getHttpClient()->sendRequest($request);
241 3
        $content = $this->parseHttpResponse($response, $url);
242
243 3
        return $this->parseResponseContent($content);
244
    }
245
246 20
    private function executeGetQuery(string $endpoint, array $params): AddressCollection
247
    {
248 20
        $baseUrl = $this->getUrl($endpoint);
249 20
        $url = $this->addGetQuery($baseUrl, $params);
250
251 20
        $content = $this->getUrlContents($url);
252
253 10
        return $this->parseResponseContent($content);
254
    }
255
256 13
    private function parseResponseContent(string $content): AddressCollection
257
    {
258 13
        $json = json_decode($content, true);
259
260 13
        if (!isset($json['results']) || empty($json['results'])) {
261 1
            return new AddressCollection([]);
262
        }
263
264 12
        $locations = $json['results'][0]['locations'];
265
266 12
        if (empty($locations)) {
267
            return new AddressCollection([]);
268
        }
269
270 12
        $results = [];
271 12
        foreach ($locations as $location) {
272 12
            if ($location['street'] || $location['postalCode'] || $location['adminArea5'] || $location['adminArea4'] || $location['adminArea3']) {
273 9
                $admins = [];
274
275 9
                $state = $location['adminArea3'];
276 9
                if ($state) {
277 9
                    $code = null;
278 9
                    if (2 == strlen($state)) {
279 4
                        $code = $state;
280
                    }
281 9
                    $admins[] = [
282 9
                        'name' => $state,
283 9
                        'code' => $code,
284 9
                        'level' => static::ADMIN_LEVEL_STATE,
285 9
                    ];
286
                }
287
288 9
                if ($location['adminArea4']) {
289 6
                    $admins[] = ['name' => $location['adminArea4'], 'level' => static::ADMIN_LEVEL_COUNTY];
290
                }
291
292 9
                $position = $location['latLng'];
293 9
                if (!$this->useRoadPosition) {
294 9
                    if ($location['displayLatLng']) {
295 9
                        $position = $location['displayLatLng'];
296
                    }
297
                }
298
299 9
                $results[] = Address::createFromArray([
300 9
                    'providedBy' => $this->getName(),
301 9
                    'latitude' => $position['lat'],
302 9
                    'longitude' => $position['lng'],
303 9
                    'streetName' => $location['street'] ?: null,
304 9
                    'locality' => $location['adminArea5'] ?: null,
305 9
                    'subLocality' => $location['adminArea6'] ?: null,
306 9
                    'postalCode' => $location['postalCode'] ?: null,
307 9
                    'adminLevels' => $admins,
308 9
                    'country' => $location['adminArea1'] ?: null,
309 9
                    'countryCode' => $location['adminArea1'] ?: null,
310 9
                ]);
311
            }
312
        }
313
314 12
        return new AddressCollection($results);
315
    }
316
317 3
    private function mapAddressToArray(Location $address): array
318
    {
319 3
        $location = [];
320
321 3
        $streetParts = [
322 3
            trim($address->getStreetNumber() ?: ''),
323 3
            trim($address->getStreetName() ?: ''),
324 3
        ];
325 3
        $street = implode(' ', array_filter($streetParts));
326 3
        if ($street) {
327 1
            $location['street'] = $street;
328
        }
329
330 3
        if ($address->getSubLocality()) {
331 1
            $location['adminArea6'] = $address->getSubLocality();
332 1
            $location['adminArea6Type'] = 'Neighborhood';
333
        }
334
335 3
        if ($address->getLocality()) {
336 3
            $location['adminArea5'] = $address->getLocality();
337 3
            $location['adminArea5Type'] = 'City';
338
        }
339
340
        /** @var AdminLevel $adminLevel */
341 3
        foreach ($address->getAdminLevels() as $adminLevel) {
342 1
            switch ($adminLevel->getLevel()) {
343 1
                case static::ADMIN_LEVEL_STATE:
344 1
                    $state = $adminLevel->getCode();
345 1
                    if (!$state) {
346
                        $state = $adminLevel->getName();
347
                    }
348 1
                    $location['adminArea3'] = $state;
349 1
                    $location['adminArea3Type'] = 'State';
350
351 1
                    break;
352
                case static::ADMIN_LEVEL_COUNTY:
353
                    $county = $adminLevel->getName();
354
                    $location['adminArea4'] = $county;
355
                    $location['adminArea4Type'] = 'County';
356
            }
357
        }
358
359 3
        $country = $address->getCountry();
360 3
        if ($country instanceof Country) {
361 1
            $code = $country->getCode();
362 1
            if (!$code) {
363
                $code = $country->getName();
364
            }
365 1
            $location['adminArea1'] = $code;
366 1
            $location['adminArea1Type'] = 'Country';
367
        }
368
369 3
        $postalCode = $address->getPostalCode();
370 3
        if ($postalCode) {
371 1
            $location['postalCode'] = $address->getPostalCode();
372
        }
373
374 3
        return $location;
375
    }
376
377 1
    private function mapBoundsToArray(Bounds $bounds)
378
    {
379 1
        return [
380 1
            'ul' => [static::KEY_LAT => $bounds->getNorth(), static::KEY_LNG => $bounds->getWest()],
381 1
            'lr' => [static::KEY_LAT => $bounds->getSouth(), static::KEY_LNG => $bounds->getEast()],
382 1
        ];
383
    }
384
385 3
    protected function parseHttpResponse(ResponseInterface $response, string $url): string
386
    {
387 3
        $statusCode = $response->getStatusCode();
388 3
        if (401 === $statusCode || 403 === $statusCode) {
389
            throw new InvalidCredentials();
390 3
        } elseif (429 === $statusCode) {
391
            throw new QuotaExceeded();
392 3
        } elseif ($statusCode >= 300) {
393
            throw InvalidServerResponse::create($url, $statusCode);
394
        }
395
396 3
        $body = (string) $response->getBody();
397 3
        if (empty($body)) {
398
            throw InvalidServerResponse::emptyResponse($url);
399
        }
400
401 3
        return $body;
402
    }
403
}
404