2 /***************************************************************************
3 * Copyright (C) 2003-2011 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 v3. Please refer
23 // to the following link for details:
24 // http://code.google.com/apis/maps/documentation/geocoding/
26 // It requires the properties gmaps_url to be defined in section Geocoder
27 // in plat/al's configuration (platal.ini & platal.conf).
28 class GMapsGeocoder
extends Geocoder
{
30 // Maximum number of Geocoding calls to the Google Maps API.
31 const MAX_GMAPS_RPC_CALLS
= 5;
33 public function getGeocodedAddress(Address
$address, $defaultLanguage = null
, $forceLanguage = false
) {
34 $this->prepareAddress($address);
35 $textAddress = $this->getTextToGeocode($address->text
);
36 if (is_null($defaultLanguage)) {
37 $defaultLanguage = Platal
::globals()->geocoder
->gmaps_language
;
40 // Try to geocode the full address.
41 $address->geocoding_calls
= 1;
42 if (($geocodedData = $this->getPlacemarkForAddress($textAddress, $defaultLanguage))) {
43 $this->getUpdatedAddress($address, $geocodedData, null
, $forceLanguage);
47 // If the full geocoding failed, try to geocode only the final part of the address.
48 // We start by geocoding everything but the first line, and continue until we get
49 // a result. To respect the limit of GMaps calls, we ignore the first few lines
50 // if there are too many address lines.
51 $addressLines = explode("\n", $textAddress);
52 $linesCount = count($addressLines);
53 for ($i = max(1, $linesCount - self
::MAX_GMAPS_RPC_CALLS +
1); $i < $linesCount; ++
$i) {
54 $extraLines = implode("\n", array_slice($addressLines, 0, $i));
55 $toGeocode = implode("\n", array_slice($addressLines, $i));
56 ++
$address->geocoding_calls
;
57 if (($geocodedData = $this->getPlacemarkForAddress($toGeocode, $defaultLanguage))) {
58 $this->getUpdatedAddress($address, $geocodedData, $extraLines, $forceLanguage);
64 public function stripGeocodingFromAddress(Address
$address) {
65 $address->formatted_address
= '';
67 $address->latitude
= null
;
68 $address->longitude
= null
;
69 $address->southwest_latitude
= null
;
70 $address->southwest_longitude
= null
;
71 $address->northeast_latitude
= null
;
72 $address->northeast_longitude
= null
;
73 $address->location_type
= null
;
74 $address->partial_match
= false
;
77 // Updates the address with the geocoded information from Google Maps. Also
78 // cleans up the final informations.
79 private function getUpdatedAddress(Address
$address, array $geocodedData, $extraLines, $forceLanguage) {
80 $this->fillAddressWithGeocoding($address, $geocodedData, false
);
81 $this->formatAddress($address, $extraLines, $forceLanguage);
84 // Retrieves the Placemark object (see #getPlacemarkFromJson()) for the @p
85 // address, by querying the Google Maps API. Returns the array on success,
86 // and null otherwise.
87 private function getPlacemarkForAddress($address, $defaultLanguage) {
88 $url = $this->getGeocodingUrl($address, $defaultLanguage);
89 $geoData = $this->getGeoJsonFromUrl($url);
91 return ($geoData ?
$this->getPlacemarkFromJson($geoData, $url) : null
);
94 // Prepares address to be geocoded
95 private function prepareAddress(Address
$address) {
96 $address->text
= preg_replace('/\s*\n\s*/m', "\n", trim($address->text
));
99 // Builds the Google Maps geocoder url to fetch information about @p address.
100 // Returns the built url.
101 private function getGeocodingUrl($address, $defaultLanguage) {
105 'language' => $defaultLanguage,
106 'region' => $globals->geocoder
->gmaps_region
,
107 'sensor' => 'false', // The queried address wasn't obtained from a GPS sensor.
108 'address' => $address, // The queries address.
111 return $globals->geocoder
->gmaps_url
. 'json?' . http_build_query($parameters);
114 // Fetches JSON-encoded data from a Google Maps API url, and decode them.
115 // Returns the json array on success, and null otherwise.
116 private function getGeoJsonFromUrl($url) {
119 // Prepare a backtrace object to log errors.
121 if ($globals->debug
& DEBUG_BT
) {
122 if (!isset(PlBacktrace
::$bt['Geoloc'])) {
123 new PlBacktrace('Geoloc');
125 $bt = &PlBacktrace
::$bt['Geoloc'];
129 // Fetch the geocoding data.
130 $rawData = file_get_contents($url);
133 $bt->stop(0, 'Could not retrieve geocoded address from GoogleMaps.');
138 // Decode the JSON-encoded data, and check for their validity.
139 $data = json_decode($rawData, true
);
141 $bt->stop(count($data), null
, $data);
147 // Extracts the most appropriate placemark from the JSON data fetched from
148 // Google Maps. Returns a Placemark array on success, and null otherwise.
149 // http://code.google.com/apis/maps/documentation/geocoding/#StatusCodes
150 private function getPlacemarkFromJson(array $data, $url) {
151 // Check for geocoding status.
152 $status = $data['status'];
154 // If no result, return null.
155 if ($status == 'ZERO_RESULTS') {
159 // If there are results return the first one.
160 if ($status == 'OK') {
161 return $data['results'][0];
165 $mailer = new PlMailer('profile/geocoding.mail.tpl');
166 $mailer->assign('status', $status);
167 $mailer->assign('url', $url);
172 // Fills the address with the geocoded data
173 private function fillAddressWithGeocoding(Address
$address, $geocodedData, $isLocal) {
174 $address->types
= implode(',', $geocodedData['types']);
175 $address->formatted_address
= $geocodedData['formatted_address'];
176 $address->components
= $geocodedData['address_components'];
177 $address->latitude
= $geocodedData['geometry']['location']['lat'];
178 $address->longitude
= $geocodedData['geometry']['location']['lng'];
179 $address->southwest_latitude
= $geocodedData['geometry']['viewport']['southwest']['lat'];
180 $address->southwest_longitude
= $geocodedData['geometry']['viewport']['southwest']['lng'];
181 $address->northeast_latitude
= $geocodedData['geometry']['viewport']['northeast']['lat'];
182 $address->northeast_longitude
= $geocodedData['geometry']['viewport']['northeast']['lng'];
183 $address->location_type
= $geocodedData['geometry']['location_type'];
184 $address->partial_match
= isset($geocodedData['partial_match']) ? true
: false
;
187 // Formats the text of the geocoded address using the unused data and
188 // compares it to the given address. If they are too different, the user
189 // will be asked to choose between them.
190 private function formatAddress(Address
$address, $extraLines, $forceLanguage)
192 /* XXX: Check how to integrate this in the new geocoding system.
193 if (!$forceLanguage) {
194 $languages = XDB::fetchOneCell('SELECT IF(ISNULL(gc1.belongsTo), gl1.language, gl2.language)
195 FROM geoloc_countries AS gc1
196 INNER JOIN geoloc_languages AS gl1 ON (gc1.iso_3166_1_a2 = gl1.iso_3166_1_a2)
197 LEFT JOIN geoloc_countries AS gc2 ON (gc1.belongsTo = gc2.iso_3166_1_a2)
198 LEFT JOIN geoloc_languages AS gl2 ON (gc2.iso_3166_1_a2 = gl2.iso_3166_1_a2)
199 WHERE gc1.iso_3166_1_a2 = {?}',
200 $address->countryId);
201 $toGeocode = substr($address->text, strlen($extraLines));
202 foreach (explode(',', $languages) as $language) {
203 if ($language != Platal::globals()->geocoder->gmaps_language) {
204 $geocodedData = $this->getPlacemarkForAddress($toGeocode, $language);
205 $this->fillAddressWithGeocoding($address, $geocodedData, true);
210 $address->text
= str_replace("\n", "\r\n", $address->text
);
213 // Trims the name of the real country if it contains an ISO 3166-1 non-country
214 // item. For that purpose, we compare the last but one line of the address with
215 // all non-country items of ISO 3166-1.
216 private function getTextToGeocode($text)
218 $res = XDB
::iterator('SELECT countryEn, country
219 FROM geoloc_countries
220 WHERE belongsTo IS NOT NULL');
221 $countries = array();
222 foreach ($res as $item) {
223 $countries[] = $item[0];
224 $countries[] = $item[1];
226 $textLines = explode("\n", $text);
227 $countLines = count($textLines);
228 $needle = strtoupper(trim($textLines[$countLines - 2]));
229 $isPseudoCountry = false
;
231 foreach ($countries as $country) {
232 if (strtoupper($country) === $needle) {
233 $isPseudoCountry = true
;
239 if ($isPseudoCountry) {
240 return implode("\n", array_slice($textLines, 0, -1));
246 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: