2 /***************************************************************************
3 * Copyright (C) 2003-2010 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 // Implementation of a Geocoder using the Google Maps API. Please refer to
23 // the following links for details:
24 // http://code.google.com/apis/maps/documentation/services.html#Geocoding
25 // http://code.google.com/intl/en/apis/maps/documentation/geocoding/
26 // http://code.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
28 // It requires the properties gmaps_key and gmaps_url to be defined in section
29 // Geocoder in plat/al's configuration (platal.ini & platal.conf).
30 class GMapsGeocoder
extends Geocoder
{
32 // Maximum number of Geocoding calls to the Google Maps API.
33 const MAX_GMAPS_RPC_CALLS
= 5;
34 // Maximum levenshtein distance authorized between input and geocoded text in a single line.
35 const MAX_LINE_DISTANCE
= 5;
36 // Maximum levenshtein distance authorized between input and geocoded text in the whole text.
37 const MAX_TOTAL_DISTANCE
= 6;
39 public function getGeocodedAddress(Address
&$address) {
40 $this->prepareAddress($address);
41 $textAddress = $this->getTextToGeocode($address->text
);
43 // Try to geocode the full address.
44 if (($geocodedData = $this->getPlacemarkForAddress($textAddress))) {
45 $this->getUpdatedAddress($address, $geocodedData, null
);
49 // If the full geocoding failed, try to geocode only the final part of the address.
50 // We start by geocoding everything but the first line, and continue until we get
51 // a result. To respect the limit of GMaps calls, we ignore the first few lines
52 // if there are too many address lines.
53 $addressLines = explode("\n", $textAddress);
54 $linesCount = count($addressLines);
55 for ($i = max(1, $linesCount - self
::MAX_GMAPS_RPC_CALLS +
1); $i < $linesCount; ++
$i) {
56 $extraLines = implode("\n", array_slice($addressLines, 0, $i));
57 $toGeocode = implode("\n", array_slice($addressLines, $i));
58 if (($geocodedData = $this->getPlacemarkForAddress($toGeocode))) {
59 $this->getUpdatedAddress($address, $geocodedData, $extraLines);
65 public function stripGeocodingFromAddress(Address
&$address) {
66 $address->geocodedText
= null
;
67 $address->geoloc_choice
= null
;
68 $address->countryId
= null
;
69 $address->country
= null
;
70 $address->administrativeAreaName
= null
;
71 $address->subAdministrativeAreaName
= null
;
72 $address->localityName
= null
;
73 $address->thoroughfareName
= null
;
74 $address->postalCode
= null
;
75 $address->accuracy
= 0;
78 // Updates the address with the geocoded information from Google Maps. Also
79 // cleans up the final informations.
80 private function getUpdatedAddress(Address
&$address, array $geocodedData, $extraLines) {
81 $this->fillAddressWithGeocoding($address, $geocodedData);
82 $this->formatAddress($address, $extraLines);
85 // Retrieves the Placemark object (see #getPlacemarkFromJson()) for the @p
86 // address, by querying the Google Maps API. Returns the array on success,
87 // and null otherwise.
88 private function getPlacemarkForAddress($address) {
89 $url = $this->getGeocodingUrl($address);
90 $geoData = $this->getGeoJsonFromUrl($url);
92 return ($geoData ?
$this->getPlacemarkFromJson($geoData) : null
);
95 // Prepares address to be geocoded
96 private function prepareAddress(Address
&$address) {
97 $address->text
= preg_replace('/\s*\n\s*/m', "\n", trim($address->text
));
100 // Builds the Google Maps geocoder url to fetch information about @p address.
101 // Returns the built url.
102 private function getGeocodingUrl($address) {
106 'key' => $globals->geocoder
->gmaps_key
,
107 'sensor' => 'false', // The queried address wasn't obtained from a GPS sensor.
108 'oe' => 'utf8', // Output encoding.
109 'output' => 'json', // Output format.
110 'gl' => 'fr', // Location preferences (addresses are in France by default).
111 'q' => $address, // The queries address.
114 return $globals->geocoder
->gmaps_url
. '?' . http_build_query($parameters);
117 // Fetches JSON-encoded data from a Google Maps API url, and decode them.
118 // Returns the json array on success, and null otherwise.
119 private function getGeoJsonFromUrl($url) {
122 // Prepare a backtrace object to log errors.
124 if ($globals->debug
& DEBUG_BT
) {
125 if (!isset(PlBacktrace
::$bt['Geoloc'])) {
126 new PlBacktrace('Geoloc');
128 $bt = &PlBacktrace
::$bt['Geoloc'];
132 // Fetch the geocoding data.
133 $rawData = file_get_contents($url);
136 $bt->stop(0, 'Could not retrieve geocoded address from GoogleMaps.');
141 // Decode the JSON-encoded data, and check for their validity.
142 $data = json_decode($rawData, true
);
144 $bt->stop(count($data), null
, $data);
150 // Extracts the most appropriate placemark from the JSON data fetched from
151 // Google Maps. Returns a Placemark array on success, and null otherwise. See
152 // http://code.google.com/apis/maps/documentation/services.html#Geocoding_Structured
153 // for details on the Placemark structure.
154 private function getPlacemarkFromJson(array $data) {
155 // Check for geocoding failures.
156 if (!isset($data['Status']['code']) ||
$data['Status']['code'] != 200) {
157 // TODO: handle non-200 codes in a better way, since the code might
158 // indicate a temporary error on Google's side.
162 // Check that at least one placemark was found.
163 if (count($data['Placemark']) == 0) {
167 // Extract the placemark with the best accuracy. This is not always the
168 // best result (since the same address may yield two different placemarks).
169 $result = $data['Placemark'][0];
170 foreach ($data['Placemark'] as $place) {
171 if ($place['AddressDetails']['Accuracy'] > $result['AddressDetails']['Accuracy']) {
179 // Fills the address with the geocoded data
180 private function fillAddressWithGeocoding(Address
&$address, $geocodedData) {
181 // The geocoded address three is
182 // Country -> AdministrativeArea -> SubAdministrativeArea -> Locality -> Thoroughfare
183 // with all the possible shortcuts
184 // The address is formatted as xAL, or eXtensible Address Language, an international
185 // standard for address formatting.
186 // xAL documentation: http://www.oasis-open.org/committees/ciq/ciq.html#6
187 $address->geocodedText
= str_replace(', ', "\n", $geocodedData['address']);
188 if (isset($geocodedData['AddressDetails']['Accuracy'])) {
189 $address->accuracy
= $geocodedData['AddressDetails']['Accuracy'];
192 $currentPosition = $geocodedData['AddressDetails'];
193 if (isset($currentPosition['Country'])) {
194 $currentPosition = $currentPosition['Country'];
195 $address->countryId
= $currentPosition['CountryNameCode'];
196 $address->country
= $currentPosition['CountryName'];
198 if (isset($currentPosition['AdministrativeArea'])) {
199 $currentPosition = $currentPosition['AdministrativeArea'];
200 $address->administrativeAreaName
= $currentPosition['AdministrativeAreaName'];
202 if (isset($currentPosition['SubAdministrativeArea'])) {
203 $currentPosition = $currentPosition['SubAdministrativeArea'];
204 $address->subAdministrativeAreaName
= $currentPosition['SubAdministrativeAreaName'];
206 if (isset($currentPosition['Locality'])) {
207 $currentPosition = $currentPosition['Locality'];
208 $address->localityName
= $currentPosition['LocalityName'];
210 if (isset($currentPosition['Thoroughfare'])) {
211 $address->thoroughfareName
= $currentPosition['Thoroughfare']['ThoroughfareName'];
213 if (isset($currentPosition['PostalCode'])) {
214 $address->postalCode
= $currentPosition['PostalCode']['PostalCodeNumber'];
218 if (isset($geocodedData['Point']['coordinates'][0])) {
219 $address->latitude
= $geocodedData['Point']['coordinates'][0];
221 if (isset($geocodedData['Point']['coordinates'][1])) {
222 $address->longitude
= $geocodedData['Point']['coordinates'][1];
224 if (isset($geocodedData['ExtendedData']['LatLonBox']['north'])) {
225 $address->north
= $geocodedData['ExtendedData']['LatLonBox']['north'];
227 if (isset($geocodedData['ExtendedData']['LatLonBox']['south'])) {
228 $address->south
= $geocodedData['ExtendedData']['LatLonBox']['south'];
230 if (isset($geocodedData['ExtendedData']['LatLonBox']['east'])) {
231 $address->east
= $geocodedData['ExtendedData']['LatLonBox']['east'];
233 if (isset($geocodedData['ExtendedData']['LatLonBox']['west'])) {
234 $address->west
= $geocodedData['ExtendedData']['LatLonBox']['west'];
238 // Formats the text of the geocoded address using the unused data and
239 // compares it to the given address. If they are too different, the user
240 // will be asked to choose between them.
241 private function formatAddress(Address
&$address, $extraLines) {
244 $address->geocodedText
= $extraLines . "\n" . $address->geocodedText
;
246 $geoloc = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
247 array('', "\n"), $address->geocodedText
));
248 $text = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
249 array('', "\n"), $address->text
));
250 $arrayGeoloc = explode("\n", $geoloc);
251 $arrayText = explode("\n", $text);
252 $countGeoloc = count($arrayGeoloc);
253 $countText = count($arrayText);
256 if (($countText > $countGeoloc) ||
($countText < $countGeoloc - 1)
257 ||
(($countText == $countGeoloc - 1)
258 && ($arrayText[$countText - 1] == strtoupper($address->country
)))) {
261 for ($i = 0; $i < $countGeoloc && $i < $countText; ++
$i) {
262 $lineDistance = levenshtein($arrayText[$i], trim($arrayGeoloc[$i]));
263 $totalDistance +
= $lineDistance;
264 if ($lineDistance > self
::MAX_LINE_DISTANCE ||
$totalDistance > self
::MAX_TOTAL_DISTANCE
) {
272 $address->geocodedText
= null
;
274 $address->geocodedText
= str_replace("\n", "\r\n", $address->geocodedText
);
276 $address->text
= str_replace("\n", "\r\n", $address->text
);
279 // Trims the name of the real country if it contains an ISO 3166-1 non-country
280 // item. For that purpose, we compare the last but one line of the address with
281 // all non-country items of ISO 3166-1.
282 private function getTextToGeocode($text)
284 $res = XDB
::iterator('SELECT country, countryFR
285 FROM geoloc_countries
286 WHERE belongsTo IS NOT NULL');
287 $countries = array();
288 foreach ($res as $item) {
289 $countries[] = $item[0];
290 $countries[] = $item[1];
292 $textLines = explode("\n", $text);
293 $countLines = count($textLines);
294 $needle = strtoupper(trim($textLines[$countLines - 2]));
295 $isPseudoCountry = false
;
296 foreach ($countries as $country) {
297 if (strtoupper($country) == $needle) {
298 $isPseudoCountry = true
;
303 if ($isPseudoCountry) {
304 return implode("\n", array_slice($textLines, 0, -1));
310 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: