Completed
Push — master ( e047c3...38d929 )
by Tobias
01:27
created

Nominatim.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Nominatim;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\UnsupportedOperation;
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 Geocoder\Provider\Nominatim\Model\NominatimAddress;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author Niklas Närhinen <[email protected]>
31
 * @author Jonathan Beliën <[email protected]>
32
 */
33
final class Nominatim extends AbstractHttpProvider implements Provider
34
{
35
    /**
36
     * @var string
37
     */
38
    private $rootUrl;
39
40
    /**
41
     * @var string
42
     */
43
    private $userAgent;
44
45
    /**
46
     * @var string
47
     */
48
    private $referer;
49
50
    /**
51
     * @param HttpClient  $client
52
     * @param string|null $locale
0 ignored issues
show
There is no parameter named $locale. 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...
53
     *
54
     * @return Nominatim
55
     */
56 28
    public static function withOpenStreetMapServer(HttpClient $client, string $userAgent, string $referer = '')
57
    {
58 28
        return new self($client, 'https://nominatim.openstreetmap.org', $userAgent, $referer);
59
    }
60
61
    /**
62
     * @param HttpClient $client  an HTTP adapter
63
     * @param string     $rootUrl Root URL of the nominatim server
64
     */
65 28
    public function __construct(HttpClient $client, $rootUrl, string $userAgent, string $referer = '')
66
    {
67 28
        parent::__construct($client);
68
69 28
        $this->rootUrl = rtrim($rootUrl, '/');
70 28
        $this->userAgent = $userAgent;
71 28
        $this->referer = $referer;
72
73 28
        if (empty($this->userAgent)) {
74
            throw new InvalidArgument('The User-Agent must be set to use the Nominatim provider.');
75
        }
76 28
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 19
    public function geocodeQuery(GeocodeQuery $query): Collection
82
    {
83 19
        $address = $query->getText();
84
        // This API does not support IPv6
85 19
        if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
86 2
            throw new UnsupportedOperation('The Nominatim provider does not support IPv6 addresses.');
87
        }
88
89 17
        if ('127.0.0.1' === $address) {
90 1
            return new AddressCollection([$this->getLocationForLocalhost()]);
91
        }
92
93 16
        $url = sprintf($this->getGeocodeEndpointUrl(), urlencode($address), $query->getLimit());
94 16
        $content = $this->executeQuery($url, $query->getLocale());
95
96 6
        $doc = new \DOMDocument();
97 6
        if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
98 2
            throw InvalidServerResponse::create($url);
99
        }
100
101 4
        $searchResult = $doc->getElementsByTagName('searchresults')->item(0);
102 4
        $attribution = $searchResult->getAttribute('attribution');
103 4
        $places = $searchResult->getElementsByTagName('place');
104
105 4
        if (null === $places || 0 === $places->length) {
106 1
            return new AddressCollection([]);
107
        }
108
109 3
        $results = [];
110 3
        foreach ($places as $place) {
111 3
            $results[] = $this->xmlResultToArray($place, $place, $attribution, false);
112
        }
113
114 3
        return new AddressCollection($results);
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 9
    public function reverseQuery(ReverseQuery $query): Collection
121
    {
122 9
        $coordinates = $query->getCoordinates();
123 9
        $longitude = $coordinates->getLongitude();
124 9
        $latitude = $coordinates->getLatitude();
125 9
        $url = sprintf($this->getReverseEndpointUrl(), $latitude, $longitude, $query->getData('zoom', 18));
126 9
        $content = $this->executeQuery($url, $query->getLocale());
127
128 4
        $doc = new \DOMDocument();
129 4
        if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
130 2
            return new AddressCollection([]);
131
        }
132
133 2
        $searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
134 2
        $attribution = $searchResult->getAttribute('attribution');
135 2
        $addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
136 2
        $result = $searchResult->getElementsByTagName('result')->item(0);
137
138 2
        return new AddressCollection([$this->xmlResultToArray($result, $addressParts, $attribution, true)]);
139
    }
140
141
    /**
142
     * @param \DOMElement $resultNode
143
     * @param \DOMElement $addressNode
144
     *
145
     * @return Location
146
     */
147 5
    private function xmlResultToArray(\DOMElement $resultNode, \DOMElement $addressNode, string $attribution, bool $reverse): Location
148
    {
149 5
        $builder = new AddressBuilder($this->getName());
150
151 5
        foreach (['state', 'county'] as $i => $tagName) {
152 5
            if (null !== ($adminLevel = $this->getNodeValue($addressNode->getElementsByTagName($tagName)))) {
153 5
                $builder->addAdminLevel($i + 1, $adminLevel, '');
154
            }
155
        }
156
157
        // get the first postal-code when there are many
158 5
        $postalCode = $this->getNodeValue($addressNode->getElementsByTagName('postcode'));
159 5
        if (!empty($postalCode)) {
160 4
            $postalCode = current(explode(';', $postalCode));
161
        }
162 5
        $builder->setPostalCode($postalCode);
163
164 5
        $localityFields = ['city', 'town', 'village', 'hamlet'];
165 5
        foreach ($localityFields as $localityField) {
166 5
            $localityFieldContent = $this->getNodeValue($addressNode->getElementsByTagName($localityField));
167 5
            if (!empty($localityFieldContent)) {
168 5
                $builder->setLocality($localityFieldContent);
169
170 5
                break;
171
            }
172
        }
173
174 5
        $builder->setStreetName($this->getNodeValue($addressNode->getElementsByTagName('road')) ?: $this->getNodeValue($addressNode->getElementsByTagName('pedestrian')));
175 5
        $builder->setStreetNumber($this->getNodeValue($addressNode->getElementsByTagName('house_number')));
176 5
        $builder->setSubLocality($this->getNodeValue($addressNode->getElementsByTagName('suburb')));
177 5
        $builder->setCountry($this->getNodeValue($addressNode->getElementsByTagName('country')));
178
179 5
        $countryCode = $this->getNodeValue($addressNode->getElementsByTagName('country_code'));
180 5
        if (!empty($countryCode)) {
181 5
            $countryCode = strtoupper($countryCode);
182
        }
183 5
        $builder->setCountryCode($countryCode);
184
185 5
        $builder->setCoordinates($resultNode->getAttribute('lat'), $resultNode->getAttribute('lon'));
186
187 5
        $boundsAttr = $resultNode->getAttribute('boundingbox');
188 5
        if ($boundsAttr) {
189 5
            $bounds = [];
190 5
            list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = explode(',', $boundsAttr);
191 5
            $builder->setBounds($bounds['south'], $bounds['west'], $bounds['north'], $bounds['east']);
192
        }
193
194 5
        $location = $builder->build(NominatimAddress::class);
195 5
        $location = $location->withAttribution($attribution);
196 5
        $location = $location->withOSMId(intval($resultNode->getAttribute('osm_id')));
197 5
        $location = $location->withOSMType($resultNode->getAttribute('osm_type'));
198
199 5
        if (false === $reverse) {
200 3
            $location = $location->withClass($resultNode->getAttribute('class'));
201 3
            $location = $location->withDisplayName($resultNode->getAttribute('display_name'));
202 3
            $location = $location->withType($resultNode->getAttribute('type'));
203
        } else {
204 2
            $location = $location->withDisplayName($resultNode->nodeValue);
205
        }
206
207 5
        return $location;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 6
    public function getName(): string
214
    {
215 6
        return 'nominatim';
216
    }
217
218
    /**
219
     * @param string      $url
220
     * @param string|null $locale
221
     *
222
     * @return string
223
     */
224 25
    private function executeQuery(string $url, string $locale = null): string
225
    {
226 25
        if (null !== $locale) {
227 4
            $url = sprintf('%s&accept-language=%s', $url, $locale);
228
        }
229
230 25
        $request = $this->getRequest($url);
231 25
        $request = $request->withHeader('User-Agent', $this->userAgent);
232
233 25
        if (!empty($this->referer)) {
234
            $request = $request->withHeader('Referer', $this->referer);
235
        }
236
237 25
        return $this->getParsedResponse($request);
238
    }
239
240 16
    private function getGeocodeEndpointUrl(): string
241
    {
242 16
        return $this->rootUrl.'/search?q=%s&format=xml&addressdetails=1&limit=%d';
243
    }
244
245 9
    private function getReverseEndpointUrl(): string
246
    {
247 9
        return $this->rootUrl.'/reverse?format=xml&lat=%F&lon=%F&addressdetails=1&zoom=%d';
248
    }
249
250 5
    private function getNodeValue(\DOMNodeList $element)
251
    {
252 5
        return $element->length ? $element->item(0)->nodeValue : null;
253
    }
254
}
255