I'm working on a geography class to help locate nearby businesses and calculate distances. To assist with generating SQL queries to locate businesses in a database, I've written functions get_bounding_box_rads
and get_bounding_box_degrees
. The idea is that you supply the lat and lon of a point and supply unitless values (you could specify miles or kilometers or furlongs, etc) to specify the radius of the sphere and the range and this function will return a bounding box of minimum and maximum latitudes and longitudes so that you can write an efficient query to retrieve records from your db. Efficient in the sense that the query has simple greater than / less than comparisons so that indexes can be used rather than some insane great circle distance calculation that results in a nasty table scan.
My problem is that the bounding box returns different longitudes (curiously, the latitudes seem identical so far) for the calculation performed in degrees vs the calculation performed in radians. I think this difference arises in the circle distance calculation which I borrowed from this wikipedia article, which says:
Can anyone tell me the cause of the difference and how I might avoid it? If not, which is more accurate, degrees or radians? It's only about a 1 in 1000 difference, but that seems non trivial to me. I wonder if this is due to a 32-bit or 64-bit rounding error, or if perhaps I've made a mistake somewhere.
You should be able to simply run the following script and see what I mean
<?php
/**
* A class to encapsulate and standardize geography-related functions.
*
* @author sneakyimp
*/
class OGH_geography {
/**
* Radius of the earth in miles
* @var float
*/
const EARTH_RADIUS_MI = 3958.8;
/**
* outer limit of latitude in radians
* @var float
*/
const LAT_LIMIT_RAD = M_PI/2;
/**
* outer limit of latitude in degrees
* @var float
*/
const LAT_LIMIT_DEGREE = 90;
/**
* outer limit of longitude in radians
* @var float
*/
const LON_LIMIT_RAD = M_PI;
/**
* outer limit of longitude in degrees
* @var float
*/
const LON_LIMIT_DEGREE = 180;
/**
* Calculates great circle distance between two points on a sphere
* @see https://en.wikipedia.org/wiki/Great-circle_distance
* @param float $lat1 Point 1 latitude in rads
* @param float $lon1 Point 1 longitude in rads
* @param float $lat2 Point 2 latitude in rads
* @param float $lon2 Point 2 longitude in rads
* @param float $radius The unitless radius of the sphere (could be km or miles)
* @return float unitless distance (depends on units in which radius is specified)
*/
public static function great_circle_distance_rads($lat1, $lon1, $lat2, $lon2, $radius) : float {
// broadly, distance = radius * central_angle
// using the 'special case of the vincenty formula we get
// central_angle = arctan(sqrt((cos(lat2) * sin(lon1)) ** 2 + ((cos(lat1) * sin(lat2)) - (sin(lat1) * cos(lat2) * cos(abs(lon2 - lon1)))) ** 2) / (sin(lat1) * sin(lat2)) + (cos(lat1) * cos(lat2) * cos(abs(lon2 - lon1))))
$coslat1 = cos($lat1);
$coslat2 = cos($lat2);
$sinlat1 = sin($lat1);
$sinlat2 = sin($lat2);
$delta_lon = abs($lon2 - $lon1);
$sin_delta_lon = sin($delta_lon);
$cos_delta_lon = cos($delta_lon);
$is1 = ($coslat2 * $sin_delta_lon) ** 2;
$is2 = (($coslat1 * $sinlat2) - ($sinlat1 * $coslat2 * $cos_delta_lon)) ** 2;
$sq_root = sqrt($is1 + $is2);
$denom = ($sinlat1 * $sinlat2) + ($coslat1 * $coslat2 * $cos_delta_lon);
//$central_angle_radians = atan($sq_root / $denom);
$central_angle_radians = atan2($sq_root, $denom);
return $central_angle_radians * $radius;
}
/**
* Returns distance units per degree or latitude
* latitude, unlike longitude has constant distance per unit, so doesn't depend on location
* @param float $radius The unitless radius of the sphere
* @return float distance/length units per degree
*/
public static function distance_per_latitude_degree($radius) :float {
// more subtle approach, use great circle distance calculation for points
// of identical longitude, 1 deg of latitude apart
$rads_per_deg = deg2rad(1);
return self::great_circle_distance_rads(0, 0, $rads_per_deg, 0, $radius);
}
/**
* Returns distance units per radian of latitude
* latitude, unlike longitude has constant distance per unit, so doesn't depend on location
* @param float $radius The unitless radius of the sphere
* @return float distance/length units per radian
*/
public static function distance_per_latitude_rad($radius) :float {
// more subtle approach, use great circle distance calculation for points
// of identical longitude, 1 rad of latitude apart
return self::great_circle_distance_rads(0, 0, 1, 0, $radius);
}
/**
* Returns distance units per radian of longitude
* distance longitudinally varies with the latitude, so we need additional param of $lat
* @param float $lat latitude, in radians, at which we want the longitudinal distance
* @param float $radius The unitless radius of the sphere
* @return float distance/length units per radian
*/
public static function distance_per_longitude_rad($lat, $radius) :float {
// use great circle distance calculation for points
// of identical latitude, 1 rad of longitude apart
return self::great_circle_distance_rads($lat, 0, $lat, 1, $radius);
}
/**
* Returns distance units per degree of longitude
* distance longitudinally varies with the latitude, so we need additional param of $lat
* @param float $lat latitude, in degrees, at which we want the longitudinal distance
* @param float $radius The unitless radius of the sphere
* @return float distance/length units per degree
*/
public static function distance_per_longitude_degree($lat, $radius) :float {
// use great circle distance calculation for points
// of identical latitude, 1 degree of longitude apart
$rads_per_deg = deg2rad(1);
$lat_rads = deg2rad($lat);
return self::great_circle_distance_rads($lat_rads, 0, $lat_rads, $rads_per_deg, $radius);
}
/**
* Given a point on the globe and a unitless range, will return upper and lower lat/lon in radians for
* a roughly square bounding box, useful for constructing a query to find nearby points
* @param float $radius unitless radius of globe
* @param float $lat latitude of center in rads
* @param float $lon longitude of center in rads
* @param float $range unitless range distance from center
* @return array associative array containing lat/lon floats in radians with keys min_lat, max_lat, min_lon, max_lon
*/
public static function get_bounding_box_rads($lat, $lon, $radius, $range) {
if (!is_numeric($lat) || $lat > self::LAT_LIMIT_RAD || $lat < -self::LAT_LIMIT_RAD) {
throw new \Exception("$lat is not a valid latitude");
}
if (!is_numeric($lon) || $lon > self::LON_LIMIT_RAD || $lon < -self::LON_LIMIT_RAD) {
throw new \Exception("$lon is not a valid longitude");
}
if (!is_numeric($radius)) {
throw new \Exception("$radius is not a valid radius");
}
if (!is_numeric($range)) {
throw new \Exception("$radius is not a valid range");
}
// sanity check...this prob shouldn't be used for more than 1/20 of the globe
$max_range = $radius/20;
if ($range < 0 || $range > $max_range) {
throw new \Exception("$range is out of bounds. Must be between 0 and $max_range");
}
$lat_dist_per_rad = self::distance_per_latitude_rad($radius);
$lon_dist_per_rad = self::distance_per_longitude_rad($lat, $radius);
$lat_adjust = $range/$lat_dist_per_rad;
$lon_adjust = $range/$lon_dist_per_rad;
$min_lat = $lat - $lat_adjust;
// watch out for min_lat out of bounds
$min_lat = ($min_lat < -self::LAT_LIMIT_RAD) ? -self::LAT_LIMIT_RAD : $min_lat;
$max_lat = $lat + $lat_adjust;
// watch out for max_lat out of bounds
$max_lat = ($max_lat > self::LAT_LIMIT_RAD) ? self::LAT_LIMIT_RAD : $max_lat;
$min_lon = $lon - $lon_adjust;
// watch out for min_lon out of bounds
$min_lon = ($min_lon <= -self::LON_LIMIT_RAD) ? ($min_lon + (2 * M_PI)) : $min_lon;
$max_lon = $lon + $lon_adjust;
// watch out for max_lon out of bounds
$max_lon = ($max_lon > self::LON_LIMIT_RAD) ? ($max_lon - (2 * M_PI)) : $max_lon;
return [
'min_lat' => $min_lat,
'max_lat' => $max_lat,
'min_lon' => $min_lon,
'max_lon' => $max_lon
];
}
/**
* Given a point on the globe and a unitless range, will return upper and lower lat/lon in degrees for
* a roughly square bounding box, useful for constructing a query to find nearby points
* @param float $radius unitless radius of globe
* @param float $lat latitude of center in degrees
* @param float $lon longitude of center in degrees
* @param float $range unitless range distance from center
* @return array associative array containing lat/lon floats in degrees with keys min_lat, max_lat, min_lon, max_lon
*/
public static function get_bounding_box_degrees($lat, $lon, $radius, $range) {
if (!is_numeric($lat) || $lat > self::LAT_LIMIT_DEGREE || $lat < -self::LAT_LIMIT_DEGREE) {
throw new \Exception("$lat is not a valid latitude");
}
if (!is_numeric($lon) || $lon > self::LON_LIMIT_DEGREE || $lon < -self::LON_LIMIT_DEGREE) {
throw new \Exception("$lon is not a valid longitude");
}
if (!is_numeric($radius)) {
throw new \Exception("$radius is not a valid radius");
}
if (!is_numeric($range)) {
throw new \Exception("$radius is not a valid range");
}
// sanity check...this prob shouldn't be used for more than 1/20 of the globe
$max_range = $radius/20;
if ($range < 0 || $range > $max_range) {
throw new \Exception("$range is out of bounds. Must be between 0 and $max_range");
}
$lat_dist_per_degree = self::distance_per_latitude_degree($radius);
$lon_dist_per_degree = self::distance_per_longitude_degree($lat, $radius);
$lat_adjust = $range/$lat_dist_per_degree;
$lon_adjust = $range/$lon_dist_per_degree;
$min_lat = $lat - $lat_adjust;
// watch out for min_lat out of bounds
$min_lat = ($min_lat < -self::LAT_LIMIT_DEGREE) ? -self::LAT_LIMIT_DEGREE : $min_lat;
$max_lat = $lat + $lat_adjust;
// watch out for max_lat out of bounds
$max_lat = ($max_lat > self::LAT_LIMIT_DEGREE) ? self::LAT_LIMIT_DEGREE : $max_lat;
$min_lon = $lon - $lon_adjust;
// watch out for min_lon out of bounds
$min_lon = ($min_lon <= -self::LON_LIMIT_DEGREE) ? ($min_lon + 360) : $min_lon;
$max_lon = $lon + $lon_adjust;
// watch out for max_lon out of bounds
$max_lon = ($max_lon > self::LON_LIMIT_DEGREE) ? ($max_lon - 360) : $max_lon;
return [
'min_lat' => $min_lat,
'max_lat' => $max_lat,
'min_lon' => $min_lon,
'max_lon' => $max_lon
];
}
} // class OGH_geography
$lat = 34.078991;
$lon = -118.263804;
$lat_rad = deg2rad($lat);
$lon_rad = deg2rad($lon);
$box = OGH_geography::get_bounding_box_rads($lat_rad, $lon_rad, 3958.8, 5);
var_dump($box);
foreach($box as $key => $val) {
$val = rad2deg($val);
echo "$key $val\n";
}
$box = OGH_geography::get_bounding_box_degrees($lat, $lon, 3958.8, 5);
var_dump($box);
foreach($box as $key => $val) {
echo "$key $val\n";
}