Skip to content

Instantly share code, notes, and snippets.

@ryantology
Created December 14, 2010 19:49
Show Gist options
  • Save ryantology/740967 to your computer and use it in GitHub Desktop.
Save ryantology/740967 to your computer and use it in GitHub Desktop.
<?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