Skip to content

Instantly share code, notes, and snippets.

@ReedD
Last active February 18, 2016 07:32
Show Gist options
  • Save ReedD/8566010 to your computer and use it in GitHub Desktop.
Save ReedD/8566010 to your computer and use it in GitHub Desktop.
A CakePHP model function to find and sort by the distance from a given a latitude and longitude coordinate with the option to restrict to a given radius.
<?php
App::uses('Model', 'Model');
App::uses('String', 'Utility');
class AppModel extends Model {
/**
* @author Reed Dadoune
* distanceQuery
* A genral case distance query builder
* Pass a number of options to this function and recieve a query
* you can pass to either the find or paginate functions to get
* objects back by distance
*
* Example:
* $query = $this->Model->distanceQuery(array(
* 'latitude' => 34.2746405,
* 'longitude' => -119.2290053
* ));
* $query['conditions']['published'] = true;
* $results = $this->Model->find('all', $query);
*
* @param array $opts Options
* - latitude The latitude coordinate of the center point
* - longitude The longitude coordinate of the center point
* - alias The model name of the query this is for
* defaults to the current model alias
* - radius The distance to at which to find objects at
* defaults to false in which case distance is calculated
* only for the sort order
* @return array A query that can be modified and passed to find or paginate
*/
public function distanceQuery($opts = array()) {
$defaults = array(
'latitude' => 0,
'longitude' => 0,
'alias' => $this->alias,
'radius' => false
);
$opts = Set::merge($defaults, $opts);
$query = array(
'fields' => array(
'*',
String::insert(
'3956 * 2 *
ASIN(SQRT(
POWER(SIN((:latitude - ABS(:alias.latitude)) * PI() / 180 / 2), 2) +
COS(:latitude * PI() / 180) *
COS(ABS(:alias.latitude) * PI() / 180) *
POWER(SIN((:longitude - :alias.longitude) * PI() / 180 / 2), 2)
)) AS distance',
array('alias' => $opts['alias'], 'latitude' => $opts['latitude'], 'longitude' => $opts['longitude'])
)
),
'order' => array('distance' => 'ASC')
);
if ($opts['radius']) {
$longitudeLower = $opts['longitude'] - $opts['radius'] / abs(cos(deg2rad($opts['latitude'])) * 69);
$longitudeUpper = $opts['longitude'] + $opts['radius'] / abs(cos(deg2rad($opts['latitude'])) * 69);
$latitudeLower = $opts['latitude'] - ($opts['radius'] / 69);
$latitudeUpper = $opts['latitude'] + ($opts['radius'] / 69);
$query['conditions'] = array(
String::insert(':alias.latitude BETWEEN ? AND ?', array('alias' => $opts['alias'])) => array($latitudeLower, $latitudeUpper),
String::insert(':alias.longitude BETWEEN ? AND ?', array('alias' => $opts['alias'])) => array($longitudeLower, $longitudeUpper)
);
$query['group'] = sprintf('%s.id HAVING distance < %f', $opts['alias'], $opts['radius']);
}
return $query;
}
}
@SwabTheDeck
Copy link

Hello,

I found this model to be very useful and I ended up using it in a project. It worked well the vast majority of the time, but I found that there were some strange results when calculating distances in certain parts of eastern Australia. When measuring between two points that were obviously < 50 mi. apart, the formula would return a distance of several thousand miles.

I did a bit of research and came across Google's version of the Haversine formula here: https://developers.google.com/maps/articles/phpsqlsearch_v3#findnearsql . After updating your Model with Google's version of the formula, these distance anomalies went away.

Since your file is a "gist", I can't submit a pull request, but I'd humbly ask that you consider integrating my fork here: https://gist.github.com/SwabTheDeck/0056d6785377d86deaa8

Let me know if you have any questions, and thanks for providing this useful model!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment