Completed
Push — master ( 80f39c...076a68 )
by Tobias
01:26
created

Nominatim   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 254
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 93.86%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 10
dl 0
loc 254
ccs 107
cts 114
cp 0.9386
rs 9.44
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A withOpenStreetMapServer() 0 4 1
A __construct() 0 12 2
C geocodeQuery() 0 65 13
A reverseQuery() 0 29 4
F jsonResultToLocation() 0 65 13
A getName() 0 4 1
A executeQuery() 0 17 3
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 25
    public static function withOpenStreetMapServer(HttpClient $client, string $userAgent, string $referer = ''): self
58
    {
59 25
        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 25
    public function __construct(HttpClient $client, $rootUrl, string $userAgent, string $referer = '')
69
    {
70 25
        parent::__construct($client);
71
72 25
        $this->rootUrl = rtrim($rootUrl, '/');
73 25
        $this->userAgent = $userAgent;
74 25
        $this->referer = $referer;
75
76 25
        if (empty($this->userAgent)) {
77
            throw new InvalidArgument('The User-Agent must be set to use the Nominatim provider.');
78
        }
79 25
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 16
    public function geocodeQuery(GeocodeQuery $query): Collection
85
    {
86 16
        $address = $query->getText();
87
88
        // This API doesn't handle IPs
89 16
        if (filter_var($address, FILTER_VALIDATE_IP)) {
90 3
            throw new UnsupportedOperation('The Nominatim provider does not support IP addresses.');
91
        }
92
93 13
        $url = $this->rootUrl
94 13
            .'/search?'
95 13
            .http_build_query([
96 13
                'format' => 'jsonv2',
97 13
                'q' => $address,
98 13
                'addressdetails' => 1,
99 13
                'limit' => $query->getLimit(),
100
            ]);
101
102 13
        $countrycodes = $query->getData('countrycodes');
103 13
        if (!is_null($countrycodes)) {
104 1
            if (is_array($countrycodes)) {
105
                $countrycodes = array_map('strtolower', $countrycodes);
106
107
                $url .= '&'.http_build_query([
108
                    'countrycodes' => implode(',', $countrycodes),
109
                ]);
110
            } else {
111 1
                $url .= '&'.http_build_query([
112 1
                    'countrycodes' => strtolower($countrycodes),
113
                ]);
114
            }
115
        }
116
117 13
        $viewbox = $query->getData('viewbox');
118 13
        if (!is_null($viewbox) && is_array($viewbox) && 4 === count($viewbox)) {
119 1
            $url .= '&'.http_build_query([
120 1
                'viewbox' => implode(',', $viewbox),
121
            ]);
122
123 1
            $bounded = $query->getData('bounded');
124 1
            if (!is_null($bounded) && true === $bounded) {
125 1
                $url .= '&'.http_build_query([
126 1
                    'bounded' => 1,
127
                ]);
128
            }
129
        }
130
131 13
        $content = $this->executeQuery($url, $query->getLocale());
132
133 8
        $json = json_decode($content);
134 8
        if (is_null($json) || !is_array($json)) {
135
            throw InvalidServerResponse::create($url);
136
        }
137
138 8
        if (empty($json)) {
139 1
            return new AddressCollection([]);
140
        }
141
142 7
        $results = [];
143 7
        foreach ($json as $place) {
144 7
            $results[] = $this->jsonResultToLocation($place, false);
145
        }
146
147 7
        return new AddressCollection($results);
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 9
    public function reverseQuery(ReverseQuery $query): Collection
154
    {
155 9
        $coordinates = $query->getCoordinates();
156 9
        $longitude = $coordinates->getLongitude();
157 9
        $latitude = $coordinates->getLatitude();
158
159 9
        $url = $this->rootUrl
160 9
            .'/reverse?'
161 9
            .http_build_query([
162 9
                'format' => 'jsonv2',
163 9
                'lat' => $latitude,
164 9
                'lon' => $longitude,
165 9
                'addressdetails' => 1,
166 9
                'zoom' => $query->getData('zoom', 18),
167
            ]);
168
169 9
        $content = $this->executeQuery($url, $query->getLocale());
170
171 4
        $json = json_decode($content);
172 4
        if (is_null($json) || isset($json->error)) {
173 2
            return new AddressCollection([]);
174
        }
175
176 2
        if (empty($json)) {
177
            return new AddressCollection([]);
178
        }
179
180 2
        return new AddressCollection([$this->jsonResultToLocation($json, true)]);
181
    }
182
183
    /**
184
     * @param \stdClass $place
185
     * @param bool      $reverse
186
     *
187
     * @return Location
188
     */
189 9
    private function jsonResultToLocation(\stdClass $place, bool $reverse): Location
190
    {
191 9
        $builder = new AddressBuilder($this->getName());
192
193 9
        foreach (['state', 'county'] as $i => $tagName) {
194 9
            if (isset($place->address->{$tagName})) {
195 9
                $builder->addAdminLevel($i + 1, $place->address->{$tagName}, '');
196
            }
197
        }
198
199
        // get the first postal-code when there are many
200 9
        if (isset($place->address->postcode)) {
201 8
            $postalCode = $place->address->postcode;
202 8
            if (!empty($postalCode)) {
203 8
                $postalCode = current(explode(';', $postalCode));
204
            }
205 8
            $builder->setPostalCode($postalCode);
206
        }
207
208 9
        $localityFields = ['city', 'town', 'village', 'hamlet'];
209 9
        foreach ($localityFields as $localityField) {
210 9
            if (isset($place->address->{$localityField})) {
211 9
                $localityFieldContent = $place->address->{$localityField};
212
213 9
                if (!empty($localityFieldContent)) {
214 9
                    $builder->setLocality($localityFieldContent);
215
216 9
                    break;
217
                }
218
            }
219
        }
220
221 9
        $builder->setStreetName($place->address->road ?? $place->address->pedestrian ?? null);
222 9
        $builder->setStreetNumber($place->address->house_number ?? null);
223 9
        $builder->setSubLocality($place->address->suburb ?? null);
224 9
        $builder->setCountry($place->address->country ?? null);
225 9
        $builder->setCountryCode(isset($place->address->country_code) ? strtoupper($place->address->country_code) : null);
226
227 9
        $builder->setCoordinates(floatval($place->lat), floatval($place->lon));
228
229 9
        $builder->setBounds($place->boundingbox[0], $place->boundingbox[2], $place->boundingbox[1], $place->boundingbox[3]);
230
231
        /** @var NominatimAddress $location */
232 9
        $location = $builder->build(NominatimAddress::class);
233 9
        $location = $location->withAttribution($place->licence);
234 9
        $location = $location->withDisplayName($place->display_name);
235
236 9
        if (isset($place->address->quarter)) {
237 1
            $location = $location->withQuarter($place->address->quarter);
238
        }
239
240 9
        if (isset($place->osm_id)) {
241 8
            $location = $location->withOSMId(intval($place->osm_id));
242
        }
243 9
        if (isset($place->osm_type)) {
244 8
            $location = $location->withOSMType($place->osm_type);
245
        }
246
247 9
        if (false === $reverse) {
248 7
            $location = $location->withCategory($place->category);
249 7
            $location = $location->withType($place->type);
250
        }
251
252 9
        return $location;
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258 9
    public function getName(): string
259
    {
260 9
        return 'nominatim';
261
    }
262
263
    /**
264
     * @param string      $url
265
     * @param string|null $locale
266
     *
267
     * @return string
268
     */
269 22
    private function executeQuery(string $url, string $locale = null): string
270
    {
271 22
        if (null !== $locale) {
272 3
            $url .= '&'.http_build_query([
273 3
                'accept-language' => $locale,
274
            ]);
275
        }
276
277 22
        $request = $this->getRequest($url);
278 22
        $request = $request->withHeader('User-Agent', $this->userAgent);
279
280 22
        if (!empty($this->referer)) {
281
            $request = $request->withHeader('Referer', $this->referer);
282
        }
283
284 22
        return $this->getParsedResponse($request);
285
    }
286
}
287