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

Nominatim   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 97.94%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 9
dl 0
loc 222
ccs 95
cts 97
cp 0.9794
rs 9.92
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A reverseQuery() 0 20 3
C xmlResultToArray() 0 62 10
A getName() 0 4 1
A executeQuery() 0 15 3
A getGeocodeEndpointUrl() 0 4 1
A getReverseEndpointUrl() 0 4 1
A getNodeValue() 0 4 2
A withOpenStreetMapServer() 0 4 1
A __construct() 0 12 2
B geocodeQuery() 0 32 7
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    an HTTP client
52
     * @param string     $userAgent Value of the User-Agent header
53
     * @param string     $referer   Value of the Referer header
54
     *
55
     * @return Nominatim
56
     */
57 22
    public static function withOpenStreetMapServer(HttpClient $client, string $userAgent, string $referer = '')
58
    {
59 22
        return new self($client, 'https://nominatim.openstreetmap.org', $userAgent, $referer);
60
    }
61
62
    /**
63
     * @param HttpClient $client    an HTTP client
64
     * @param string     $rootUrl   Root URL of the nominatim server
65
     * @param string     $userAgent Value of the User-Agent header
66
     * @param string     $referer   Value of the Referer header
67
     */
68 22
    public function __construct(HttpClient $client, $rootUrl, string $userAgent, string $referer = '')
69
    {
70 22
        parent::__construct($client);
71
72 22
        $this->rootUrl = rtrim($rootUrl, '/');
73 22
        $this->userAgent = $userAgent;
74 22
        $this->referer = $referer;
75
76 22
        if (empty($this->userAgent)) {
77
            throw new InvalidArgument('The User-Agent must be set to use the Nominatim provider.');
78
        }
79 22
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 13
    public function geocodeQuery(GeocodeQuery $query): Collection
85
    {
86 13
        $address = $query->getText();
87
88
        // This API doesn't handle IPs
89 13
        if (filter_var($address, FILTER_VALIDATE_IP)) {
90 3
            throw new UnsupportedOperation('The Nominatim provider does not support IP addresses.');
91
        }
92
93 10
        $url = sprintf($this->getGeocodeEndpointUrl(), urlencode($address), $query->getLimit());
94 10
        $content = $this->executeQuery($url, $query->getLocale());
95
96 5
        $doc = new \DOMDocument();
97 5
        if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
98 2
            throw InvalidServerResponse::create($url);
99
        }
100
101 3
        $searchResult = $doc->getElementsByTagName('searchresults')->item(0);
102 3
        $attribution = $searchResult->getAttribute('attribution');
103 3
        $places = $searchResult->getElementsByTagName('place');
104
105 3
        if (null === $places || 0 === $places->length) {
106 1
            return new AddressCollection([]);
107
        }
108
109 2
        $results = [];
110 2
        foreach ($places as $place) {
111 2
            $results[] = $this->xmlResultToArray($place, $place, $attribution, false);
112
        }
113
114 2
        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 4
    private function xmlResultToArray(\DOMElement $resultNode, \DOMElement $addressNode, string $attribution, bool $reverse): Location
148
    {
149 4
        $builder = new AddressBuilder($this->getName());
150
151 4
        foreach (['state', 'county'] as $i => $tagName) {
152 4
            if (null !== ($adminLevel = $this->getNodeValue($addressNode->getElementsByTagName($tagName)))) {
153 4
                $builder->addAdminLevel($i + 1, $adminLevel, '');
154
            }
155
        }
156
157
        // get the first postal-code when there are many
158 4
        $postalCode = $this->getNodeValue($addressNode->getElementsByTagName('postcode'));
159 4
        if (!empty($postalCode)) {
160 4
            $postalCode = current(explode(';', $postalCode));
161
        }
162 4
        $builder->setPostalCode($postalCode);
163
164 4
        $localityFields = ['city', 'town', 'village', 'hamlet'];
165 4
        foreach ($localityFields as $localityField) {
166 4
            $localityFieldContent = $this->getNodeValue($addressNode->getElementsByTagName($localityField));
167 4
            if (!empty($localityFieldContent)) {
168 4
                $builder->setLocality($localityFieldContent);
169
170 4
                break;
171
            }
172
        }
173
174 4
        $builder->setStreetName($this->getNodeValue($addressNode->getElementsByTagName('road')) ?: $this->getNodeValue($addressNode->getElementsByTagName('pedestrian')));
175 4
        $builder->setStreetNumber($this->getNodeValue($addressNode->getElementsByTagName('house_number')));
176 4
        $builder->setSubLocality($this->getNodeValue($addressNode->getElementsByTagName('suburb')));
177 4
        $builder->setCountry($this->getNodeValue($addressNode->getElementsByTagName('country')));
178
179 4
        $countryCode = $this->getNodeValue($addressNode->getElementsByTagName('country_code'));
180 4
        if (!empty($countryCode)) {
181 4
            $countryCode = strtoupper($countryCode);
182
        }
183 4
        $builder->setCountryCode($countryCode);
184
185 4
        $builder->setCoordinates($resultNode->getAttribute('lat'), $resultNode->getAttribute('lon'));
186
187 4
        $boundsAttr = $resultNode->getAttribute('boundingbox');
188 4
        if ($boundsAttr) {
189 4
            $bounds = [];
190 4
            list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = explode(',', $boundsAttr);
191 4
            $builder->setBounds($bounds['south'], $bounds['west'], $bounds['north'], $bounds['east']);
192
        }
193
194 4
        $location = $builder->build(NominatimAddress::class);
195 4
        $location = $location->withAttribution($attribution);
196 4
        $location = $location->withOSMId(intval($resultNode->getAttribute('osm_id')));
197 4
        $location = $location->withOSMType($resultNode->getAttribute('osm_type'));
198
199 4
        if (false === $reverse) {
200 2
            $location = $location->withClass($resultNode->getAttribute('class'));
201 2
            $location = $location->withDisplayName($resultNode->getAttribute('display_name'));
202 2
            $location = $location->withType($resultNode->getAttribute('type'));
203
        } else {
204 2
            $location = $location->withDisplayName($resultNode->nodeValue);
205
        }
206
207 4
        return $location;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 4
    public function getName(): string
214
    {
215 4
        return 'nominatim';
216
    }
217
218
    /**
219
     * @param string      $url
220
     * @param string|null $locale
221
     *
222
     * @return string
223
     */
224 19
    private function executeQuery(string $url, string $locale = null): string
225
    {
226 19
        if (null !== $locale) {
227 3
            $url = sprintf('%s&accept-language=%s', $url, $locale);
228
        }
229
230 19
        $request = $this->getRequest($url);
231 19
        $request = $request->withHeader('User-Agent', $this->userAgent);
232
233 19
        if (!empty($this->referer)) {
234
            $request = $request->withHeader('Referer', $this->referer);
235
        }
236
237 19
        return $this->getParsedResponse($request);
238
    }
239
240 10
    private function getGeocodeEndpointUrl(): string
241
    {
242 10
        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 4
    private function getNodeValue(\DOMNodeList $element)
251
    {
252 4
        return $element->length ? $element->item(0)->nodeValue : null;
253
    }
254
}
255