Browse Source

geocoding

euromark 13 years ago
parent
commit
9e774a4d8f
5 changed files with 929 additions and 13 deletions
  1. 269 0
      Lib/CurlLib.php
  2. 21 13
      Lib/GeocodeLib.php
  3. 136 0
      Lib/HttpSocketLib.php
  4. 324 0
      Model/Behavior/GeocoderBehavior.php
  5. 179 0
      Test/Case/Behavior/GeocoderBehaviorTest.php

+ 269 - 0
Lib/CurlLib.php

@@ -0,0 +1,269 @@
+<?php
+
+/**
+ * Curl wrapper with goodies
+ * - can switch the UA to test certain browser sensitive features
+ * - can simulate/establish tor connection
+ * 
+ * @license MIT
+ * @cakephp 2.0
+ * 2011-07-16 ms
+ */
+class CurlLib {
+
+	public $settings = array(
+		'CURLOPT_SSL_VERIFYPEER' => false,
+	);
+
+	protected $Ch = null;
+	
+	public $cookie = null;
+	
+	public $tor = '127.0.0.1:9050';
+	
+	public $header = array();
+	
+	public $persistentHeader = array();
+	
+	protected $lastUrl = '';
+	
+	public $ua = array(
+		'Firefox' => array(
+			'Firefox/3.0.2 Linux' => 'Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.2) Gecko/2008091700 SUSE/3.0.2-5.2 Firefox/3.0.2'
+		),
+		'IE' => array(
+			'6' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
+			'7' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)',
+			'8' => 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)'
+		),
+		'Konqueror' => array(
+			'Konqueror/3.5' => 'Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko).'
+		),
+		'Opera' => array(
+			'9.60' => 'Opera/9.60 (X11; Linux i686; U; de) Presto/2.1.1',
+			'10' => 'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00'
+		),
+		'Safari' => array(
+			'1.0' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.7',
+			'1.2' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8',
+			'3.3' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5'
+		),
+		'Chrome' => array(
+			'8' => 'Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/8.1.0.0 Safari/540.0'
+		),
+		'Bots' => array(
+			'Google' => 'Googlebot/2.1 (+http://www.google.com/bot.html)'
+		)
+	);
+
+	public function set($key, $value) {
+		return curl_setopt($this->Ch, $key, $value);
+	}
+
+	public function  __construct($timeout = 5, $cookie = true) {
+		$this->cookie = null;
+		if ($cookie !== false) {
+			if ($cookie === true) {
+				$this->cookie['file'] = tempnam(sys_get_temp_dir(), 'curl_cookie');
+				$this->cookie['remove'] = true;
+			} else {
+				$this->cookie['remove'] = false;
+				$this->cookie['file'] = $cookie;
+			}
+		}
+		$this->Ch = curl_init();
+		if ($this->cookie !== false) {
+			$this->set(CURLOPT_COOKIEJAR, $this->cookie['file']);
+			$this->set(CURLOPT_COOKIEFILE, $this->cookie['file']);
+		}
+		$this->set(CURLOPT_FOLLOWLOCATION, true);
+		$this->set(CURLOPT_ENCODING, "");
+		$this->set(CURLOPT_RETURNTRANSFER, true);
+		$this->set(CURLOPT_AUTOREFERER, true);
+		$this->set(CURLOPT_CONNECTTIMEOUT, $timeout);
+		$this->set(CURLOPT_TIMEOUT, $timeout);
+		$this->set(CURLOPT_MAXREDIRS, 10);
+		$this->setUserAgent();
+	}
+
+	public function setUserAgent($ua = 'Firefox', $version = null) {
+		if (isset($this->userAgents[$ua])) {
+			if ($version !== null && isset($this->userAgents[$ua][$version])) {
+				$ua = $this->userAgents[$ua][$version];
+			} else {
+				$ua = array_values($this->userAgents[$ua]);
+				krsort($ua);
+				list($ua) = $ua;
+			}
+		}
+		return $this->set(CURLOPT_USERAGENT, $ua);
+	}
+
+	//TODO: use Dummy.FakerLib instead
+	public function randomizeUserAgent() {
+		//list of browsers
+		$agentBrowser = array(
+			'Firefox',
+			'Safari',
+			'Opera',
+			'Flock',
+			'Internet Explorer',
+			'Seamonkey',
+			'Konqueror',
+			'GoogleBot'
+		);
+		//list of operating systems
+		$agentOS = array(
+			'Windows 3.1',
+			'Windows 95',
+			'Windows 98',
+			'Windows 2000',
+			'Windows NT',
+			'Windows XP',
+			'Windows Vista',
+			'Redhat Linux',
+			'Ubuntu',
+			'Fedora',
+			'AmigaOS',
+			'OS 10.5'
+		);
+		//randomly generate UserAgent
+		$ua = $agentBrowser[rand(0,count($agentBrowser)-1)].'/'.rand(1,8).'.'.rand(0,9).' (' .$agentOS[rand(0,count($agentOS)-1)].' '.rand(1,7).'.'.rand(0,9).'; en-US;)';
+		$this->setUserAgent($ua);
+		return $ua;
+	}
+
+	public function setSocks5Proxy($proxy = false) {
+		$this->set(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
+		if ($proxy) {
+			return $this->set(CURLOPT_PROXY, $proxy);
+		} else {
+			return $this->set(CURLOPT_PROXY, false);
+		}
+	}
+
+	public function setHttpProxy($proxy = false) {
+		$this->set(CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
+		if ($proxy) {
+			return $this->set(CURLOPT_PROXY, $proxy);
+		} else {
+			return $this->set(CURLOPT_PROXY, false);
+		}
+	}
+
+	public function setTor($tor = null) {
+		if ($tor === null) {
+			$tor = $this->tor;
+		}
+		return $this->setSocks5Proxy($tor);
+	}
+
+	public function setHeader($key, $header, $persistent = false) {
+		if ($persistent) {
+			$this->persistentHeader[$key] = $header;
+		} else {
+			$this->header[$key] = $header;
+		}
+	}
+
+	public function unsetHeader($key, $persistent = false) {
+		if ($persistent) {
+			unset($this->persistentHeader[$key]);
+		} else {
+			unset($this->header[$key]);
+		}
+	}
+
+	public function exec() {
+		$header = array();
+		foreach ($this->header as $tk => $tv) {
+			$header[] = $tk . ': ' . $tv;
+		}
+		$this->set(CURLOPT_HTTPHEADER, $header);
+		$this->header = $this->persistentHeader;
+		$content = curl_exec($this->Ch);
+		$info = curl_getinfo($this->Ch);
+		return array($content, $info);
+	}
+
+	/**
+	 * get/set referer
+	 * 
+	 * @param url
+	 * @return mixed
+	 * 2012-06-06 ms
+	 */
+	public function referer($url = null) {
+		if ($url === null) {
+			if ($this->lastUrl !== null) {
+				return $this->set(CURLOPT_REFERER, $this->lastUrl);
+			}
+		} else {
+			$this->lastUrl = null;
+			return $this->set(CURLOPT_REFERER, $url);
+		}
+		return false;
+	}
+
+	protected function _prepareData($url, $getdata = array(), $data = array()) {
+		if (strpos($url, '?') === false && ( //If Url has not a "?" in it
+				(is_array($getdata) && !empty($getdata)) || //And $getdata is array and has more than one value
+				(!is_array($getdata) && strlen($getdata) > 0))) { //or its a a string and is longer than 0
+			$url .= '?';
+		}
+		$data = array(
+			$getdata,
+			$data
+		);
+		foreach ($data as $i => $part) {
+			if (is_array($part)) {
+				$string = '';
+				foreach ($part as $key => $value) {
+					$string .= urlencode($key) . '=' . urlencode($value) . '&';
+				}
+				$part = rtrim($string, '&');
+			} else {
+				$part = urlencode($part);
+			}
+			$data[$i] = $part;
+		}
+		$data[0] = $url . $data[0];
+		return $data;
+	}
+
+	public function post($url, $data = array(), $getdata = array()) {
+		$this->referer();
+		$this->set(CURLOPT_POST, true);
+
+		$data = $this->_prepareData($url, $getdata, $data);
+
+		$this->set(CURLOPT_URL, $data[0]);
+		$this->set(CURLOPT_POSTFIELDS, $data[1]);
+		return $this->exec();
+	}
+
+	public function get($url, $data = array()) {
+		$this->referer();
+		$this->set(CURLOPT_HTTPGET, true);
+
+		$data = $this->_prepareData($url, $data);
+
+		$this->set(CURLOPT_URL, $data[0]);
+		$this->set(CURLOPT_SSL_VERIFYPEER, false);
+		$this->lastUrl = $url;
+		return $this->exec();
+	}
+
+	public function  __destruct() {
+		if ($this->cookie !== false) {
+			if (isset($this->cookie['handle'])) {
+				fclose($this->cookie['handle']);
+			}
+			if ($this->cookie['remove']) {
+				unlink($this->cookie['file']);
+			}
+		}
+		curl_close($this->Ch);
+	}
+
+}

+ 21 - 13
Lib/GeocodeLib.php

@@ -6,7 +6,11 @@ App::uses('HttpSocketLib', 'Tools.Lib');
 /**
  * geocode via google (UPDATE: api3)
  * @see DEPRECATED api2: http://code.google.com/intl/de-DE/apis/maps/articles/phpsqlgeocode.html
- * @sse http://code.google.com/intl/de/apis/maps/documentation/geocoding/#Types
+ * @see http://code.google.com/intl/de/apis/maps/documentation/geocoding/#Types
+ * 
+ * @author Mark Scherer
+ * @cakephp 2.x
+ * @licence MIT
  * 2010-06-25 ms
  */
 class GeocodeLib {
@@ -355,7 +359,12 @@ class GeocodeLib {
 			if ($this->options['output'] == 'json') {
 				//$res = json_decode($result);
 			} else {
-				$res = Xml::build($result);
+				try {
+					$res = Xml::build($result);
+				} catch (Exception $e) {
+					CakeLog::write('geocode', $e->getMessage());
+					$res = array();
+				}
 			}
 
 			if (!is_object($res)) {
@@ -676,7 +685,10 @@ class GeocodeLib {
 
 
 	/**
-	 * fuzziness filter for coordinates (lat or lng)
+	 * Fuzziness filter for coordinates (lat or lng).
+	 * Useful if you store other users' locations and want to grant some 
+	 * privacy protection. This way the coordinates will be slightly modified.
+	 * 
 	 * @param float coord
 	 * @param int level (0 = nothing to 5 = extrem)
 	 * - 1:
@@ -684,10 +696,9 @@ class GeocodeLib {
 	 * - 3:
 	 * - 4:
 	 * - 5:
-	 * @static
 	 * 2011-03-16 ms
 	 */
-	public function blur($coord, $level = 0) {
+	public static function blur($coord, $level = 0) {
 		if (!$level) {
 			return $coord;
 		}
@@ -773,7 +784,7 @@ Array
 
 					[1] => Array
 						(
-							[long_name] => Schwäbisch Hall
+							[long_name] => Schwaebisch Hall
 							[short_name] => SHA
 							[Type] => Array
 								(
@@ -785,7 +796,7 @@ Array
 
 					[2] => Array
 						(
-							[long_name] => Baden-Württemberg
+							[long_name] => Baden-Wuerttemberg
 							[short_name] => BW
 							[Type] => Array
 								(
@@ -882,10 +893,10 @@ Array
 							[CountryName] => Deutschland
 							[AdministrativeArea] => Array
 								(
-									[AdministrativeAreaName] => Baden-Württemberg
+									[AdministrativeAreaName] => Baden-Wuerttemberg
 									[SubAdministrativeArea] => Array
 										(
-											[SubAdministrativeAreaName] => Schwäbisch Hall
+											[SubAdministrativeAreaName] => Schwaebisch Hall
 											[PostalCode] => Array
 												(
 													[PostalCodeNumber] => 74523
@@ -918,10 +929,7 @@ Array
 
 		)
 
-)
-
-
-{
+) {
 	"status": "OK",
 	"results": [ {
 	"types": [ "street_address" ],

+ 136 - 0
Lib/HttpSocketLib.php

@@ -0,0 +1,136 @@
+<?php
+App::uses('HttpSocket', 'Network/Http');
+App::uses('CurlLib', 'Tools.Lib');
+
+/**
+ * Wrapper for curl, php or file_get_contents
+ * 
+ * @author Mark Scherer
+ * @license MIT
+ * @cakephp 2.0
+ * 2011-10-14 ms
+ */
+class HttpSocketLib {
+
+	// First tries with curl, then cake, then php
+	public $use = array('curl' => true, 'cake'=> true, 'php' => true);
+	public $debug = null;
+	public $timeout = 5;
+	public $cacheUsed = null;
+	public $error = array();
+
+	public function __construct($use = array()) {
+		if (is_array($use)) {
+			foreach ($use as $key => $value) {
+				if (array_key_exists($key, $this->use)) {
+					$this->use[$key] = $value;
+				}
+			}
+		} elseif (array_key_exists($use, $this->use)) {
+			$this->use[$use] = true;
+			if ($use == 'cake') {
+				$this->use['curl'] = false;
+			} elseif ($use == 'php') {
+				$this->use['curl'] = $this->use['cake'] = false;
+			}
+		}
+	}
+
+	public function setError($error) {
+		if (empty($error)) {
+			return;
+		}
+		$this->error[] = $error;
+	}
+
+	public function error($asString = true, $separator = ', ') {
+		return implode(', ', $this->error);
+	}
+	
+	public function reset() {
+		$this->error = array();
+		$this->debug = null;
+	}
+
+
+	/**
+	 * fetches url with curl if available
+	 * fallbacks: cake and php
+	 * note: expects url with json encoded content
+	 * @access private
+	 **/
+	public function fetch($url, $options = array()) {
+		if (!is_array($options)) {
+			$options = array('agent'=>$options);
+		}
+		$defaults = array(
+			'agent' => 'cakephp http socket lib',
+			'cache' => false,
+			'clearCache' => false,
+			'use' => $this->use,
+			'timeout' => $this->timeout,
+		);
+		$options = am($defaults, $options);
+		
+		# cached?
+		if ($options['cache']) {
+			$cacheName = md5($url);
+			$cacheConfig = $options['cache'] === true ? null: $options['cache'];
+			$cacheConfig = !Cache::isInitialized($cacheConfig) ? null : $cacheConfig;
+			
+			if ($options['clearCache']) {
+				Cache::delete('http_'.$cacheName, $cacheConfig);
+			} elseif (($res = Cache::read('http_'.$cacheName, $cacheConfig)) !== false && $res !== null) {
+				$this->cacheUsed = true;
+				return $res;
+			}
+		}
+		$res = $this->_fetch($url, $options);
+		if ($options['cache']) {
+			Cache::write('http_'.$cacheName, $res, $cacheConfig);
+		}
+		return $res;
+	}
+	
+	public function _fetch($url, $options) {
+		if ($options['use']['curl'] && function_exists('curl_init')) {
+			$this->debug = 'curl';
+			$Ch = new CurlLib();
+			$Ch->setUserAgent($options['agent']);
+			$data = $Ch->get($url);
+			$response = $data[0];
+			$status = $data[1]['http_code'];
+			if ($status != '200') {
+				$this->setError('Error '.$status);
+				return false;
+			}
+			return $response;
+
+		} elseif ($options['use']['cake']) {
+			$this->debug = 'cake';
+
+			$HttpSocket = new HttpSocket(array('timeout' => $options['timeout']));
+			$response = $HttpSocket->get($url);
+			if ($response->code != 200) { //TODO: status 200?
+				return false;
+			}
+			return $response;
+
+		} elseif ($options['use']['php']) {
+			$this->debug = 'php';
+
+			$response = file_get_contents($url, 'r');
+			//TODO: status 200?
+			if (empty($response)) {
+				return false;
+			}
+			return $response;
+			
+		} else {
+			throw new CakeException('no protocol given');
+		}
+		return null;
+	}
+
+
+}

+ 324 - 0
Model/Behavior/GeocoderBehavior.php

@@ -0,0 +1,324 @@
+<?php
+App::uses('GeocodeLib', 'Tools.Lib');
+
+/**
+ * A geocoding behavior for CakePHP to easily geocode addresses.
+ * Uses the GeocodeLib for actual geocoding.
+ * 
+ * @author Mark Scherer
+ * @cakephp 2.x
+ * @licence MIT
+ * 2011-01-13 ms
+ */
+class GeocoderBehavior extends ModelBehavior {
+
+	/**
+	 * Initiate behavior for the model using specified settings. Available settings:
+	 *
+	 * - label: (array | string, optional) set to the field name that contains the
+	 * 			string from where to generate the slug, or a set of field names to
+	 * 			concatenate for generating the slug. DEFAULTS TO: title
+	 *
+	 * - real: (boolean, optional) if set to true then field names defined in
+	 * 			label must exist in the database table. DEFAULTS TO: true
+	 *
+	 * - expect: (array)postal_code, locality, sublocality, ...
+	 *
+	 * - accuracy: see above
+	 *
+	 * - override: lat/lng override on changes?
+	 *
+	 * - update: what fields to update (key=>value array pairs)
+	 *
+	 * - before: validate/save (defaults to save)
+	 * 			set to false if you only want to use the validation rules etc
+	 *
+	 * @param object $Model Model using the behaviour
+	 * @param array $settings Settings to override for model.
+	 * 2011-01-13 ms
+	 */
+	public function setup(Model $Model, $settings = array()) {
+		$default = array(
+			'real' => true, 'address' => array('street', 'postal_code', 'city', 'country'),
+			'require'=>false, 'allowEmpty'=>true, 'invalidate' => array(), 'expect'=>array(),
+			'lat'=>'lat', 'lng'=>'lng', 'formatted_address'=>'formatted_address', 'host' => 'de', 'language' => 'de', 'region'=> '', 'bounds' => '', 'overwrite' => false, 'update'=>array(), 'before'=>'save',
+			'min_accuracy' => 0, 'allow_inconclusive' => true,
+			'log' => true, // log successfull results to geocode.log (errors will be logged to error.log in either case)
+		);
+
+		if (!isset($this->settings[$Model->alias])) {
+			$this->settings[$Model->alias] = $default;
+		}
+
+		$this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], is_array($settings) ? $settings : array());
+	}
+
+
+	public function beforeValidate(Model $Model) {
+		parent::beforeValidate($Model);
+
+		if ($this->settings[$Model->alias]['before'] == 'validate') {
+			return $this->geocode($Model);
+		}
+
+		return true;
+	}
+
+	public function beforeSave(Model $Model) {
+		parent::beforeSave($Model);
+
+		if ($this->settings[$Model->alias]['before'] == 'save') {
+			return $this->geocode($Model);
+		}
+
+		return true;
+	}
+
+
+	/**
+	 * Run before a model is saved, used to set up slug for model.
+	 *
+	 * @param object $Model Model about to be saved.
+	 * @return boolean true if save should proceed, false otherwise
+	 */
+	public function geocode(Model $Model, $return = true) {
+		// Make address fields an array
+		if (!is_array($this->settings[$Model->alias]['address'])) {
+			$addressfields = array($this->settings[$Model->alias]['address']);
+		} else {
+			$addressfields = $this->settings[$Model->alias]['address'];
+		}
+		$addressfields = array_unique($addressfields);
+
+		// Make sure all address fields are available
+
+		if ($this->settings[$Model->alias]['real']) {
+			foreach ($addressfields as $field) {
+				if (!$Model->hasField($field)) {
+					return $return;
+				}
+			}
+		}
+
+		$adressdata = array();
+		foreach ($addressfields as $field) {
+			if (!empty($Model->data[$Model->alias][$field])) {
+				$adressdata[] = $Model->data[$Model->alias][$field];
+			}
+		}
+
+		$Model->data[$Model->alias]['geocoder_result'] = array();
+
+		// See if we should request a geocode
+		if ((!$this->settings[$Model->alias]['real'] || ($Model->hasField($this->settings[$Model->alias]['lat']) && $Model->hasField($this->settings[$Model->alias]['lng']))) && ($this->settings[$Model->alias]['overwrite'] || (empty($Model->data[$Model->alias][$this->settings[$Model->alias]['lat']]) || ($Model->data[$Model->alias][$this->settings[$Model->alias]['lat']]==0 && $Model->data[$Model->alias][$this->settings[$Model->alias]['lat']]==0)))) {
+
+			if (!empty($Model->whitelist) && (!in_array($this->settings[$Model->alias]['lat'], $Model->whitelist) || !in_array($this->settings[$Model->alias]['lng'], $Model->whitelist))) {
+				/** HACK to prevent 0 inserts if not wanted! just use whitelist now to narrow fields down - 2009-03-18 ms */
+				//$Model->whitelist[] = $this->settings[$Model->alias]['lat'];
+				//$Model->whitelist[] = $this->settings[$Model->alias]['lng'];
+				return $return;
+			}
+
+			$geocode = $this->_geocode($adressdata, $this->settings[$Model->alias]);
+
+			if (empty($geocode) && !empty($this->settings[$Model->alias]['allowEmpty'])) {
+				return true;
+			}
+			if (empty($geocode)) {
+				return false;
+			}
+
+			if (!empty($geocode['type']) && !empty($this->settings[$Model->alias]['expect'])) {
+				if (!in_array($geocode['type'], (array)$this->settings[$Model->alias]['expect'])) {
+					return $return;
+				}
+			}
+
+			//pr($geocode);
+			//pr($this->Geocode->getResult());
+			// Now set the geocode as part of the model data to be saved, making sure that
+			// we are on the white list of fields to be saved
+			//pr ($Model->whitelist); die();
+
+			//pr($geocode); die();
+
+			# if both are 0, thats not valid, otherwise continue
+			if (!empty($geocode['lat']) || !empty($geocode['lng'])) { /** HACK to prevent 0 inserts of incorrect runs - 2009-04-07 ms */
+				$Model->data[$Model->alias][$this->settings[$Model->alias]['lat']] = $geocode['lat'];
+				$Model->data[$Model->alias][$this->settings[$Model->alias]['lng']] = $geocode['lng'];
+			} else {
+				if (isset($Model->data[$Model->alias][$this->settings[$Model->alias]['lat']])) {
+					unset($Model->data[$Model->alias][$this->settings[$Model->alias]['lat']]);
+				}
+				if (isset($Model->data[$Model->alias][$this->settings[$Model->alias]['lng']])) {
+					unset($Model->data[$Model->alias][$this->settings[$Model->alias]['lng']]);
+				}
+				if ($this->settings[$Model->alias]['require']) {
+					if ($fields = $this->settings[$Model->alias]['invalidate']) {
+						$Model->invalidate($fields[0], $fields[1], isset($fields[2]) ? $fields[2] : true);
+					}
+					return false;
+				}
+			}
+
+			if (!empty($this->settings[$Model->alias]['formatted_address'])) {
+				$Model->data[$Model->alias][$this->settings[$Model->alias]['formatted_address']] = $geocode['formatted_address'];
+			} else {
+				if (isset($Model->data[$Model->alias][$this->settings[$Model->alias]['formatted_address']])) {
+					unset($Model->data[$Model->alias][$this->settings[$Model->alias]['formatted_address']]);
+				}
+			}
+
+			if (!empty($geocode['inconclusive'])) {
+				$Model->data[$Model->alias]['geocoder_inconclusive'] = $geocode['inconclusive'];
+				$Model->data[$Model->alias]['geocoder_results'] = $geocode['results'];
+			} else {
+				$Model->data[$Model->alias]['geocoder_result'] = $geocode;
+			}
+
+			$Model->data[$Model->alias]['geocoder_result']['address_data'] = implode(' ', $adressdata);
+
+			if (!empty($this->settings[$Model->alias]['update'])) {
+				foreach ($this->settings[$Model->alias]['update'] as $key => $field) {
+					if (!empty($geocode[$key])) {
+						$Model->data[$Model->alias][$field] = $geocode[$key];
+					}
+				}
+			}
+
+
+			# correct country id if neccessary
+			/*
+			if (in_array('country_name', $this->settings[$Model->alias]['address'])) {
+				App::uses('Country', 'Tools.Model');
+
+				if (!empty($geocode['country']) && in_array($geocode['country'], ($countries = Country::addressList()))) {
+					$countries = array_shift(array_keys($countries, $geocode['country']));
+					$Model->data[$Model->alias]['country'] = $countries;
+				} else {
+					$Model->data[$Model->alias]['country'] = 0;
+				}
+			}
+			*/
+		}
+
+		return $return;
+	}
+
+	public function setDistanceAsVirtualField(Model $Model, $lat, $lng, $modelName = null) {
+		$Model->virtualFields['distance'] = $this->distance($Model, $lat, $lng, $modelName);
+	}
+
+	public function distanceConditions(Model $Model, $distance = null, $modelName = null) {
+		if ($modelName === null) {
+			$modelName = $Model->alias;
+		}
+		$conditions = array(
+			$modelName . '.lat <> 0',
+			$modelName . '.lng <> 0',
+		);
+		if ($distance !== null) {
+			$conditions[] = '1=1 HAVING distance < ' . intval($distance);
+		}
+		return $conditions;
+	}
+
+	public function distanceField(Model $Model, $lat, $lng, $fieldName = null, $modelName = null) {
+		if ($modelName === null) {
+			$modelName = $Model->alias;
+		}
+		return $this->distance($Model, $lat, $lng, $modelName).
+			' '.
+			'AS '.(!empty($fieldName) ? $fieldName : 'distance');
+	}
+
+	public function distance(Model $Model, $lat, $lng, $modelName = null) {
+		if ($modelName === null) {
+			$modelName = $Model->alias;
+		}
+		return '6371.04 * ACOS( COS( PI()/2 - RADIANS(90 - '.$modelName.'.lat)) * ' .
+			'COS( PI()/2 - RADIANS(90 - '. $lat .')) * ' .
+			'COS( RADIANS('.$modelName.'.lng) - RADIANS('. $lng .')) + ' .
+			'SIN( PI()/2 - RADIANS(90 - '.$modelName.'.lat)) * ' .
+			'SIN( PI()/2 - RADIANS(90 - '. $lat . ')))';
+	}
+
+	public function distanceByField(Model $Model, $lat, $lng, $byFieldName = null, $fieldName = null, $modelName = null) {
+		if ($modelName === null) {
+			$modelName = $Model->alias;
+		}
+		if ($fieldName === null) {
+			$fieldName = 'distance';
+		}
+		if ($byFieldName === null) {
+			$byFieldName = 'radius';
+		}
+
+		return $this->distance($Model, $lat, $lng, $modelName).' '.$byFieldName;
+	}
+
+	public function paginateDistanceCount(Model $Model, $conditions = null, $recursive = -1, $extra = array()) {
+		if (!empty($extra['radius'])) {
+			$conditions[] = $extra['distance'].' < '.$extra['radius'].(!empty($extra['startRadius'])?' AND '.$extra['distance'].' > '.$extra['startRadius']:'').(!empty($extra['endRadius'])?' AND '.$extra['distance'].' < '.$extra['endRadius']:'');
+		}
+		if (!empty($extra['group'])) {
+			unset($extra['group']);
+		}
+		$extra['behavior'] = true;
+		return $Model->paginateCount($conditions, $recursive, $extra);
+	}
+
+
+	/**
+	 * Returns if a latitude is valid or not.
+	 * validation rule for models
+	 *
+	 * @param Model
+	 * @param float $latitude
+	 * @return bool
+	 */
+	public function validateLatitude(Model $Model, $latitude) {
+		if (is_array($latitude)) {
+			$latitude = array_shift($latitude);
+		}
+		return ($latitude <= 90 && $latitude >= -90);
+	}
+
+	/**
+	 * Returns if a longitude is valid or not.
+	 * validation rule for models
+	 *
+	 * @param Model
+	 * @param float $longitude
+	 * @return bool
+	 */
+	public function validateLongitude(Model $Model, $longitude) {
+		if (is_array($longitude)) {
+			$longitude = array_shift($longitude);
+		}
+		return ($longitude <= 180 && $longitude >= -180);
+	}
+
+
+	protected function _geocode($addressfields, $options = array()) {
+		$address = implode(' ', $addressfields);
+		if (empty($address)) {
+			return array();
+		}
+
+		$geocodeOptions = array('log'=>$options['log'], 'min_accuracy'=>$options['min_accuracy'], 'expect'=>$options['expect'], 'allow_inconclusive'=>$options['allow_inconclusive'], 'host'=>$options['host']);
+		$this->Geocode = new GeocodeLib($geocodeOptions);
+
+		$settings = array('language' => $options['language']);
+		if (!$this->Geocode->geocode($address, $settings)) {
+			return array('lat' => 0, 'lng' => 0, 'formatted_address' => '');
+		}
+
+		$res = $this->Geocode->getResult();
+		if (isset($res[0])) {
+			$res = $res[0];
+		}
+		return $res;
+	}
+
+}

+ 179 - 0
Test/Case/Behavior/GeocoderBehaviorTest.php

@@ -0,0 +1,179 @@
+<?php
+
+App::uses('GeocoderBehavior', 'Tools.Model/Behavior');
+App::uses('Set', 'Utility');
+//App::uses('Model', 'Model');
+App::uses('AppModel', 'Model');
+
+class GeocoderBehaviorTest extends CakeTestCase {
+
+	public $fixtures = array(
+		'core.comment'
+	);
+
+
+	public function startTest() {
+		$this->Comment = ClassRegistry::init('Comment');
+
+
+		$this->Comment->Behaviors->attach('Tools.Geocoder', array('real'=>false));
+	}
+
+	public function testBasic() {
+		echo '<h3>'.__FUNCTION__.'</h3>';
+		
+		$data = array(
+			'street' => 'Krebenweg 22',
+			'zip' => '74523',
+			'city' => 'Bibersfeld'
+		);
+		$res = $this->Comment->save($data);
+		debug($res);
+		$this->assertTrue(!empty($res['Comment']['lat']) && !empty($res['Comment']['lng']) && round($res['Comment']['lat']) === 49.0 && round($res['Comment']['lng']) === 10.0);
+		// accuracy = 4
+		
+		
+		# inconclusive
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'München'
+		);
+		$res = $this->Comment->save($data);
+		$this->assertEquals('', $this->Comment->Behaviors->Geocoder->Geocode->error());
+
+		debug($res);
+		$this->assertTrue(!empty($res['Comment']['lat']) && !empty($res['Comment']['lng']));
+		$this->assertEquals('München, Deutschland', $res['Comment']['geocoder_result']['formatted_address']);
+		
+		$data = array(
+			'city' => 'Bibersfeld'
+		);
+		$res = $this->Comment->save($data);
+		debug($res);
+		$this->assertTrue(!empty($res));
+		$this->assertEquals('', $this->Comment->Behaviors->Geocoder->Geocode->error());
+	}
+
+	public function testMinAccLow() {
+		echo '<h3>'.__FUNCTION__.'</h3>';
+		
+		$this->Comment->Behaviors->detach('Geocoder');
+		$this->Comment->Behaviors->attach('Geocoder', array('real'=>false, 'min_accuracy'=>0));
+		// accuracy = 1
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'Deutschland'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($res);
+
+		//debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(!empty($res['Comment']['lat']) && !empty($res['Comment']['lng']));
+
+	}
+
+	public function testMinAccHigh() {
+		echo '<h3>'.__FUNCTION__.'</h3>';
+		
+		$this->Comment->Behaviors->detach('Geocoder');
+		$this->Comment->Behaviors->attach('Geocoder', array('real'=>false, 'min_accuracy'=>4));
+		// accuracy = 1
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'Deutschland'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($res);
+
+		//debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(!isset($res['Comment']['lat']) && !isset($res['Comment']['lng']));
+
+	}
+
+
+	public function testMinInc() {
+		echo '<h3>'.__FUNCTION__.'</h3>';
+		
+		$this->Comment->Behaviors->detach('Geocoder');
+		$this->Comment->Behaviors->attach('Geocoder', array('real'=>false, 'min_accuracy'=>GeocodeLib::ACC_SUBLOC));
+		
+		$this->assertEquals(GeocodeLib::ACC_SUBLOC, $this->Comment->Behaviors->Geocoder->settings['Comment']['min_accuracy']);
+		
+		// accuracy = 1
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'Neustadt'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($this->Comment->Behaviors->Geocoder->Geocode->getResult()).BR;
+
+		debug($res);
+
+		//debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(!isset($res['Comment']['lat']) && !isset($res['Comment']['lng']));
+
+	}
+
+	public function testMinIncAllowed() {
+		echo '<h3>'.__FUNCTION__.'</h3>';
+		
+		$this->Comment->Behaviors->detach('Geocoder');
+		$this->Comment->Behaviors->attach('Geocoder', array('real'=>false, 'allow_inconclusive'=>true));
+		// accuracy = 1
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'Neustadt'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($this->Comment->Behaviors->Geocoder->Geocode->url()).BR;
+
+		debug($this->Comment->Behaviors->Geocoder->Geocode->getResult()).BR;
+
+		debug($res);
+
+		//debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(!empty($res['Comment']['lat']) && !empty($res['Comment']['lng']));
+
+	}
+
+	public function testExpect() {
+		$this->Comment->Behaviors->detach('Geocoder');
+		$this->Comment->Behaviors->attach('Geocoder', array('real'=>false, 'expect'=>array('postal_code')));
+		// accuracy = 1
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => 'Bibersfeld'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($res);
+
+		debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(empty($res['Comment']['lat']) && empty($res['Comment']['lng']));
+
+		$data = array(
+	 		//'street' => 'Leopoldstraße',
+			'city' => '74523'
+		);
+		$res = $this->Comment->save($data);
+		debug($this->Comment->Behaviors->Geocoder->Geocode->error()).BR;
+
+		debug($res);
+
+		//debug($this->Comment->Behaviors->Geocoder->Geocode->debug());
+		$this->assertTrue(!empty($res['Comment']['lat']) && !empty($res['Comment']['lng']));
+	}
+
+}
+
+
+