Skip to content

Commit 2b9b360

Browse files
authored
New Pelias provider (#1004)
* Initial commit * Update Pelias.php Update provider name in UnsupportedOperation exception. * Update Pelias.php Don't make it an abstract class. * Add (simple) tests There is no default instance for the Pelias API. * Apply fixes from StyleCI * Apply fixes from StyleCI * Update documentation * Update PeliasTest.php @ExpectedException is not supported anymore. See https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#testing-exceptions * Update Pelias.php Make the UnsupportedOperation exception message contain the current provider name. * Update Pelias.php Location::getBounds will return null or a Bounds object with coordinates data. It will never return Bounds without data. * Update Pelias.php * Update README.md Add link to Pelias install documentation.
1 parent 3c68d6d commit 2b9b360

File tree

8 files changed

+531
-0
lines changed

8 files changed

+531
-0
lines changed

src/Provider/Pelias/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Change Log
2+
3+
The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.
4+
5+
## 1.0.0
6+
7+
First release of this provider.

src/Provider/Pelias/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2011 — William Durand <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

src/Provider/Pelias/Pelias.php

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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+
private $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(
67+
sprintf(
68+
'The %s provider does not support IP addresses, only street addresses.',
69+
$this->getName()
70+
)
71+
);
72+
}
73+
74+
$data = [
75+
'text' => $address,
76+
'size' => $query->getLimit(),
77+
];
78+
79+
return sprintf('%s/search?%s', $this->root, http_build_query(array_merge($data, $query_data)));
80+
}
81+
82+
/**
83+
* {@inheritdoc}
84+
*/
85+
public function geocodeQuery(GeocodeQuery $query): Collection
86+
{
87+
return $this->executeQuery($this->getGeocodeQueryUrl($query));
88+
}
89+
90+
/**
91+
* @param ReverseQuery $query
92+
* @param array $query_data Additional query data (API key for instance).
93+
*
94+
* @return string
95+
*
96+
* @throws \Geocoder\Exception\Exception
97+
*/
98+
protected function getReverseQueryUrl(ReverseQuery $query, array $query_data = []): string
99+
{
100+
$coordinates = $query->getCoordinates();
101+
$longitude = $coordinates->getLongitude();
102+
$latitude = $coordinates->getLatitude();
103+
104+
$data = [
105+
'point.lat' => $latitude,
106+
'point.lon' => $longitude,
107+
'size' => $query->getLimit(),
108+
];
109+
110+
return sprintf('%s/reverse?%s', $this->root, http_build_query(array_merge($data, $query_data)));
111+
}
112+
113+
/**
114+
* {@inheritdoc}
115+
*/
116+
public function reverseQuery(ReverseQuery $query): Collection
117+
{
118+
return $this->executeQuery($this->getReverseQueryUrl($query));
119+
}
120+
121+
/**
122+
* {@inheritdoc}
123+
*/
124+
public function getName(): string
125+
{
126+
return 'pelias';
127+
}
128+
129+
/**
130+
* @param $url
131+
*
132+
* @return Collection
133+
*/
134+
protected function executeQuery(string $url): AddressCollection
135+
{
136+
$content = $this->getUrlContents($url);
137+
$json = json_decode($content, true);
138+
139+
if (isset($json['meta'])) {
140+
switch ($json['meta']['status_code']) {
141+
case 401:
142+
case 403:
143+
throw new InvalidCredentials('Invalid or missing api key.');
144+
case 429:
145+
throw new QuotaExceeded('Valid request but quota exceeded.');
146+
}
147+
}
148+
149+
if (
150+
!isset($json['type'])
151+
|| 'FeatureCollection' !== $json['type']
152+
|| !isset($json['features'])
153+
|| 0 === count($json['features'])
154+
) {
155+
return new AddressCollection([]);
156+
}
157+
158+
$locations = $json['features'];
159+
160+
if (empty($locations)) {
161+
return new AddressCollection([]);
162+
}
163+
164+
$results = [];
165+
foreach ($locations as $location) {
166+
if (isset($location['bbox'])) {
167+
$bounds = [
168+
'south' => $location['bbox'][3],
169+
'west' => $location['bbox'][2],
170+
'north' => $location['bbox'][1],
171+
'east' => $location['bbox'][0],
172+
];
173+
} else {
174+
$bounds = null;
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 null|string
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 null|string
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 null|string
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 null|string
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+
}

src/Provider/Pelias/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Pelias Geocoder provider
2+
3+
[![Build Status](https://travis-ci.org/geocoder-php/pelias-provider.svg?branch=master)](http://travis-ci.org/geocoder-php/pelias-provider)
4+
[![Latest Stable Version](https://poser.pugx.org/geocoder-php/pelias-provider/v/stable)](https://packagist.org/packages/geocoder-php/pelias-provider)
5+
[![Total Downloads](https://poser.pugx.org/geocoder-php/pelias-provider/downloads)](https://packagist.org/packages/geocoder-php/pelias-provider)
6+
[![Monthly Downloads](https://poser.pugx.org/geocoder-php/pelias-provider/d/monthly.png)](https://packagist.org/packages/geocoder-php/pelias-provider)
7+
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/pelias-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/pelias-provider)
8+
[![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/pelias-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/pelias-provider)
9+
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
10+
11+
This is the Pelias provider from the PHP Geocoder. This is a **READ ONLY** repository. See the
12+
[main repo](https://github.com/geocoder-php/Geocoder) for information and documentation.
13+
14+
## Pelias
15+
16+
Pelias is an open-source geocoder. You can find its [documentation here](https://github.com/pelias/documentation).
17+
18+
Pelias does not provide an API per se but other API (and providers) are based on Pelias and extend this provider.
19+
You can run your own Pelias instance on your server, see [documentation](https://github.com/pelias/documentation/blob/master/getting_started_install.md) to install it.
20+
21+
For instance:
22+
23+
- Geocode Earth ([Website](https://geocode.earth/) & [Provider](https://github.com/geocoder-php/geocode-earth-provider))
24+
25+
## Install
26+
27+
```bash
28+
composer require geocoder-php/pelias-provider
29+
```
30+
31+
## Contribute
32+
33+
Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or
34+
report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues).
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Tests;
14+
15+
use Geocoder\IntegrationTest\ProviderIntegrationTest;
16+
use Geocoder\Provider\Pelias\Pelias;
17+
use Http\Client\HttpClient;
18+
19+
/**
20+
* @author Tobias Nyholm <[email protected]>
21+
*/
22+
class IntegrationTest extends ProviderIntegrationTest
23+
{
24+
protected $skippedTests = [
25+
'testGeocodeQuery' => 'No Pelias "default" instance.',
26+
'testGeocodeQueryWithNoResults' => 'No Pelias "default" instance.',
27+
'testReverseQuery' => 'No Pelias "default" instance.',
28+
'testReverseQueryWithNoResults' => 'No Pelias "default" instance.',
29+
];
30+
31+
protected $testIpv4 = false;
32+
33+
protected $testIpv6 = false;
34+
35+
protected function createProvider(HttpClient $httpClient)
36+
{
37+
return new Pelias($httpClient, 'http://localhost/');
38+
}
39+
40+
protected function getCacheDir()
41+
{
42+
return __DIR__.'/.cached_responses';
43+
}
44+
45+
protected function getApiKey()
46+
{
47+
return null;
48+
}
49+
}

0 commit comments

Comments
 (0)