3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2016
33 class CRM_Contact_BAO_ProximityQuery
{
36 * Trigonometry for calculating geographical distances.
38 * Modification made in: CRM-13904
39 * http://en.wikipedia.org/wiki/Great-circle_distance
40 * http://www.movable-type.co.uk/scripts/latlong.html
42 * All function arguments and return values measure distances in metres
43 * and angles in degrees. The ellipsoid model is from the WGS-84 datum.
44 * Ka-Ping Yee, 2003-08-11
45 * earth_radius_semimajor = 6378137.0;
46 * earth_flattening = 1/298.257223563;
47 * earth_radius_semiminor = $earth_radius_semimajor * (1 - $earth_flattening);
48 * earth_eccentricity_sq = 2*$earth_flattening - pow($earth_flattening, 2);
49 * This library is an implementation of UCB CS graduate student, Ka-Ping Yee (http://www.zesty.ca).
50 * This version has been taken from Drupal's location module: http://drupal.org/project/location
53 static protected $_earthFlattening;
54 static protected $_earthRadiusSemiMinor;
55 static protected $_earthRadiusSemiMajor;
56 static protected $_earthEccentricitySQ;
58 public static function initialize() {
59 static $_initialized = FALSE;
64 self
::$_earthFlattening = 1.0 / 298.257223563;
65 self
::$_earthRadiusSemiMajor = 6378137.0;
66 self
::$_earthRadiusSemiMinor = self
::$_earthRadiusSemiMajor * (1.0 - self
::$_earthFlattening);
67 self
::$_earthEccentricitySQ = 2 * self
::$_earthFlattening - pow(self
::$_earthFlattening, 2);
72 * Latitudes in all of U. S.: from -7.2 (American Samoa) to 70.5 (Alaska).
73 * Latitudes in continental U. S.: from 24.6 (Florida) to 49.0 (Washington).
74 * Average latitude of all U. S. zipcodes: 37.9.
78 * Estimate the Earth's radius at a given latitude.
79 * Default to an approximate average radius for the United States.
81 * @param float $latitude
84 public static function earthRadius($latitude) {
85 $lat = deg2rad($latitude);
87 $x = cos($lat) / self
::$_earthRadiusSemiMajor;
88 $y = sin($lat) / self
::$_earthRadiusSemiMinor;
89 return 1.0 / sqrt($x * $x +
$y * $y);
93 * Convert longitude and latitude to earth-centered earth-fixed coordinates.
94 * X axis is 0 long, 0 lat; Y axis is 90 deg E; Z axis is north pole.
96 * @param float $longitude
97 * @param float $latitude
98 * @param float|int $height
102 public static function earthXYZ($longitude, $latitude, $height = 0) {
103 $long = deg2rad($longitude);
104 $lat = deg2rad($latitude);
106 $cosLong = cos($long);
108 $sinLong = sin($long);
111 $radius = self
::$_earthRadiusSemiMajor / sqrt(1 - self
::$_earthEccentricitySQ * $sinLat * $sinLat);
113 $x = ($radius +
$height) * $cosLat * $cosLong;
114 $y = ($radius +
$height) * $cosLat * $sinLong;
115 $z = ($radius * (1 - self
::$_earthEccentricitySQ) +
$height) * $sinLat;
117 return array($x, $y, $z);
121 * Convert a given angle to earth-surface distance.
123 * @param float $angle
124 * @param float $latitude
127 public static function earthArcLength($angle, $latitude) {
128 return deg2rad($angle) * self
::earthRadius($latitude);
132 * Estimate the min and max longitudes within $distance of a given location.
134 * @param float $longitude
135 * @param float $latitude
136 * @param float $distance
139 public static function earthLongitudeRange($longitude, $latitude, $distance) {
140 $long = deg2rad($longitude);
141 $lat = deg2rad($latitude);
142 $radius = self
::earthRadius($latitude);
144 $angle = $distance / $radius;
145 $diff = asin(sin($angle) / cos($lat));
146 $minLong = $long - $diff;
147 $maxLong = $long +
$diff;
149 if ($minLong < -pi()) {
150 $minLong = $minLong +
pi() * 2;
153 if ($maxLong > pi()) {
154 $maxLong = $maxLong - pi() * 2;
164 * Estimate the min and max latitudes within $distance of a given location.
166 * @param float $longitude
167 * @param float $latitude
168 * @param float $distance
171 public static function earthLatitudeRange($longitude, $latitude, $distance) {
172 $long = deg2rad($longitude);
173 $lat = deg2rad($latitude);
174 $radius = self
::earthRadius($latitude);
176 $angle = $distance / $radius;
177 $minLat = $lat - $angle;
178 $maxLat = $lat +
$angle;
179 $rightangle = pi() / 2.0;
181 // wrapped around the south pole
182 if ($minLat < -$rightangle) {
183 $overshoot = -$minLat - $rightangle;
184 $minLat = -$rightangle +
$overshoot;
185 if ($minLat > $maxLat) {
188 $minLat = -$rightangle;
191 // wrapped around the north pole
192 if ($maxLat > $rightangle) {
193 $overshoot = $maxLat - $rightangle;
194 $maxLat = $rightangle - $overshoot;
195 if ($maxLat < $minLat) {
198 $maxLat = $rightangle;
208 * @param float $latitude
209 * @param float $longitude
210 * @param float $distance
211 * @param string $tablePrefix
215 public static function where($latitude, $longitude, $distance, $tablePrefix = 'civicrm_address') {
221 list($minLongitude, $maxLongitude) = self
::earthLongitudeRange($longitude, $latitude, $distance);
222 list($minLatitude, $maxLatitude) = self
::earthLatitudeRange($longitude, $latitude, $distance);
224 // DONT consider NAN values (which is returned by rad2deg php function)
225 // for checking BETWEEN geo_code's criteria as it throws obvious 'NAN' field not found DB: Error
226 $geoCodeWhere = array();
227 if (!is_nan($minLatitude)) {
228 $geoCodeWhere[] = "{$tablePrefix}.geo_code_1 >= $minLatitude ";
230 if (!is_nan($maxLatitude)) {
231 $geoCodeWhere[] = "{$tablePrefix}.geo_code_1 <= $maxLatitude ";
233 if (!is_nan($minLongitude)) {
234 $geoCodeWhere[] = "{$tablePrefix}.geo_code_2 >= $minLongitude ";
236 if (!is_nan($maxLongitude)) {
237 $geoCodeWhere[] = "{$tablePrefix}.geo_code_2 <= $maxLongitude ";
239 $geoCodeWhereClause = implode(' AND ', $geoCodeWhere);
242 {$geoCodeWhereClause} AND
244 COS(RADIANS({$tablePrefix}.geo_code_1)) *
245 COS(RADIANS($latitude)) *
246 COS(RADIANS({$tablePrefix}.geo_code_2) - RADIANS($longitude)) +
247 SIN(RADIANS({$tablePrefix}.geo_code_1)) *
248 SIN(RADIANS($latitude))
249 ) * 6378137 <= $distance
257 * @param CRM_Contact_BAO_Query $query
258 * @param array $values
263 public static function process(&$query, &$values) {
264 list($name, $op, $distance, $grouping, $wildcard) = $values;
266 // also get values array for all address related info
267 $proximityVars = array(
268 'street_address' => 1,
271 'state_province_id' => 0,
273 'state_province' => 0,
275 'distance_unit' => 0,
278 $proximityAddress = array();
280 foreach ($proximityVars as $var => $recordQill) {
281 $proximityValues = $query->getWhereValues("prox_{$var}", $grouping);
282 if (!empty($proximityValues) &&
283 !empty($proximityValues[2])
285 $proximityAddress[$var] = $proximityValues[2];
287 $qill[] = $proximityValues[2];
292 if (empty($proximityAddress)) {
296 if (isset($proximityAddress['state_province_id'])) {
297 $proximityAddress['state_province'] = CRM_Core_PseudoConstant
::stateProvince($proximityAddress['state_province_id']);
298 $qill[] = $proximityAddress['state_province'];
301 $config = CRM_Core_Config
::singleton();
302 if (!isset($proximityAddress['country_id'])) {
303 // get it from state if state is present
304 if (isset($proximityAddress['state_province_id'])) {
305 $proximityAddress['country_id'] = CRM_Core_PseudoConstant
::countryIDForStateID($proximityAddress['state_province_id']);
307 elseif (isset($config->defaultContactCountry
)) {
308 $proximityAddress['country_id'] = $config->defaultContactCountry
;
312 if (!empty($proximityAddress['country_id'])) {
313 $proximityAddress['country'] = CRM_Core_PseudoConstant
::country($proximityAddress['country_id']);
314 $qill[] = $proximityAddress['country'];
318 isset($proximityAddress['distance_unit']) &&
319 $proximityAddress['distance_unit'] == 'miles'
321 $qillUnits = " {$distance} " . ts('miles');
322 $distance = $distance * 1609.344;
325 $qillUnits = " {$distance} " . ts('km');
326 $distance = $distance * 1000.00;
329 $qill = ts('Proximity search to a distance of %1 from %2',
332 2 => implode(', ', $qill),
336 $fnName = isset($config->geocodeMethod
) ?
$config->geocodeMethod
: NULL;
337 if (empty($fnName)) {
338 CRM_Core_Error
::fatal(ts('Proximity searching requires you to set a valid geocoding provider'));
341 $query->_tables
['civicrm_address'] = $query->_whereTables
['civicrm_address'] = 1;
343 require_once str_replace('_', DIRECTORY_SEPARATOR
, $fnName) . '.php';
344 $fnName::format($proximityAddress);
346 !is_numeric(CRM_Utils_Array
::value('geo_code_1', $proximityAddress)) ||
347 !is_numeric(CRM_Utils_Array
::value('geo_code_2', $proximityAddress))
349 // we are setting the where clause to 0 here, so we wont return anything
350 $qill .= ': ' . ts('We could not geocode the destination address.');
351 $query->_qill
[$grouping][] = $qill;
352 $query->_where
[$grouping][] = ' (0) ';
356 $query->_qill
[$grouping][] = $qill;
357 $query->_where
[$grouping][] = self
::where(
358 $proximityAddress['geo_code_1'],
359 $proximityAddress['geo_code_2'],
367 * @param array $input
372 public static function fixInputParams(&$input) {
373 foreach ($input as $param) {
374 if (CRM_Utils_Array
::value('0', $param) == 'prox_distance') {
375 // add prox_ prefix to these
376 $param_alter = array('street_address', 'city', 'postal_code', 'state_province', 'country');
378 foreach ($input as $key => $_param) {
379 if (in_array($_param[0], $param_alter)) {
380 $input[$key][0] = 'prox_' . $_param[0];
382 // _id suffix where needed
383 if ($_param[0] == 'country' ||
$_param[0] == 'state_province') {
384 $input[$key][0] .= '_id';
386 // flatten state_province array
387 if (is_array($input[$key][2])) {
388 $input[$key][2] = $input[$key][2][0];