Skip to content

Commit cbf4f5e

Browse files
committed
Merge pull request #258 from davedevelopment/abstract-nominatim-provider
Abstract nominatim provider
2 parents 23fa3dd + 41a1fd9 commit cbf4f5e

File tree

4 files changed

+198
-127
lines changed

4 files changed

+198
-127
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ Currently, there are many providers for the following APIs:
3232
* [Google Maps](http://code.google.com/apis/maps/documentation/geocoding/) as Address-Based geocoding and reverse geocoding provider;
3333
* [Google Maps for Business](https://developers.google.com/maps/documentation/business/webservices) as Address-Based geocoding and reverse geocoding provider;
3434
* [Bing Maps](http://msdn.microsoft.com/en-us/library/ff701715.aspx) as Address-Based geocoding and reverse geocoding provider;
35-
* [OpenStreetMaps](http://nominatim.openstreetmap.org/) as Address-Based geocoding and reverse geocoding provider;
35+
* [OpenStreetMaps](http://nominatim.openstreetmap.org/) as Address-Based geocoding and reverse geocoding provider (based on the Nominatim provider);
36+
* [Nominatim](http://wiki.openstreetmap.org/wiki/Nominatim) as Address-Based geocoding and reverse geocoding provider;
3637
* [CloudMade](http://developers.cloudmade.com/projects/show/geocoding-http-api) as Address-Based geocoding and reverse geocoding provider;
3738
* [Geoip](http://php.net/manual/book.geoip.php), the PHP extension, as IP-Based geocoding provider;
3839
* ChainProvider is a special provider that takes a list of providers and iterates
@@ -155,6 +156,11 @@ A valid api key is required.
155156

156157
The `OpenStreetMapsProvider` named `openstreetmaps` is able to geocode and reverse geocode **street addresses**.
157158

159+
### NominatimProvider ###
160+
161+
The `NominatimProvider` named `nominatim` is able to geocode and reverse geocode **street addresses**.
162+
Access to a Nominatim server is required. See the [Nominatim
163+
Wiki Page](http://wiki.openstreetmap.org/wiki/Nominatim) for more information.
158164

159165
### CloudMadeProvider ###
160166

@@ -287,6 +293,9 @@ $geocoder->registerProviders(array(
287293
new \Geocoder\Provider\ArcGISOnlineProvider(
288294
$adapter, $sourceCountry, $useSsl
289295
),
296+
new \Geocoder\Provider\NominatimProvider(
297+
$adapter, 'http://your.nominatim.server', $locale
298+
),
290299
));
291300
```
292301

@@ -298,7 +307,7 @@ Parameters:
298307
* `$service` is available for `MaxMindProvider`.
299308
* `$useSsl` is available for `GoogleMapsProvider`, `GoogleMapsBusinessProvider`, `MaxMindProvider` and `ArcGISOnlineProvider`.
300309
* `$sourceCountry` is available for `ArcGISOnlineProvider`.
301-
310+
* `$rootUrl` is available for `NominatimProvider`.
302311

303312
### Using The ChainProvider ###
304313

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Geocoder package.
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*
8+
* @license MIT License
9+
*/
10+
11+
namespace Geocoder\Provider;
12+
13+
use Geocoder\Exception\NoResultException;
14+
use Geocoder\Exception\UnsupportedException;
15+
use Geocoder\HttpAdapter\HttpAdapterInterface;
16+
17+
/**
18+
* @author Niklas Närhinen <[email protected]>
19+
*/
20+
class NominatimProvider extends AbstractProvider implements LocaleAwareProviderInterface
21+
{
22+
/**
23+
* @param HttpAdapterInterface $adapter An HTTP adapter.
24+
* @param string $rootUrl Root URL of the nominatim server
25+
* @param string $locale A locale (optional).
26+
*/
27+
public function __construct(HttpAdapterInterface $adapter, $rootUrl, $locale = null)
28+
{
29+
parent::__construct($adapter, $locale);
30+
31+
$this->rootUrl = rtrim($rootUrl, '/');
32+
}
33+
34+
/**
35+
* {@inheritDoc}
36+
*/
37+
public function getGeocodedData($address)
38+
{
39+
// This API does not support IPv6
40+
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
41+
throw new UnsupportedException('The NominatimProvider does not support IPv6 addresses.');
42+
}
43+
44+
if ('127.0.0.1' === $address) {
45+
return array($this->getLocalhostDefaults());
46+
}
47+
48+
$query = sprintf($this->getGeocodeEndpointUrl(), urlencode($address), $this->getMaxResults());
49+
$content = $this->executeQuery($query);
50+
51+
if (null === $content) {
52+
throw new NoResultException(sprintf('Could not resolve address "%s"', $address));
53+
}
54+
55+
$doc = new \DOMDocument();
56+
if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
57+
throw new NoResultException(sprintf('Could not execute query %s', $query));
58+
}
59+
60+
$searchResult = $doc->getElementsByTagName('searchresults')->item(0);
61+
$places = $searchResult->getElementsByTagName('place');
62+
63+
if (null === $places || 0 === $places->length) {
64+
throw new NoResultException(sprintf('Could not execute query %s', $query));
65+
}
66+
67+
$results = array();
68+
69+
foreach ($places as $place) {
70+
$boundsAttr = $place->getAttribute('boundingbox');
71+
list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = $boundsAttr
72+
? explode(',', $boundsAttr)
73+
: null;
74+
75+
$results[] = array_merge($this->getDefaults(), array(
76+
'latitude' => $place->getAttribute('lat'),
77+
'longitude' => $place->getAttribute('lon'),
78+
'bounds' => $bounds,
79+
'zipcode' => $this->getNodeValue($place->getElementsByTagName('postcode')),
80+
'county' => $this->getNodeValue($place->getElementsByTagName('county')),
81+
'region' => $this->getNodeValue($place->getElementsByTagName('state')),
82+
'streetNumber' => $this->getNodeValue($place->getElementsByTagName('house_number')),
83+
'streetName' => $this->getNodeValue($place->getElementsByTagName('road')) ?: $this->getNodeValue($place->getElementsByTagName('pedestrian')),
84+
'city' => $this->getNodeValue($place->getElementsByTagName('city')),
85+
'cityDistrict' => $this->getNodeValue($place->getElementsByTagName('suburb')),
86+
'country' => $this->getNodeValue($place->getElementsByTagName('country')),
87+
'countryCode' => strtoupper($this->getNodeValue($place->getElementsByTagName('country_code'))),
88+
));
89+
}
90+
91+
return $results;
92+
}
93+
94+
/**
95+
* {@inheritDoc}
96+
*/
97+
public function getReversedData(array $coordinates)
98+
{
99+
$query = sprintf($this->getReverseEndpointUrl(), $coordinates[0], $coordinates[1]);
100+
$content = $this->executeQuery($query);
101+
102+
if (null === $content) {
103+
throw new NoResultException(sprintf('Unable to resolve the coordinates %s', implode(', ', $coordinates)));
104+
}
105+
106+
$doc = new \DOMDocument();
107+
if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
108+
throw new NoResultException(sprintf('Could not resolve coordinates %s', implode(', ', $coordinates)));
109+
}
110+
111+
$searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
112+
$addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
113+
$result = $searchResult->getElementsByTagName('result')->item(0);
114+
115+
return array(array_merge($this->getDefaults(), array(
116+
'latitude' => $result->getAttribute('lat'),
117+
'longitude' => $result->getAttribute('lon'),
118+
'zipcode' => $this->getNodeValue($addressParts->getElementsByTagName('postcode')),
119+
'county' => $this->getNodeValue($addressParts->getElementsByTagName('county')),
120+
'region' => $this->getNodeValue($addressParts->getElementsByTagName('state')),
121+
'streetNumber' => $this->getNodeValue($addressParts->getElementsByTagName('house_number')),
122+
'streetName' => $this->getNodeValue($addressParts->getElementsByTagName('road')) ?: $this->getNodeValue($addressParts->getElementsByTagName('pedestrian')),
123+
'city' => $this->getNodeValue($addressParts->getElementsByTagName('city')),
124+
'cityDistrict' => $this->getNodeValue($addressParts->getElementsByTagName('suburb')),
125+
'country' => $this->getNodeValue($addressParts->getElementsByTagName('country')),
126+
'countryCode' => strtoupper($this->getNodeValue($addressParts->getElementsByTagName('country_code'))),
127+
)));
128+
}
129+
130+
/**
131+
* {@inheritDoc}
132+
*/
133+
public function getName()
134+
{
135+
return 'openstreetmaps';
136+
}
137+
138+
/**
139+
* @param string $query
140+
*
141+
* @return string
142+
*/
143+
protected function executeQuery($query)
144+
{
145+
if (null !== $this->getLocale()) {
146+
$query = sprintf('%s&accept-language=%s', $query, $this->getLocale());
147+
}
148+
149+
return $this->getAdapter()->getContent($query);
150+
}
151+
152+
/**
153+
* @return string
154+
*/
155+
protected function getGeocodeEndpointUrl()
156+
{
157+
return $this->rootUrl.'/search?q=%s&format=xml&addressdetails=1&limit=%d';
158+
}
159+
160+
/**
161+
* @return string
162+
*/
163+
protected function getReverseEndpointUrl()
164+
{
165+
return $this->rootUrl.'/reverse?format=xml&lat=%F&lon=%F&addressdetails=1&zoom=18';
166+
}
167+
168+
/**
169+
* @param \DOMNodeList
170+
*
171+
* @return string
172+
*/
173+
private function getNodeValue(\DOMNodeList $element)
174+
{
175+
return $element->length ? $element->item(0)->nodeValue : null;
176+
}
177+
}

src/Geocoder/Provider/OpenStreetMapsProvider.php

Lines changed: 8 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010

1111
namespace Geocoder\Provider;
1212

13-
use Geocoder\Exception\NoResultException;
14-
use Geocoder\Exception\UnsupportedException;
13+
use Geocoder\HttpAdapter\HttpAdapterInterface;
1514

1615
/**
1716
* @author Niklas Närhinen <[email protected]>
1817
*/
19-
class OpenStreetMapsProvider extends AbstractProvider implements LocaleAwareProviderInterface
18+
class OpenStreetMapsProvider extends NominatimProvider
2019
{
2120
/**
2221
* @var string
@@ -29,130 +28,16 @@ class OpenStreetMapsProvider extends AbstractProvider implements LocaleAwareProv
2928
const REVERSE_ENDPOINT_URL = 'http://nominatim.openstreetmap.org/reverse?format=xml&lat=%F&lon=%F&addressdetails=1&zoom=18';
3029

3130
/**
32-
* {@inheritDoc}
33-
*/
34-
public function getGeocodedData($address)
35-
{
36-
// This API does not support IPv6
37-
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
38-
throw new UnsupportedException('The OpenStreetMapsProvider does not support IPv6 addresses.');
39-
}
40-
41-
if ('127.0.0.1' === $address) {
42-
return array($this->getLocalhostDefaults());
43-
}
44-
45-
$query = sprintf(self::GEOCODE_ENDPOINT_URL, urlencode($address), $this->getMaxResults());
46-
$content = $this->executeQuery($query);
47-
48-
if (null === $content) {
49-
throw new NoResultException(sprintf('Could not resolve address "%s"', $address));
50-
}
51-
52-
$doc = new \DOMDocument();
53-
if (!@$doc->loadXML($content) || null === $doc->getElementsByTagName('searchresults')->item(0)) {
54-
throw new NoResultException(sprintf('Could not execute query %s', $query));
55-
}
56-
57-
$searchResult = $doc->getElementsByTagName('searchresults')->item(0);
58-
$places = $searchResult->getElementsByTagName('place');
59-
60-
if (null === $places || 0 === $places->length) {
61-
throw new NoResultException(sprintf('Could not execute query %s', $query));
62-
}
63-
64-
$results = array();
65-
66-
foreach ($places as $place) {
67-
$boundsAttr = $place->getAttribute('boundingbox');
68-
list($bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']) = $boundsAttr
69-
? explode(',', $boundsAttr)
70-
: null;
71-
72-
$results[] = array_merge($this->getDefaults(), array(
73-
'latitude' => $place->getAttribute('lat'),
74-
'longitude' => $place->getAttribute('lon'),
75-
'bounds' => $bounds,
76-
'zipcode' => $this->getNodeValue($place->getElementsByTagName('postcode')),
77-
'county' => $this->getNodeValue($place->getElementsByTagName('county')),
78-
'region' => $this->getNodeValue($place->getElementsByTagName('state')),
79-
'streetNumber' => $this->getNodeValue($place->getElementsByTagName('house_number')),
80-
'streetName' => $this->getNodeValue($place->getElementsByTagName('road')) ?: $this->getNodeValue($place->getElementsByTagName('pedestrian')),
81-
'city' => $this->getNodeValue($place->getElementsByTagName('city')),
82-
'cityDistrict' => $this->getNodeValue($place->getElementsByTagName('suburb')),
83-
'country' => $this->getNodeValue($place->getElementsByTagName('country')),
84-
'countryCode' => strtoupper($this->getNodeValue($place->getElementsByTagName('country_code'))),
85-
));
86-
}
87-
88-
return $results;
89-
}
90-
91-
/**
92-
* {@inheritDoc}
93-
*/
94-
public function getReversedData(array $coordinates)
95-
{
96-
$query = sprintf(self::REVERSE_ENDPOINT_URL, $coordinates[0], $coordinates[1]);
97-
$content = $this->executeQuery($query);
98-
99-
if (null === $content) {
100-
throw new NoResultException(sprintf('Unable to resolve the coordinates %s', implode(', ', $coordinates)));
101-
}
102-
103-
$doc = new \DOMDocument();
104-
if (!@$doc->loadXML($content) || $doc->getElementsByTagName('error')->length > 0) {
105-
throw new NoResultException(sprintf('Could not resolve coordinates %s', implode(', ', $coordinates)));
106-
}
107-
108-
$searchResult = $doc->getElementsByTagName('reversegeocode')->item(0);
109-
$addressParts = $searchResult->getElementsByTagName('addressparts')->item(0);
110-
$result = $searchResult->getElementsByTagName('result')->item(0);
111-
112-
return array(array_merge($this->getDefaults(), array(
113-
'latitude' => $result->getAttribute('lat'),
114-
'longitude' => $result->getAttribute('lon'),
115-
'zipcode' => $this->getNodeValue($addressParts->getElementsByTagName('postcode')),
116-
'county' => $this->getNodeValue($addressParts->getElementsByTagName('county')),
117-
'region' => $this->getNodeValue($addressParts->getElementsByTagName('state')),
118-
'streetNumber' => $this->getNodeValue($addressParts->getElementsByTagName('house_number')),
119-
'streetName' => $this->getNodeValue($addressParts->getElementsByTagName('road')) ?: $this->getNodeValue($addressParts->getElementsByTagName('pedestrian')),
120-
'city' => $this->getNodeValue($addressParts->getElementsByTagName('city')),
121-
'cityDistrict' => $this->getNodeValue($addressParts->getElementsByTagName('suburb')),
122-
'country' => $this->getNodeValue($addressParts->getElementsByTagName('country')),
123-
'countryCode' => strtoupper($this->getNodeValue($addressParts->getElementsByTagName('country_code'))),
124-
)));
125-
}
126-
127-
/**
128-
* {@inheritDoc}
129-
*/
130-
public function getName()
131-
{
132-
return 'openstreetmaps';
133-
}
134-
135-
/**
136-
* @param string $query
137-
*
138-
* @return string
31+
* @var string
13932
*/
140-
protected function executeQuery($query)
141-
{
142-
if (null !== $this->getLocale()) {
143-
$query = sprintf('%s&accept-language=%s', $query, $this->getLocale());
144-
}
145-
146-
return $this->getAdapter()->getContent($query);
147-
}
33+
const ROOT_URL = 'http://nominatim.openstreetmap.org';
14834

14935
/**
150-
* @param \DOMNodeList
151-
*
152-
* @return string
36+
* @param HttpAdapterInterface $adapter An HTTP adapter.
37+
* @param string $locale A locale (optional).
15338
*/
154-
private function getNodeValue(\DOMNodeList $element)
39+
public function __construct(HttpAdapterInterface $adapter, $locale = null)
15540
{
156-
return $element->length ? $element->item(0)->nodeValue : null;
41+
parent::__construct($adapter, static::ROOT_URL, $locale);
15742
}
15843
}

tests/Geocoder/Tests/Provider/OpenStreetMapsProviderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public function testGetGeocodedDataWithLocalhostIPv4()
338338

339339
/**
340340
* @expectedException \Geocoder\Exception\UnsupportedException
341-
* @expectedExceptionMessage The OpenStreetMapsProvider does not support IPv6 addresses.
341+
* @expectedExceptionMessage The NominatimProvider does not support IPv6 addresses.
342342
*/
343343
public function testGetGeocodedDataWithLocalhostIPv6()
344344
{
@@ -420,7 +420,7 @@ public function testGetGeocodedDataWithRealIPv4WithLocale()
420420

421421
/**
422422
* @expectedException \Geocoder\Exception\UnsupportedException
423-
* @expectedExceptionMessage The OpenStreetMapsProvider does not support IPv6 addresses.
423+
* @expectedExceptionMessage The NominatimProvider does not support IPv6 addresses.
424424
*/
425425
public function testGetGeocodedDataWithRealIPv6()
426426
{

0 commit comments

Comments
 (0)