Fix possible out-of-bound array access in GMapsGeocoder::getTextToGeocode
[platal.git] / classes / gmapsgeocoder.php
index 38eff81..e009080 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /***************************************************************************
- *  Copyright (C) 2003-2010 Polytechnique.org                              *
+ *  Copyright (C) 2003-2014 Polytechnique.org                              *
  *  http://opensource.polytechnique.org/                                   *
  *                                                                         *
  *  This program is free software; you can redistribute it and/or modify   *
  *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
  ***************************************************************************/
 
-// Implementation of a Geocoder using the Google Maps API. Please refer to
-// the following links for details:
-// http://code.google.com/apis/maps/documentation/services.html#Geocoding
-// http://code.google.com/intl/en/apis/maps/documentation/geocoding/
-// http://code.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
+// Implementation of a Geocoder using the Google Maps API v3. Please refer
+// to the following link for details:
+// http://code.google.com/apis/maps/documentation/geocoding/
 //
-// It requires the properties gmaps_key and gmaps_url to be defined in section
-// Geocoder in plat/al's configuration (platal.ini & platal.conf).
+// It requires the properties gmaps_url to be defined in section Geocoder
+// in plat/al's configuration (platal.ini & platal.conf).
 class GMapsGeocoder extends Geocoder {
 
     // Maximum number of Geocoding calls to the Google Maps API.
     const MAX_GMAPS_RPC_CALLS = 5;
-    // Maximum levenshtein distance authorized between input and geocoded text in a single line.
-    const MAX_LINE_DISTANCE = 5;
-    // Maximum levenshtein distance authorized between input and geocoded text in the whole text.
-    const MAX_TOTAL_DISTANCE = 6;
 
-    public function getGeocodedAddress(Address &$address) {
+    static public function buildStaticMapURL($latitude, $longitude, $color, $separator = '&')
+    {
+        if (!$latitude || !$longitude) {
+            return null;
+        }
+
+        $parameters = array(
+            'size'    => '300x100',
+            'markers' => 'color:' . $color . '|' . $latitude . ',' . $longitude,
+            'zoom'    => '12',
+            'sensor'  => 'false'
+        );
+
+        return Platal::globals()->maps->static_map . '?' . http_build_query($parameters, '', $separator);
+    }
+
+    public function getGeocodedAddress(Address $address, $defaultLanguage = null, $forceLanguage = false) {
         $this->prepareAddress($address);
         $textAddress = $this->getTextToGeocode($address->text);
+        if (is_null($defaultLanguage)) {
+            $defaultLanguage = Platal::globals()->geocoder->gmaps_language;
+        }
 
         // Try to geocode the full address.
-        if (($geocodedData = $this->getPlacemarkForAddress($textAddress))) {
-            $this->getUpdatedAddress($address, $geocodedData, null);
+        $address->geocoding_calls = 1;
+        if (($geocodedData = $this->getPlacemarkForAddress($textAddress, $defaultLanguage))) {
+            $this->getUpdatedAddress($address, $geocodedData, null, $forceLanguage);
             return;
         }
 
@@ -55,64 +69,62 @@ class GMapsGeocoder extends Geocoder {
         for ($i = max(1, $linesCount - self::MAX_GMAPS_RPC_CALLS + 1); $i < $linesCount; ++$i) {
             $extraLines = implode("\n", array_slice($addressLines, 0, $i));
             $toGeocode  = implode("\n", array_slice($addressLines, $i));
-            if (($geocodedData = $this->getPlacemarkForAddress($toGeocode))) {
-                $this->getUpdatedAddress($address, $geocodedData, $extraLines);
+            ++$address->geocoding_calls;
+            if (($geocodedData = $this->getPlacemarkForAddress($toGeocode, $defaultLanguage))) {
+                $this->getUpdatedAddress($address, $geocodedData, $extraLines, $forceLanguage);
                 return;
             }
         }
     }
 
-    public function stripGeocodingFromAddress(Address &$address) {
-        $address->geocodedText = null;
-        $address->geoloc_choice = null;
-        $address->countryId = null;
-        $address->country = null;
-        $address->administrativeAreaName = null;
-        $address->subAdministrativeAreaName = null;
-        $address->localityName = null;
-        $address->thoroughfareName = null;
-        $address->postalCode = null;
-        $address->accuracy = 0;
+    public function stripGeocodingFromAddress(Address $address) {
+        $address->formatted_address = '';
+        $address->types = '';
+        $address->latitude = null;
+        $address->longitude = null;
+        $address->southwest_latitude = null;
+        $address->southwest_longitude = null;
+        $address->northeast_latitude = null;
+        $address->northeast_longitude = null;
+        $address->location_type = null;
+        $address->partial_match = false;
     }
 
     // Updates the address with the geocoded information from Google Maps. Also
     // cleans up the final informations.
-    private function getUpdatedAddress(Address &$address, array $geocodedData, $extraLines) {
-        $this->fillAddressWithGeocoding($address, $geocodedData);
-        $this->formatAddress($address, $extraLines);
+    private function getUpdatedAddress(Address $address, array $geocodedData, $extraLines, $forceLanguage) {
+        $this->fillAddressWithGeocoding($address, $geocodedData, false);
+        $this->formatAddress($address, $extraLines, $forceLanguage);
     }
 
     // Retrieves the Placemark object (see #getPlacemarkFromJson()) for the @p
     // address, by querying the Google Maps API. Returns the array on success,
     // and null otherwise.
-    private function getPlacemarkForAddress($address) {
-        $url     = $this->getGeocodingUrl($address);
+    private function getPlacemarkForAddress($address, $defaultLanguage) {
+        $url     = $this->getGeocodingUrl($address, $defaultLanguage);
         $geoData = $this->getGeoJsonFromUrl($url);
 
-        return ($geoData ? $this->getPlacemarkFromJson($geoData) : null);
+        return ($geoData ? $this->getPlacemarkFromJson($geoData, $url) : null);
     }
 
     // Prepares address to be geocoded
-    private function prepareAddress(Address &$address) {
+    private function prepareAddress(Address $address) {
         $address->text = preg_replace('/\s*\n\s*/m', "\n", trim($address->text));
     }
 
     // Builds the Google Maps geocoder url to fetch information about @p address.
     // Returns the built url.
-    private function getGeocodingUrl($address) {
+    private function getGeocodingUrl($address, $defaultLanguage) {
         global $globals;
 
         $parameters = array(
-            'key'    => $globals->geocoder->gmaps_key,
-            'sensor' => 'false',   // The queried address wasn't obtained from a GPS sensor.
-            'hl'     => $globals->geocoder->gmaps_hl,
-            'oe'     => 'utf8',    // Output encoding.
-            'output' => 'json',    // Output format.
-            'gl'     => $globals->geocoder->gmaps_gl,
-            'q'      => $address,  // The queries address.
+            'language' => $defaultLanguage,
+            'region'   => $globals->geocoder->gmaps_region,
+            'sensor'   => 'false',  // The queried address wasn't obtained from a GPS sensor.
+            'address'  => $address, // The queries address.
         );
 
-        return $globals->geocoder->gmaps_url . '?' . http_build_query($parameters);
+        return $globals->geocoder->gmaps_url . 'json?' . http_build_query($parameters);
     }
 
     // Fetches JSON-encoded data from a Google Maps API url, and decode them.
@@ -149,131 +161,68 @@ class GMapsGeocoder extends Geocoder {
     }
 
     // Extracts the most appropriate placemark from the JSON data fetched from
-    // Google Maps. Returns a Placemark array on success, and null otherwise. See
-    // http://code.google.com/apis/maps/documentation/services.html#Geocoding_Structured
-    // for details on the Placemark structure.
-    private function getPlacemarkFromJson(array $data) {
-        // Check for geocoding failures.
-        if (!isset($data['Status']['code']) || $data['Status']['code'] != 200) {
-            // TODO: handle non-200 codes in a better way, since the code might
-            // indicate a temporary error on Google's side.
-            return null;
-        }
+    // Google Maps. Returns a Placemark array on success, and null otherwise.
+    // http://code.google.com/apis/maps/documentation/geocoding/#StatusCodes
+    private function getPlacemarkFromJson(array $data, $url) {
+        // Check for geocoding status.
+        $status = $data['status'];
 
-        // Check that at least one placemark was found.
-        if (count($data['Placemark']) == 0) {
+        // If no result, return null.
+        if ($status == 'ZERO_RESULTS') {
             return null;
         }
 
-        // Extract the placemark with the best accuracy. This is not always the
-        // best result (since the same address may yield two different placemarks).
-        $result = $data['Placemark'][0];
-        foreach ($data['Placemark'] as $place) {
-            if ($place['AddressDetails']['Accuracy'] > $result['AddressDetails']['Accuracy']) {
-                $result = $place;
-            }
+        // If there are results return the first one.
+        if ($status == 'OK') {
+            return $data['results'][0];
         }
 
-        return $result;
+        // Report the error.
+        $mailer = new PlMailer('profile/geocoding.mail.tpl');
+        $mailer->assign('status', $status);
+        $mailer->assign('url', $url);
+        $mailer->send();
+        return null;
     }
 
     // Fills the address with the geocoded data
-    private function fillAddressWithGeocoding(Address &$address, $geocodedData) {
-        // The geocoded address three is
-        // Country -> AdministrativeArea -> SubAdministrativeArea -> Locality -> Thoroughfare
-        // with all the possible shortcuts
-        // The address is formatted as xAL, or eXtensible Address Language, an international
-        // standard for address formatting.
-        // xAL documentation: http://www.oasis-open.org/committees/ciq/ciq.html#6
-        $address->geocodedText = str_replace(', ', "\n", $geocodedData['address']);
-        if (isset($geocodedData['AddressDetails']['Accuracy'])) {
-            $address->accuracy = $geocodedData['AddressDetails']['Accuracy'];
-        }
-
-        $currentPosition = $geocodedData['AddressDetails'];
-        if (isset($currentPosition['Country'])) {
-            $currentPosition    = $currentPosition['Country'];
-            $address->countryId = $currentPosition['CountryNameCode'];
-            $address->country   = $currentPosition['CountryName'];
-        }
-        if (isset($currentPosition['AdministrativeArea'])) {
-            $currentPosition                 = $currentPosition['AdministrativeArea'];
-            $address->administrativeAreaName = $currentPosition['AdministrativeAreaName'];
-        }
-        if (isset($currentPosition['SubAdministrativeArea'])) {
-            $currentPosition                    = $currentPosition['SubAdministrativeArea'];
-            $address->subAdministrativeAreaName = $currentPosition['SubAdministrativeAreaName'];
-        }
-        if (isset($currentPosition['Locality'])) {
-            $currentPosition       = $currentPosition['Locality'];
-            $address->localityName = $currentPosition['LocalityName'];
-        }
-        if (isset($currentPosition['Thoroughfare'])) {
-            $address->thoroughfareName = $currentPosition['Thoroughfare']['ThoroughfareName'];
-        }
-        if (isset($currentPosition['PostalCode'])) {
-            $address->postalCode = $currentPosition['PostalCode']['PostalCodeNumber'];
-        }
-
-        // Gets coordinates.
-        if (isset($geocodedData['Point']['coordinates'][0])) {
-            $address->latitude = $geocodedData['Point']['coordinates'][0];
-        }
-        if (isset($geocodedData['Point']['coordinates'][1])) {
-            $address->longitude = $geocodedData['Point']['coordinates'][1];
-        }
-        if (isset($geocodedData['ExtendedData']['LatLonBox']['north'])) {
-            $address->north = $geocodedData['ExtendedData']['LatLonBox']['north'];
-        }
-        if (isset($geocodedData['ExtendedData']['LatLonBox']['south'])) {
-            $address->south = $geocodedData['ExtendedData']['LatLonBox']['south'];
-        }
-        if (isset($geocodedData['ExtendedData']['LatLonBox']['east'])) {
-            $address->east = $geocodedData['ExtendedData']['LatLonBox']['east'];
-        }
-        if (isset($geocodedData['ExtendedData']['LatLonBox']['west'])) {
-            $address->west = $geocodedData['ExtendedData']['LatLonBox']['west'];
-        }
+    private function fillAddressWithGeocoding(Address $address, $geocodedData, $isLocal) {
+        $address->types               = implode(',', $geocodedData['types']);
+        $address->formatted_address   = $geocodedData['formatted_address'];
+        $address->components          = $geocodedData['address_components'];
+        $address->latitude            = $geocodedData['geometry']['location']['lat'];
+        $address->longitude           = $geocodedData['geometry']['location']['lng'];
+        $address->southwest_latitude  = $geocodedData['geometry']['viewport']['southwest']['lat'];
+        $address->southwest_longitude = $geocodedData['geometry']['viewport']['southwest']['lng'];
+        $address->northeast_latitude  = $geocodedData['geometry']['viewport']['northeast']['lat'];
+        $address->northeast_longitude = $geocodedData['geometry']['viewport']['northeast']['lng'];
+        $address->location_type       = $geocodedData['geometry']['location_type'];
+        $address->partial_match       = isset($geocodedData['partial_match']) ? true : false;
     }
 
     // Formats the text of the geocoded address using the unused data and
     // compares it to the given address. If they are too different, the user
     // will be asked to choose between them.
-    private function formatAddress(Address &$address, $extraLines) {
-        $same = true;
-        if ($extraLines) {
-            $address->geocodedText = $extraLines . "\n" . $address->geocodedText;
-        }
-        $geoloc = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
-                                          array('', "\n"), $address->geocodedText));
-        $text   = strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
-                                          array('', "\n"), $address->text));
-        $arrayGeoloc = explode("\n", $geoloc);
-        $arrayText   = explode("\n", $text);
-        $countGeoloc = count($arrayGeoloc);
-        $countText   = count($arrayText);
-
-        $totalDistance = 0;
-        if (($countText > $countGeoloc) || ($countText < $countGeoloc - 1)
-            || (($countText == $countGeoloc - 1)
-                && ($arrayText[$countText - 1] == strtoupper($address->country)))) {
-            $same = false;
-        } else {
-            for ($i = 0; $i < $countGeoloc && $i < $countText; ++$i) {
-                $lineDistance = levenshtein($arrayText[$i], trim($arrayGeoloc[$i]));
-                $totalDistance += $lineDistance;
-                if ($lineDistance > self::MAX_LINE_DISTANCE || $totalDistance > self::MAX_TOTAL_DISTANCE) {
-                    $same = false;
+    private function formatAddress(Address $address, $extraLines, $forceLanguage)
+    {
+        /* XXX: Check how to integrate this in the new geocoding system.
+        if (!$forceLanguage) {
+            $languages = XDB::fetchOneCell('SELECT  IF(ISNULL(gc1.belongsTo), gl1.language, gl2.language)
+                                              FROM  geoloc_countries AS gc1
+                                        INNER JOIN  geoloc_languages AS gl1 ON (gc1.iso_3166_1_a2 = gl1.iso_3166_1_a2)
+                                         LEFT JOIN  geoloc_countries AS gc2 ON (gc1.belongsTo = gc2.iso_3166_1_a2)
+                                         LEFT JOIN  geoloc_languages AS gl2 ON (gc2.iso_3166_1_a2 = gl2.iso_3166_1_a2)
+                                             WHERE  gc1.iso_3166_1_a2 = {?}',
+                                           $address->countryId);
+            $toGeocode = substr($address->text, strlen($extraLines));
+            foreach (explode(',', $languages) as $language) {
+                if ($language != Platal::globals()->geocoder->gmaps_language) {
+                    $geocodedData = $this->getPlacemarkForAddress($toGeocode, $language);
+                    $this->fillAddressWithGeocoding($address, $geocodedData, true);
                     break;
                 }
             }
-        }
-
-        if ($same) {
-            $address->geocodedText = null;
-        } else {
-            $address->geocodedText = str_replace("\n", "\r\n", $address->geocodedText);
-        }
+        }*/
         $address->text = str_replace("\n", "\r\n", $address->text);
     }
 
@@ -292,12 +241,17 @@ class GMapsGeocoder extends Geocoder {
         }
         $textLines  = explode("\n", $text);
         $countLines = count($textLines);
+        if ($countLines < 2) {
+            return $text;
+        }
         $needle     = strtoupper(trim($textLines[$countLines - 2]));
         $isPseudoCountry = false;
-        foreach ($countries as $country) {
-            if (strtoupper($country) == $needle) {
-                $isPseudoCountry = true;
-                break;
+        if ($needle) {
+            foreach ($countries as $country) {
+                if (strtoupper($country) === $needle) {
+                    $isPseudoCountry = true;
+                    break;
+                }
             }
         }
 
@@ -308,5 +262,5 @@ class GMapsGeocoder extends Geocoder {
     }
 }
 
-// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker fenc=utf-8:
 ?>