Completed
Push — master ( 076a68...8610b7 )
by Tobias
01:12
created

Nominatim::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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