2 /***************************************************************************
3 * Copyright (C) 2003-2009 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
22 // Interface for an address geocoder. It provides support for transforming a free
23 // form address into a fully structured one.
24 // TODO: define and use an Address object instead of a key-value map.
25 abstract class Geocoder
{
26 // Geocodes @p the address, and returns the corresponding updated address.
27 // Unknown key-value pairs available in the input map are retained as-is.
28 abstract public function getGeocodedAddress(array $address);
30 // Updates geoloc_administrativeareas, geoloc_subadministrativeareas and
31 // geoloc_localities databases with new geocoded data and returns the
33 static public function getAreaId(array &$address, $area)
35 static $databases = array(
36 'administrativeArea' => 'geoloc_administrativeareas',
37 'subAdministrativeArea' => 'geoloc_subadministrativeareas',
38 'locality' => 'geoloc_localities',
41 if (isset($address[$area . 'Name']) && isset($databases[$area])) {
42 $res = XDB
::query("SELECT id
43 FROM " . $databases[$area] . "
45 $address[$area . 'Name']);
46 if ($res->numRows() == 0) {
47 $address[$area . 'Id'] = XDB
::execute("INSERT INTO " . $databases[$area] . " (name, country)
49 $address[$area . 'Name'], $address['countryId']);
51 $address[$area . 'Id'] = $res->fetchOneCell();
57 // Implementation of a Geocoder using the Google Maps API. Please refer to
58 // the following links for details:
59 // http://code.google.com/apis/maps/documentation/services.html#Geocoding
60 // http://code.google.com/intl/en/apis/maps/documentation/geocoding/
61 // http://code.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
63 // It requires the properties gmaps_key and gmaps_url to be defined in section
64 // Geocoder in plat/al's configuration (platal.ini & platal.conf).
65 class GMapsGeocoder
extends Geocoder
{
67 // Maximum number of Geocoding calls to the Google Maps API.
68 const MAX_GMAPS_RPC_CALLS
= 5;
70 public function getGeocodedAddress(array $address) {
71 $address = $this->prepareAddress($address);
72 $textAddress = $address['text'];
74 // Try to geocode the full address.
75 if (($geocodedData = $this->getPlacemarkForAddress($textAddress))) {
76 return $this->getUpdatedAddress($address, $geocodedData, null
);
79 // If the full geocoding failed, try to geocode only the final part of the address.
80 // We start by geocoding everything but the first line, and continue until we get
81 // a result. To respect the limit of GMaps calls, we ignore the first few lines
82 // if there are too many address lines.
83 $addressLines = explode("\n", $textAddress);
84 $linesCount = count($addressLines);
85 for ($i = max(1, $linesCount - self
::MAX_GMAPS_RPC_CALLS +
1); $i < $linesCount; ++
$i) {
86 $extraLines = implode("\n", array_slice($addressLines, 0, $i));
87 $toGeocode = implode("\n", array_slice($addressLines, $i));
88 if (($geocodedData = $this->getPlacemarkForAddress($toGeocode))) {
89 return $this->getUpdatedAddress($address, $geocodedData, $extraLines);
93 // No geocoding could be done, the initial address is returned as-is.
97 // Updates the address with the geocoded information from Google Maps. Also
98 // cleans up the final informations.
99 private function getUpdatedAddress(array $address, array $geocodedData, $extraLines) {
100 $this->fillAddressWithGeocoding(&$address, $geocodedData);
102 // If the accuracy is 6, it means only the street has been gecoded
103 // but not the number, thus we need to fix it.
104 if ($address['accuracy'] == 6) {
105 $this->fixStreetNumber($address);
108 // We can now format the address.
109 $this->formatAddress($address, $extraLines);
111 // Some entities in ISO 3166 are not countries, thus they have to be replaced
112 // by the country they belong to.
113 // TODO: fixCountry($address);
118 // Retrieves the Placemark object (see #getPlacemarkFromJson()) for the @p
119 // address, by querying the Google Maps API. Returns the array on success,
120 // and null otherwise.
121 private function getPlacemarkForAddress($address) {
122 $url = $this->getGeocodingUrl($address);
123 $geoData = $this->getGeoJsonFromUrl($url);
125 return ($geoData ?
$this->getPlacemarkFromJson($geoData) : null
);
128 // Prepares address to be geocoded
129 private function prepareAddress($address) {
130 $address['text'] = preg_replace('/\s*\n\s*/m', "\n", trim($address['text']));
131 // TODO: $address['postalAddress'] = getPostalAddress($address['text']);
132 $address['updateTime'] = time();
133 unset($address['changed']);
137 // Builds the Google Maps geocoder url to fetch information about @p address.
138 // Returns the built url.
139 private function getGeocodingUrl($address) {
143 'key' => $globals->geocoder
->gmaps_key
,
144 'sensor' => 'false', // The queried address wasn't obtained from a GPS sensor.
145 'hl' => 'fr', // Output langage.
146 'oe' => 'utf8', // Output encoding.
147 'output' => 'json', // Output format.
148 'gl' => 'fr', // Location preferences (addresses are in France by default).
149 'q' => $address, // The queries address.
152 return $globals->geocoder
->gmaps_url
. '?' . http_build_query($parameters);
155 // Fetches JSON-encoded data from a Google Maps API url, and decode them.
156 // Returns the json array on success, and null otherwise.
157 private function getGeoJsonFromUrl($url) {
160 // Prepare a backtrace object to log errors.
162 if ($globals->debug
& DEBUG_BT
) {
163 if (!isset(PlBacktrace
::$bt['Geoloc'])) {
164 new PlBacktrace('Geoloc');
166 $bt = &PlBacktrace
::$bt['Geoloc'];
170 // Fetch the geocoding data.
171 $rawData = file_get_contents($url);
174 $bt->stop(0, "Could not retrieve geocoded address from GoogleMaps.");
179 // Decode the JSON-encoded data, and check for their validity.
180 $data = json_decode($rawData, true
);
182 $bt->stop(count($data), null
, $data);
188 // Extracts the most appropriate placemark from the JSON data fetched from
189 // Google Maps. Returns a Placemark array on success, and null otherwise. See
190 // http://code.google.com/apis/maps/documentation/services.html#Geocoding_Structured
191 // for details on the Placemark structure.
192 private function getPlacemarkFromJson(array $data) {
193 // Check for geocoding failures.
194 if (!isset($data['Status']['code']) ||
$data['Status']['code'] != 200) {
195 // TODO: handle non-200 codes in a better way, since the code might
196 // indicate a temporary error on Google's side.
200 // Check that at least one placemark was found.
201 if (count($data['Placemark']) == 0) {
205 // Extract the placemark with the best accuracy. This is not always the
206 // best result (since the same address may yield two different placemarks).
207 $result = $data['Placemark'][0];
208 foreach ($data['Placemark'] as $place) {
209 if ($place['AddressDetails']['Accuracy'] > $result['AddressDetails']['Accuracy']) {
217 // Fills the address with the geocoded data
218 private function fillAddressWithGeocoding(&$address, $geocodedData) {
219 // The geocoded address three is
220 // Country -> AdministrativeArea -> SubAdministrativeArea -> Locality -> Thoroughfare
221 // with all the possible shortcuts
222 // The address is formatted as xAL, or eXtensible Address Language, an international
223 // standard for address formatting.
224 // xAL documentation: http://www.oasis-open.org/committees/ciq/ciq.html#6
225 $address['geoloc'] = str_replace(", ", "\n", $geocodedData['address']);
226 if (isset($geocodedData['AddressDetails']['Accuracy'])) {
227 $address['accuracy'] = $geocodedData['AddressDetails']['Accuracy'];
230 $currentPosition = $geocodedData['AddressDetails'];
231 if (isset($currentPosition['Country'])) {
232 $currentPosition = $currentPosition['Country'];
233 $address['countryId'] = $currentPosition['CountryNameCode'];
234 $address['country'] = $currentPosition['CountryName'];
236 if (isset($currentPosition['AdministrativeArea'])) {
237 $currentPosition = $currentPosition['AdministrativeArea'];
238 $address['administrativeAreaName'] = $currentPosition['AdministrativeAreaName'];
240 if (isset($currentPosition['SubAdministrativeArea'])) {
241 $currentPosition = $currentPosition['SubAdministrativeArea'];
242 $address['subAdministrativeAreaName'] = $currentPosition['SubAdministrativeAreaName'];
244 if (isset($currentPosition['Locality'])) {
245 $currentPosition = $currentPosition['Locality'];
246 $address['localityName'] = $currentPosition['LocalityName'];
248 if (isset($currentPosition['Thoroughfare'])) {
249 $address['thoroughfareName'] = $currentPosition['Thoroughfare']['ThoroughfareName'];
251 if (isset($currentPosition['PostalCode'])) {
252 $address['postalCode'] = $currentPosition['PostalCode']['PostalCodeNumber'];
256 if (isset($geocodedData['Point']['coordinates'][0])) {
257 $address['latitude'] = $geocodedData['Point']['coordinates'][0];
259 if (isset($geocodedData['Point']['coordinates'][1])) {
260 $address['longitude'] = $geocodedData['Point']['coordinates'][1];
262 if (isset($geocodedData['ExtendedData']['LatLonBox']['north'])) {
263 $address['north'] = $geocodedData['ExtendedData']['LatLonBox']['north'];
265 if (isset($geocodedData['ExtendedData']['LatLonBox']['south'])) {
266 $address['south'] = $geocodedData['ExtendedData']['LatLonBox']['south'];
268 if (isset($geocodedData['ExtendedData']['LatLonBox']['east'])) {
269 $address['east'] = $geocodedData['ExtendedData']['LatLonBox']['east'];
271 if (isset($geocodedData['ExtendedData']['LatLonBox']['west'])) {
272 $address['west'] = $geocodedData['ExtendedData']['LatLonBox']['west'];
276 // Formats the text of the geocoded address using the unused data and
277 // compares it to the given address. If they are too different, the user
278 // will be asked to choose between them.
279 private function formatAddress(&$address, $extraLines) {
282 $address['geoloc'] = $extraLines . "\n" . $address['geoloc'];
284 $geoloc = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
285 array("", "\n"), $address['geoloc']));
286 $text = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
287 array("", "\n"), $address['text']));
288 $arrayGeoloc = explode("\n", $geoloc);
289 $arrayText = explode("\n", $text);
290 $countGeoloc = count($arrayGeoloc);
291 $countText = count($arrayText);
293 if (($countText > $countGeoloc) ||
($countText < $countGeoloc - 1)
294 ||
(($countText == $countGeoloc - 1)
295 && ($arrayText[$countText - 1] == strtoupper($address['country'])))) {
298 for ($i = 0; $i < $countGeoloc && $i < $countText; ++
$i) {
299 if (levenshtein($arrayText[$i], trim($arrayGeoloc[$i])) > 3) {
305 $address['text'] = $address['geoloc'];
306 unset($address['geoloc']);
310 // Search for the lign from the given address that is the closest to the geocoded thoroughfareName
311 // and replaces the corresponding lign in the geocoded text by it.
312 static protected function fixStreetNumber(&$address)
314 if (isset($address['thoroughfareName'])) {
315 $thoroughfareName = $address['thoroughfareName'];
316 $thoroughfareToken = strtoupper(trim(preg_replace(array("/[,\"'#~:;_\-]/", "/\r\n/"),
317 array("", "\n"), $thoroughfareName)));
318 $geolocLines = explode("\n", $address['geoloc']);
319 $textLines = explode("\n", $address['text']);
320 $mindist = strlen($thoroughfareToken);
323 foreach ($textLines as $i => $token) {
324 if (($l = levenshtein(strtoupper(trim(preg_replace(array("/[,\"'#~:;_\-]/", "/\r\n/"),
325 array("", "\n"), $token))),
326 $thoroughfareToken)) < $mindist) {
331 foreach ($geolocLines as $i => $line) {
332 if (strtoupper(trim($thoroughfareName)) == strtoupper(trim($line))) {
337 $geolocLines[$pos] = $textLines[$minpos];
338 $address['geoloc'] = implode("\n", $geolocLines);
343 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: