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\LocationIQ;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\InvalidCredentials;
19
use Geocoder\Location;
20
use Geocoder\Model\AddressBuilder;
21
use Geocoder\Model\AddressCollection;
22
use Geocoder\Query\GeocodeQuery;
23
use Geocoder\Query\ReverseQuery;
24
use Geocoder\Http\Provider\AbstractHttpProvider;
25
use Geocoder\Provider\Provider;
26
use Http\Client\HttpClient;
27
28
/**
29
 * @author Srihari Thalla <[email protected]>
30
 */
31
final class LocationIQ extends AbstractHttpProvider implements Provider
32
{
33
    /**
34
     * @var string
35
     */
36
    const BASE_API_URL = 'https://{region}.locationiq.com/v1';
37
38
    /**
39
     * @var string
40
     */
41
    protected $baseUrl;
42
43
    /**
44
     * @var array
45
     */
46
    protected $regions = [
47
        'us1',
48
        'eu1',
49
    ];
50
51
    /**
52
     * @var string
53
     */
54
    private $apiKey;
55
56
    /**
57
     * @param HttpClient $client an HTTP adapter
58
     * @param string     $apiKey an API key
59
     */
60 18
    public function __construct(HttpClient $client, string $apiKey, string $region = null)
61
    {
62 18
        if (empty($apiKey)) {
63
            throw new InvalidCredentials('No API key provided.');
64
        }
65
66 18
        $this->apiKey = $apiKey;
67 18
        if (null === $region) {
68 17
            $region = $this->regions[0];
69 1
        } elseif (true !== in_array($region, $this->regions, true)) {
70 1
            throw new InvalidArgument(sprintf('`region` must be null or one of `%s`', implode('`, `', $this->regions)));
71
        }
72 17
        $this->baseUrl = str_replace('{region}', $region, self::BASE_API_URL);
73
74 17
        parent::__construct($client);
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 9
    public function geocodeQuery(GeocodeQuery $query): Collection
81
    {
82 9
        $address = $query->getText();
83
84 9
        $url = sprintf($this->getGeocodeEndpointUrl(), urlencode($address), $query->getLimit());
85
86 9
        $content = $this->executeQuery($url, $query->getLocale());
87
88 4
        $doc = new \DOMDocument();
89 4
        if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
90 2
            throw InvalidServerResponse::create($url);
91
        }
92
93 2
        $searchResult = $doc->getElementsByTagName('searchresults')->item(0);
94 2
        $places = $searchResult->getElementsByTagName('place');
95
96 2
        if (null === $places || 0 === $places->length) {
97 1
            return new AddressCollection([]);
98
        }
99
100 1
        $results = [];
101 1
        foreach ($places as $place) {
102 1
            $results[] = $this->xmlResultToArray($place, $place);
103
        }
104
105 1
        return new AddressCollection($results);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 8
    public function reverseQuery(ReverseQuery $query): Collection
112
    {
113 8
        $coordinates = $query->getCoordinates();
114 8
        $longitude = $coordinates->getLongitude();
115 8
        $latitude = $coordinates->getLatitude();
116 8
        $url = sprintf($this->getReverseEndpointUrl(), $latitude, $longitude, $query->getData('zoom', 18));
117 8
        $content = $this->executeQuery($url, $query->getLocale());
118
119 3
        $doc = new \DOMDocument();
120 3
        if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
121 1
            return new AddressCollection([]);
122
        }
123
124 2
        $searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
125 2
        $addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
126 2
        $result = $searchResult->getElementsByTagName('result')->item(0);
127
128 2
        return new AddressCollection([$this->xmlResultToArray($result, $addressParts)]);
129
    }
130
131
    /**
132
     * @param \DOMElement $resultNode
133
     * @param \DOMElement $addressNode
134
     *
135
     * @return Location
136
     */
137 3
    private function xmlResultToArray(\DOMElement $resultNode, \DOMElement $addressNode): Location
138
    {
139 3
        $builder = new AddressBuilder($this->getName());
140
141 3
        foreach (['state', 'county'] as $i => $tagName) {
142 3
            if (null !== ($adminLevel = $this->getNodeValue($addressNode->getElementsByTagName($tagName)))) {
143 3
                $builder->addAdminLevel($i + 1, $adminLevel, '');
144
            }
145
        }
146
147
        // get the first postal-code when there are many
148 3
        $postalCode = $this->getNodeValue($addressNode->getElementsByTagName('postcode'));
149 3
        if (!empty($postalCode)) {
150 3
            $postalCode = current(explode(';', $postalCode));
151
        }
152 3
        $builder->setPostalCode($postalCode);
153 3
        $builder->setStreetName($this->getNodeValue($addressNode->getElementsByTagName('road')) ?: $this->getNodeValue($addressNode->getElementsByTagName('pedestrian')));
154 3
        $builder->setStreetNumber($this->getNodeValue($addressNode->getElementsByTagName('house_number')));
155 3
        $builder->setLocality($this->getNodeValue($addressNode->getElementsByTagName('city')));
156 3
        $builder->setSubLocality($this->getNodeValue($addressNode->getElementsByTagName('suburb')));
157 3
        $builder->setCountry($this->getNodeValue($addressNode->getElementsByTagName('country')));
158 3
        $builder->setCoordinates($resultNode->getAttribute('lat'), $resultNode->getAttribute('lon'));
159
160 3
        $countryCode = $this->getNodeValue($addressNode->getElementsByTagName('country_code'));
161 3
        if (!is_null($countryCode)) {
162 3
            $builder->setCountryCode(strtoupper($countryCode));
163
        }
164
165 3
        $boundsAttr = $resultNode->getAttribute('boundingbox');
166 3
        if ($boundsAttr) {
167 3
            $bounds = [];
168 3
            list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = explode(',', $boundsAttr);
169 3
            $builder->setBounds($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']);
170
        }
171
172 3
        return $builder->build();
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 3
    public function getName(): string
179
    {
180 3
        return 'locationiq';
181
    }
182
183
    /**
184
     * @param string      $url
185
     * @param string|null $locale
186
     *
187
     * @return string
188
     */
189 17
    private function executeQuery(string $url, string $locale = null): string
190
    {
191 17
        if (null !== $locale) {
192 3
            $url = sprintf('%s&accept-language=%s', $url, $locale);
193
        }
194
195 17
        return $this->getUrlContents($url);
196
    }
197
198 9
    private function getGeocodeEndpointUrl(): string
199
    {
200 9
        return $this->baseUrl.'/search.php?q=%s&format=xmlv1.1&addressdetails=1&normalizecity=1&limit=%d&key='.$this->apiKey;
201
    }
202
203 8
    private function getReverseEndpointUrl(): string
204
    {
205 8
        return $this->baseUrl.'/reverse.php?format=xmlv1.1&lat=%F&lon=%F&addressdetails=1&normalizecity=1&zoom=%d&key='.$this->apiKey;
206
    }
207
208 3
    private function getNodeValue(\DOMNodeList $element)
209
    {
210 3
        return $element->length ? $element->item(0)->nodeValue : null;
211
    }
212
}
213