Pelias::getGeocodeQueryUrl()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 2
eloc 7
c 2
b 1
f 0
nc 2
nop 2
dl 0
loc 15
rs 10
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\Pelias;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidCredentials;
17
use Geocoder\Exception\QuotaExceeded;
18
use Geocoder\Exception\UnsupportedOperation;
19
use Geocoder\Model\Address;
20
use Geocoder\Model\AddressCollection;
21
use Geocoder\Query\GeocodeQuery;
22
use Geocoder\Query\ReverseQuery;
23
use Geocoder\Http\Provider\AbstractHttpProvider;
24
use Geocoder\Provider\Provider;
25
use Http\Client\HttpClient;
26
27
class Pelias extends AbstractHttpProvider implements Provider
28
{
29
    /**
30
     * @var string
31
     */
32
    protected $root;
33
34
    /**
35
     * @var int
36
     */
37
    private $version;
38
39
    /**
40
     * @param HttpClient $client  an HTTP adapter
41
     * @param string     $root    url of Pelias API
42
     * @param int        $version version of Pelias API
43
     */
44
    public function __construct(HttpClient $client, string $root, int $version = 1)
45
    {
46
        $this->root = sprintf('%s/v%d', rtrim($root, '/'), $version);
47
        $this->version = $version;
48
49
        parent::__construct($client);
50
    }
51
52
    /**
53
     * @param GeocodeQuery $query
54
     * @param array        $query_data additional query data (API key for instance)
55
     *
56
     * @return string
57
     *
58
     * @throws \Geocoder\Exception\Exception
59
     */
60
    protected function getGeocodeQueryUrl(GeocodeQuery $query, array $query_data = []): string
61
    {
62
        $address = $query->getText();
63
64
        // This API doesn't handle IPs
65
        if (filter_var($address, FILTER_VALIDATE_IP)) {
66
            throw new UnsupportedOperation(sprintf('The %s provider does not support IP addresses, only street addresses.', $this->getName()));
67
        }
68
69
        $data = [
70
            'text' => $address,
71
            'size' => $query->getLimit(),
72
        ];
73
74
        return sprintf('%s/search?%s', $this->root, http_build_query(array_merge($data, $query_data)));
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function geocodeQuery(GeocodeQuery $query): Collection
81
    {
82
        return $this->executeQuery($this->getGeocodeQueryUrl($query));
83
    }
84
85
    /**
86
     * @param ReverseQuery $query
87
     * @param array        $query_data additional query data (API key for instance)
88
     *
89
     * @return string
90
     *
91
     * @throws \Geocoder\Exception\Exception
92
     */
93
    protected function getReverseQueryUrl(ReverseQuery $query, array $query_data = []): string
94
    {
95
        $coordinates = $query->getCoordinates();
96
        $longitude = $coordinates->getLongitude();
97
        $latitude = $coordinates->getLatitude();
98
99
        $data = [
100
            'point.lat' => $latitude,
101
            'point.lon' => $longitude,
102
            'size' => $query->getLimit(),
103
        ];
104
105
        return sprintf('%s/reverse?%s', $this->root, http_build_query(array_merge($data, $query_data)));
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function reverseQuery(ReverseQuery $query): Collection
112
    {
113
        return $this->executeQuery($this->getReverseQueryUrl($query));
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function getName(): string
120
    {
121
        return 'pelias';
122
    }
123
124
    /**
125
     * @param $url
126
     *
127
     * @return Collection
128
     */
129
    protected function executeQuery(string $url): AddressCollection
130
    {
131
        $content = $this->getUrlContents($url);
132
        $json = json_decode($content, true);
133
134
        if (isset($json['meta'])) {
135
            switch ($json['meta']['status_code']) {
136
                case 401:
137
                case 403:
138
                    throw new InvalidCredentials('Invalid or missing api key.');
139
                case 429:
140
                    throw new QuotaExceeded('Valid request but quota exceeded.');
141
            }
142
        }
143
144
        if (
145
            !isset($json['type'])
146
            || 'FeatureCollection' !== $json['type']
147
            || !isset($json['features'])
148
            || 0 === count($json['features'])
149
        ) {
150
            return new AddressCollection([]);
151
        }
152
153
        $locations = $json['features'];
154
155
        if (empty($locations)) {
156
            return new AddressCollection([]);
157
        }
158
159
        $results = [];
160
        foreach ($locations as $location) {
161
            if (isset($location['bbox'])) {
162
                $bounds = [
163
                    'south' => $location['bbox'][3],
164
                    'west' => $location['bbox'][2],
165
                    'north' => $location['bbox'][1],
166
                    'east' => $location['bbox'][0],
167
                ];
168
            } else {
169
                $bounds = [
170
                    'south' => null,
171
                    'west' => null,
172
                    'north' => null,
173
                    'east' => null,
174
                ];
175
            }
176
177
            $props = $location['properties'];
178
179
            $adminLevels = [];
180
            foreach (['region', 'county', 'locality', 'macroregion', 'country'] as $i => $component) {
181
                if (isset($props[$component])) {
182
                    $adminLevels[] = ['name' => $props[$component], 'level' => $i + 1];
183
                }
184
            }
185
186
            $results[] = Address::createFromArray([
187
                'providedBy' => $this->getName(),
188
                'latitude' => $location['geometry']['coordinates'][1],
189
                'longitude' => $location['geometry']['coordinates'][0],
190
                'bounds' => $bounds,
191
                'streetNumber' => isset($props['housenumber']) ? $props['housenumber'] : null,
192
                'streetName' => isset($props['street']) ? $props['street'] : null,
193
                'subLocality' => isset($props['neighbourhood']) ? $props['neighbourhood'] : null,
194
                'locality' => isset($props['locality']) ? $props['locality'] : null,
195
                'postalCode' => isset($props['postalcode']) ? $props['postalcode'] : null,
196
                'adminLevels' => $adminLevels,
197
                'country' => isset($props['country']) ? $props['country'] : null,
198
                'countryCode' => isset($props['country_a']) ? strtoupper($props['country_a']) : null,
199
            ]);
200
        }
201
202
        return new AddressCollection($results);
203
    }
204
205
    /**
206
     * @param array $components
207
     *
208
     * @return string|null
209
     */
210
    protected function guessLocality(array $components)
211
    {
212
        $localityKeys = ['city', 'town', 'village', 'hamlet'];
213
214
        return $this->guessBestComponent($components, $localityKeys);
215
    }
216
217
    /**
218
     * @param array $components
219
     *
220
     * @return string|null
221
     */
222
    protected function guessStreetName(array $components)
223
    {
224
        $streetNameKeys = ['road', 'street', 'street_name', 'residential'];
225
226
        return $this->guessBestComponent($components, $streetNameKeys);
227
    }
228
229
    /**
230
     * @param array $components
231
     *
232
     * @return string|null
233
     */
234
    protected function guessSubLocality(array $components)
235
    {
236
        $subLocalityKeys = ['neighbourhood', 'city_district'];
237
238
        return $this->guessBestComponent($components, $subLocalityKeys);
239
    }
240
241
    /**
242
     * @param array $components
243
     * @param array $keys
244
     *
245
     * @return string|null
246
     */
247
    protected function guessBestComponent(array $components, array $keys)
248
    {
249
        foreach ($keys as $key) {
250
            if (isset($components[$key]) && !empty($components[$key])) {
251
                return $components[$key];
252
            }
253
        }
254
255
        return null;
256
    }
257
}
258