Skip to content

Instantly share code, notes, and snippets.

@Gerifield
Created August 20, 2014 08:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gerifield/2d35c911dced6b893ed5 to your computer and use it in GitHub Desktop.
Save Gerifield/2d35c911dced6b893ed5 to your computer and use it in GitHub Desktop.
Haversine distance formula calculator for PHP, with some little hacks and a lot of parameters.
/**
* Haversine distance formula.
*
* This calculates the distance between geo coordinates in km<br>
* Algorithm: http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL<br>
* Full calculation with Haversine formula is slow, so limit the whole distance to a rectangle,
* then calculate the distance.
* <br>
* 1° of lat -> 111km (69 miles)<br>
* 1° of lon -> cos(lat)*111<br>
* <br>
* The "countit" version is a dirty hack, but it works....<br><br>
* <br>
* IMPORTANT: Be very careful with the parameters and NEVER use unfiltered user input!
* <br><br>
* @param exp Expression, what you want to give back (SELECT <exp>, distance FROM...)
* @param table Database for the calculation (user, question, etc..)
* @param lat Latitude
* @param lon Longtitude
* @param radius Radius in meter
* @param limit SQL limit (DEFAULT: 1000)
* @param limitMax SQL limit maximum
* @param wheres Possible more WHERE conditions
* @param latname Name of the lat fieald
* @param lonname Name of the lon fieald
* @param areaname Name of the area fieald
* @param countit Only count the result or not
* @param countit_distinct Limitations for the count
* @param groupby Group by param
*
* @return An array with the 'exp' or all the values and a 'distance' key value
* <br>
* <br>
* Example: haversineCalculator(array('table' => 'user', 'lat' => 19.01213, 'lon' => 21.00232, 'radius' => 400));<br>
* This'll give back the results from the 'user' table (where you need to have a lat, lon and area column).
*/
function haversineCalculator($params = array()){
$defaults = array(
'exp' => '*',
'table' => '',
'lat' => 0,
'lon' => 0,
'radius' => 5000, //you should put this in a config file for example
'limit' => 1000,
'limitMax' => '',
'order' => 'distance',
'wheres' => '',
'latname' => 'lat',
'lonname' => 'lon',
'areaname' => 'area',
'countit' => false,
'countit_distinct' => '*',
'groupby' => ''
);
$params = array_merge($defaults, $params);
//Not too beautiful, but I don't want to touch the code below
$exp = $params['exp'];
$db = $params['table'];
$lat = $params['lat'];
$lon = $params['lon'];
$dist = ($params['radius'] + 5000) / 1000; //km -> m conversion, the database calculates with km + the '5000' value should come from the max distance config file or somewhere
//get a bigger area for circle engraving
$latname = $params['latname'];
$lonname = $params['lonname'];
$radiusname = $params['areaname'];
$wheres = $params['wheres'];
//calcluate a rectangular area first
$lon1 = $lon - ($dist / abs( cos(deg2rad($lat))*111.0447 ));
$lon2 = $lon + ($dist / abs( cos(deg2rad($lat))*111.0447 ));
$lat1 = $lat - ($dist / 111.0447);
$lat2 = $lat + ($dist / 111.0447);
//echo "DST: ".$dist."km, " .$lon1." ".$lon2." - ".$lat1." ".$lat2."<br />";
if($params['countit']){
//changed the count stuff
$res = R::getAll('SELECT count('.$params['countit_distinct'].') as number from (SELECT '.$exp.',
( 6371 * 2 *
ASIN(SQRT( POWER(SIN(( :lat - ('.$db.'.'.$latname.')) * pi()/180 / 2), 2)
+COS( :lat *pi()/180) * COS( ('.$db.'.'.$latname.')*pi()/180)*
POWER(SIN( ( :lon - '.$db.'.'.$lonname.') * pi()/180 / 2 ),2 )
)) ) AS distance
FROM '.$db.' WHERE '.$lonname.' BETWEEN '.$lon1.' and '.$lon2.' AND '.$latname.' BETWEEN '.$lat1.' and '.$lat2.' '.$wheres.'
'. ( !empty($params['groupby'])? 'GROUP BY '.$params['groupby']:'' ) .'
HAVING distance < ( :area + ('.$db.'.'.$radiusname.'/1000) ) ORDER BY '.$params['order'].' LIMIT :limit '.$params['limitMax'].') as t;',
array(':lat' => $lat,
':lon' => $lon,
':area' => ($params['radius']/1000),
/*':order' => $params['order'],*/
':limit' => $params['limit'],
));
}else{
$res = R::getAll('SELECT '.$exp.',
( 6371 * 2 *
ASIN(SQRT( POWER(SIN(( :lat - ('.$db.'.'.$latname.')) * pi()/180 / 2), 2)
+COS( :lat *pi()/180) * COS( ('.$db.'.'.$latname.')*pi()/180)*
POWER(SIN( ( :lon - '.$db.'.'.$lonname.') * pi()/180 / 2 ),2 )
)) ) AS distance
FROM '.$db.' WHERE '.$lonname.' BETWEEN '.$lon1.' and '.$lon2.' AND '.$latname.' BETWEEN '.$lat1.' and '.$lat2.' '.$wheres.'
'. ( !empty($params['groupby'])? 'GROUP BY '.$params['groupby']:'' ) .'
HAVING distance < ( :area + ('.$db.'.'.$radiusname.'/1000) ) ORDER BY '.$params['order'].' LIMIT :limit '.$params['limitMax'],
array(':lat' => $lat,
':lon' => $lon,
':area' => ($params['radius']/1000),
/*':order' => $params['order'],*/
':limit' => $params['limit'],
));
}
//Maybe try this
//https://developers.google.com/maps/articles/phpsqlsearch_v3
return $res;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment