Created
December 14, 2010 19:49
-
-
Save ryantology/740967 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
uses('http_socket'); | |
class GeocodedBehavior extends ModelBehavior { | |
/** | |
* Index of geo-data lookup services. Each item contains a lookup URL with placeholders, | |
* and a regular expression to parse latitude and longitude values. | |
* | |
* @var array | |
* @access public | |
*/ | |
var $lookupServices = array( | |
'google' => array( | |
'http://maps.google.com/maps/geo?&q=%address&output=csv&key=%key', | |
'/200,[^,]+,([^,]+),([^,\s]+)/' | |
), | |
'yahoo' => array( | |
'http://api.local.yahoo.com/MapsService/V1/geocode?appid=%key&location=%address', | |
'/<Latitude>(.*)<\/Latitude><Longitude>(.*)<\/Longitude>/U' | |
) | |
); | |
/** | |
* Units relative to 1 kilometer. | |
* k: kilometers, m: miles, f: feet, i: inches, n: nautical miles | |
* | |
* @var array | |
* @access protected | |
*/ | |
protected $units = array('k' => 1, 'K' => 1.609344, 'm' => 0.621371192, 'f' => 3280.8399, 'i' => 39370.0787, 'n' => 0.539956803); | |
function setup(&$model, $config = array()) { | |
$this->settings[$model->name] = am(array( | |
'lookup' => 'google', | |
'key' => Configure::read('GMAPS_APIKEY'), | |
'cacheTable' => 'geocodes', | |
'fields' => array('lat', 'lon') | |
), $config); | |
extract($this->settings[$model->name]); | |
if (!isset($model->Geocode)) { | |
if (App::import('Geocode')) { | |
$model->Geocode =& new Geocode(); | |
} else { | |
$model->Geocode =& new DynamicModel(array('name' => 'Geocode', 'table' => $cacheTable)); | |
} | |
} | |
if (!isset($this->lookupServices[low($lookup)])) { | |
trigger_error('The lookup service "' . $lookup . '" does not exist.', E_USER_WARNING); | |
return false; | |
} | |
if (!isset($this->connection)) { | |
$this->connection = new HttpSocket(); | |
} | |
} | |
/** | |
* Get the geocode latitude/longitude points from given address. | |
* Look in the cache first, otherwise get from web service (Google/Yahoo!) | |
* | |
* @param string $address | |
*/ | |
function geocode(&$model, $address) { | |
extract($this->settings[$model->name]); | |
if (is_array($address)) { | |
$out = ''; | |
if (isset($address[$model->name])) { | |
$address = $address[$model->name]; | |
} | |
$vars = array('postal'); | |
foreach ($vars as $var) { | |
if (isset($address[$var])) { | |
$out = trim($out) . ' ' . $address[$var]; | |
} | |
} | |
$address = trim($out); | |
} | |
if (empty($address)) { | |
// trigger_error | |
return false; | |
} | |
if (!$code = $model->Geocode->findByAddress(low($address))) { | |
if ($code = $this->_geocoords($model, $address)) { | |
$model->Geocode->create(); | |
$model->Geocode->save(array('address' => low($address), 'lat' => $code[$fields[0]], 'lon' => $code[$fields[1]])); | |
} | |
} else { | |
$code = array($fields[0] => $code['Geocode']['lat'], $fields[1] => $code['Geocode']['lon']); | |
} | |
return array_reverse($code); | |
} | |
/** | |
* Get geocode lat/lon points for given address from web service (Google/Yahoo!) | |
* | |
* @param string $address | |
* @access private | |
* @return array Latitude and longitude data, or false on failure | |
*/ | |
function _geocoords(&$model, $address) { | |
extract($this->settings[$model->name]); | |
$url = r( | |
array('%key', '%address'), | |
array($key, rawurlencode($address)), | |
$this->lookupServices[low($lookup)][0] | |
); | |
$code = false; | |
if($result = $this->connection->get($url)) { | |
if (preg_match($this->lookupServices[low($lookup)][1], $result, $match)) { | |
$code = array($fields[0] => floatval($match[1]), $fields[1] => floatval($match[2])); | |
} | |
} | |
return $code; | |
} | |
/** | |
* Calculate the distance between to geographic coordinates using the circle distance formula | |
* | |
* @param float $lat1 | |
* @param float $lat2 | |
* @param float $lon1 | |
* @param float $lon2 | |
* @param float $unit M=miles, K=kilometers, N=nautical miles, I=inches, F=feet | |
*/ | |
function distance($lat1, $lon1, $lat2 = null, $lon2 = null, $unit = 'M') { | |
$m = 69.09 * rad2deg(acos(sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($lon1 - $lon2)))); | |
if (isset($this->units[up($unit)])) { | |
$m *= $this->units[up($unit)]; | |
} | |
return $m; | |
} | |
/** | |
* Generates an SQL query to calculate the distance between the coordinates of each record and the given x/y values, | |
* and compares the result to $distance. | |
* | |
* @param mixed $x Either a float or an array. If an array, it should contain the X and Y values of the coordinate. | |
* @param mixed $y If $x is an array, this value is used as $distance, otherwise, the Y coordinate. | |
* @param float $distance The distance (in miles) to search within | |
*/ | |
function findallbydistance(&$model, $x, $y, $distance = null) { | |
extract($this->settings[$model->name]); | |
if (is_array($x)) { | |
$distance = $y; | |
list($x, $y) = array_values($x); | |
} | |
list($x2, $y2) = array($model->escapeField($fields[1]), $model->escapeField($fields[0])); | |
list($x, $y, $distance) = array(floatval($x), floatval($y), floatval($distance)); | |
return $model->findAll("(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180) <= {$distance}*1.609344"); | |
} | |
function findProximityCondition(&$model, $x, $y, $distance = null) { | |
extract($this->settings[$model->name]); | |
if (is_array($x)) { | |
$distance = $y; | |
list($x, $y) = array_values($x); | |
} | |
list($x2, $y2) = array($model->escapeField($fields[1]), $model->escapeField($fields[0])); | |
list($x, $y, $distance) = array(floatval($x), floatval($y), floatval($distance)); | |
return "(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180) <= {$distance}*1.609344"; | |
} | |
/** | |
* Give back needed condition / ordering clause to find points near given point | |
* Hacked from http://github.com/mariano/syrup/blob/a122c1c058abb662d5c096076930b0ba78053dd7/plugins/geocode/models/behaviors/geocodable.php | |
* @param object $model Model | |
* @param array $point Point (latitude, longitude), expressed in numeric degrees | |
* @param float $distance If specified, add condition to only match points within given distance | |
* @param string $unit Unit (k: kilometers, m: miles, f: feet, i: inches, n: nautical miles) | |
* @param string $direction Sorting direction (ASC / DESC) | |
* @return array Query parameters (conditions, order) | |
* @access public | |
*/ | |
public function distanceQuery(&$model, $point, $distance = null, $unit = 'k', $direction = 'ASC') { | |
$unit = (!empty($unit) && array_key_exists(strtolower($unit), $this->units) ? $unit : 'k'); | |
$settings = $this->settings[$model->alias]; | |
foreach($point as $k => $v) { | |
$point[$k] = floatval($v); | |
} | |
list($latitude, $longitude) = $point; | |
list($latitudeField, $longitudeField) = array( | |
$model->escapeField($settings['fields'][0]), | |
$model->escapeField($settings['fields'][1]), | |
); | |
$earthRadiusKm = 6371; | |
$expression = '(' . $earthRadiusKm . ' * 2 * ATAN2( | |
SQRT( | |
SIN(RADIANS(' . $latitude . ' - ' . $latitudeField . ')/2) * SIN(RADIANS(' . $latitude . ' - ' . $latitudeField . ')/2) + | |
SIN(RADIANS(' . $longitude . ' - ' . $longitudeField . ')/2) * SIN(RADIANS(' . $longitude . ' - ' . $longitudeField . ')/2) * | |
COS(RADIANS(' . $latitude . ')) * COS(RADIANS(' . $longitude . ')) | |
), | |
SQRT(1 - ( | |
SIN(RADIANS(' . $latitude . ' - ' . $latitudeField . ')/2) * SIN(RADIANS(' . $latitude . ' - ' . $latitudeField . ')/2) + | |
SIN(RADIANS(' . $longitude . ' - ' . $longitudeField . ')/2) * SIN(RADIANS(' . $longitude . ' - ' . $longitudeField . ')/2) * | |
COS(RADIANS(' . $latitude . ')) * COS(RADIANS(' . $longitude . ')) | |
)) | |
) * ' . $this->units[strtolower($unit)] . ')'; | |
$expression = str_replace("\n", ' ', $expression); | |
$query = array( | |
'order' => $expression . ' ' . $direction, | |
'conditions' => array( | |
'OR' => array( | |
array( | |
'ROUND(' . $latitudeField . ', 4) !=' => round($latitude, 4), | |
'ROUND(' . $longitudeField . ', 4) !=' => round($longitude, 4) | |
), | |
array($latitudeField => $latitude, $longitudeField => $longitude) | |
) | |
) | |
); | |
if (!empty($distance)) { | |
$query['conditions']['OR'] = array($expression . ' <= ' . $distance, array($latitudeField => $latitude, $longitudeField => $longitude)); | |
} | |
return $query; | |
} | |
} | |
class DynamicModel extends AppModel { | |
function __construct($options = array()) { | |
if (is_string($options)) { | |
$options = array('name' => $options); | |
} | |
if (!isset($options['name'])) { | |
return null; | |
} | |
$options = am(array( | |
'id' => false, | |
'table' => null, | |
'ds' => null | |
), $options); | |
$this->name = $options['name']; | |
parent::__construct($options['id'], $options['table'], $options['ds']); | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment