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\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 Psr\Http\Client\ClientInterface;
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 ClientInterface $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 26
    public static function withOpenStreetMapServer(ClientInterface $client, string $userAgent, string $referer = ''): self
58
    {
59 26
        return new self($client, 'https://nominatim.openstreetmap.org', $userAgent, $referer);
60
    }
61
62
    /**
63
     * @param ClientInterface $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 26
    public function __construct(ClientInterface $client, $rootUrl, string $userAgent, string $referer = '')
69
    {
70 26
        parent::__construct($client);
71
72 26
        $this->rootUrl = rtrim($rootUrl, '/');
73 26
        $this->userAgent = $userAgent;
74 26
        $this->referer = $referer;
75
76 26
        if (empty($this->userAgent)) {
77
            throw new InvalidArgument('The User-Agent must be set to use the Nominatim provider.');
78
        }
79
    }
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 14
            ], '', '&', PHP_QUERY_RFC3986);
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
                ], '', '&', PHP_QUERY_RFC3986);
111
            } else {
112 1
                $url .= '&'.http_build_query([
113 1
                    'countrycodes' => strtolower($countrycodes),
114 1
                ], '', '&', PHP_QUERY_RFC3986);
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 2
            ], '', '&', PHP_QUERY_RFC3986);
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 2
                ], '', '&', PHP_QUERY_RFC3986);
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 9
    public function reverseQuery(ReverseQuery $query): Collection
155
    {
156 9
        $coordinates = $query->getCoordinates();
157 9
        $longitude = $coordinates->getLongitude();
158 9
        $latitude = $coordinates->getLatitude();
159
160 9
        $url = $this->rootUrl
161 9
            .'/reverse?'
162 9
            .http_build_query([
163 9
                'format' => 'jsonv2',
164 9
                'lat' => $latitude,
165 9
                'lon' => $longitude,
166 9
                'addressdetails' => 1,
167 9
                'zoom' => $query->getData('zoom', 18),
168 9
            ], '', '&', PHP_QUERY_RFC3986);
169
170 9
        $content = $this->executeQuery($url, $query->getLocale());
171
172 4
        $json = json_decode($content);
173 4
        if (is_null($json) || isset($json->error)) {
174 1
            return new AddressCollection([]);
175
        }
176
177 3
        if (empty($json)) {
178
            return new AddressCollection([]);
179
        }
180
181 3
        return new AddressCollection([$this->jsonResultToLocation($json, true)]);
182
    }
183
184
    /**
185
     * @param \stdClass $place
186
     * @param bool      $reverse
187
     *
188
     * @return Location
189
     */
190 11
    private function jsonResultToLocation(\stdClass $place, bool $reverse): Location
191
    {
192 11
        $builder = new AddressBuilder($this->getName());
193
194 11
        foreach (['state', 'county'] as $i => $tagName) {
195 11
            if (isset($place->address->{$tagName})) {
196 8
                $builder->addAdminLevel($i + 1, $place->address->{$tagName}, '');
197
            }
198
        }
199
200
        // get the first postal-code when there are many
201 11
        if (isset($place->address->postcode)) {
202 10
            $postalCode = $place->address->postcode;
203 10
            if (!empty($postalCode)) {
204 10
                $postalCode = current(explode(';', $postalCode));
205
            }
206 10
            $builder->setPostalCode($postalCode);
207
        }
208
209 11
        $localityFields = ['city', 'town', 'village', 'hamlet'];
210 11
        foreach ($localityFields as $localityField) {
211 11
            if (isset($place->address->{$localityField})) {
212 10
                $localityFieldContent = $place->address->{$localityField};
213
214 10
                if (!empty($localityFieldContent)) {
215 10
                    $builder->setLocality($localityFieldContent);
216
217 10
                    break;
218
                }
219
            }
220
        }
221
222 11
        $builder->setStreetName($place->address->road ?? $place->address->pedestrian ?? null);
223 11
        $builder->setStreetNumber($place->address->house_number ?? null);
224 11
        $builder->setSubLocality($place->address->suburb ?? null);
225 11
        $builder->setCountry($place->address->country ?? null);
226 11
        $builder->setCountryCode(isset($place->address->country_code) ? strtoupper($place->address->country_code) : null);
227
228 11
        $builder->setCoordinates(floatval($place->lat), floatval($place->lon));
229
230 11
        $builder->setBounds($place->boundingbox[0], $place->boundingbox[2], $place->boundingbox[1], $place->boundingbox[3]);
231
232
        /** @var NominatimAddress $location */
233 11
        $location = $builder->build(NominatimAddress::class);
234 11
        $location = $location->withAttribution($place->licence);
235 11
        $location = $location->withDisplayName($place->display_name);
236
237 11
        $includedAddressKeys = ['city', 'town', 'village', 'state', 'county', 'hamlet', 'postcode', 'road', 'pedestrian', 'house_number', 'suburb', 'country', 'country_code', 'quarter'];
238
239 11
        $location = $location->withDetails(array_diff_key((array) $place->address, array_flip($includedAddressKeys)));
240
241 11
        if (isset($place->extratags)) {
242 8
            $location = $location->withTags((array) $place->extratags);
243
        }
244 11
        if (isset($place->address->quarter)) {
245 2
            $location = $location->withQuarter($place->address->quarter);
246
        }
247 11
        if (isset($place->address->neighbourhood)) {
248 6
            $location = $location->withNeighbourhood($place->address->neighbourhood);
249
        }
250 11
        if (isset($place->osm_id)) {
251 10
            $location = $location->withOSMId(intval($place->osm_id));
252
        }
253 11
        if (isset($place->osm_type)) {
254 10
            $location = $location->withOSMType($place->osm_type);
255
        }
256
257 11
        if (false === $reverse) {
258 8
            $location = $location->withCategory($place->category);
259 8
            $location = $location->withType($place->type);
260
        }
261
262 11
        return $location;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 11
    public function getName(): string
269
    {
270 11
        return 'nominatim';
271
    }
272
273
    /**
274
     * @param string      $url
275
     * @param string|null $locale
276
     *
277
     * @return string
278
     */
279 23
    private function executeQuery(string $url, string $locale = null): string
280
    {
281 23
        if (null !== $locale) {
282 4
            $url .= '&'.http_build_query([
283 4
                'accept-language' => $locale,
284 4
            ], '', '&', PHP_QUERY_RFC3986);
285
        }
286
287 23
        $request = $this->getRequest($url);
288 23
        $request = $request->withHeader('User-Agent', $this->userAgent);
289
290 23
        if (!empty($this->referer)) {
291
            $request = $request->withHeader('Referer', $this->referer);
292
        }
293
294 23
        return $this->getParsedResponse($request);
295
    }
296
}
297