Merge branch 'master' of git://github.com/civicrm/civicrm-core into codingstandards-12
[civicrm-core.git] / CRM / Contact / BAO / ProximityQuery.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2014
32 * $Id$
33 *
34 */
35 class CRM_Contact_BAO_ProximityQuery {
36
37 /**
38 * Trigonometry for calculating geographical distances.
39 *
40 * Modification made in: CRM-13904
41 * http://en.wikipedia.org/wiki/Great-circle_distance
42 * http://www.movable-type.co.uk/scripts/latlong.html
43 *
44 * All function arguments and return values measure distances in metres
45 * and angles in degrees. The ellipsoid model is from the WGS-84 datum.
46 * Ka-Ping Yee, 2003-08-11
47 * earth_radius_semimajor = 6378137.0;
48 * earth_flattening = 1/298.257223563;
49 * earth_radius_semiminor = $earth_radius_semimajor * (1 - $earth_flattening);
50 * earth_eccentricity_sq = 2*$earth_flattening - pow($earth_flattening, 2);
51 * This library is an implementation of UCB CS graduate student, Ka-Ping Yee (http://www.zesty.ca).
52 * This version has been taken from Drupal's location module: http://drupal.org/project/location
53 */
54
55 static protected $_earthFlattening;
56 static protected $_earthRadiusSemiMinor;
57 static protected $_earthRadiusSemiMajor;
58 static protected $_earthEccentricitySQ;
59
60 public static function initialize() {
61 static $_initialized = FALSE;
62
63 if (!$_initialized) {
64 $_initialized = TRUE;
65
66 self::$_earthFlattening = 1.0 / 298.257223563;
67 self::$_earthRadiusSemiMajor = 6378137.0;
68 self::$_earthRadiusSemiMinor = self::$_earthRadiusSemiMajor * (1.0 - self::$_earthFlattening);
69 self::$_earthEccentricitySQ = 2 * self::$_earthFlattening - pow(self::$_earthFlattening, 2);
70 }
71 }
72
73 /*
74 * Latitudes in all of U. S.: from -7.2 (American Samoa) to 70.5 (Alaska).
75 * Latitudes in continental U. S.: from 24.6 (Florida) to 49.0 (Washington).
76 * Average latitude of all U. S. zipcodes: 37.9.
77 */
78
79 /**
80 * Estimate the Earth's radius at a given latitude.
81 * Default to an approximate average radius for the United States.
82 *
83 * @param float $latitude
84 * @return float
85 */
86 public static function earthRadius($latitude) {
87 $lat = deg2rad($latitude);
88
89 $x = cos($lat) / self::$_earthRadiusSemiMajor;
90 $y = sin($lat) / self::$_earthRadiusSemiMinor;
91 return 1.0 / sqrt($x * $x + $y * $y);
92 }
93
94 /**
95 * Convert longitude and latitude to earth-centered earth-fixed coordinates.
96 * X axis is 0 long, 0 lat; Y axis is 90 deg E; Z axis is north pole.
97 *
98 * @param float $longitude
99 * @param float $latitude
100 * @param float $height
101 * @return array
102 */
103 public static function earthXYZ($longitude, $latitude, $height = 0) {
104 $long = deg2rad($longitude);
105 $lat = deg2rad($latitude);
106
107 $cosLong = cos($long);
108 $cosLat = cos($lat);
109 $sinLong = sin($long);
110 $sinLat = sin($lat);
111
112 $radius = self::$_earthRadiusSemiMajor / sqrt(1 - self::$_earthEccentricitySQ * $sinLat * $sinLat);
113
114 $x = ($radius + $height) * $cosLat * $cosLong;
115 $y = ($radius + $height) * $cosLat * $sinLong;
116 $z = ($radius * (1 - self::$_earthEccentricitySQ) + $height) * $sinLat;
117
118 return array($x, $y, $z);
119 }
120
121 /**
122 * Convert a given angle to earth-surface distance.
123 *
124 * @param float $angle
125 * @param float $latitude
126 * @return float
127 */
128 public static function earthArcLength($angle, $latitude) {
129 return deg2rad($angle) * self::earthRadius($latitude);
130 }
131
132 /**
133 * Estimate the min and max longitudes within $distance of a given location.
134 *
135 * @param float $longitude
136 * @param float $latitude
137 * @param float $distance
138 * @return array
139 */
140 public static function earthLongitudeRange($longitude, $latitude, $distance) {
141 $long = deg2rad($longitude);
142 $lat = deg2rad($latitude);
143 $radius = self::earthRadius($latitude);
144
145 $angle = $distance / $radius;
146 $diff = asin(sin($angle) / cos($lat));
147 $minLong = $long - $diff;
148 $maxLong = $long + $diff;
149
150 if ($minLong < -pi()) {
151 $minLong = $minLong + pi() * 2;
152 }
153
154 if ($maxLong > pi()) {
155 $maxLong = $maxLong - pi() * 2;
156 }
157
158 return array(
159 rad2deg($minLong),
160 rad2deg($maxLong),
161 );
162 }
163
164 /**
165 * Estimate the min and max latitudes within $distance of a given location.
166 *
167 * @param float $longitude
168 * @param float $latitude
169 * @param float $distance
170 * @return array
171 */
172 public static function earthLatitudeRange($longitude, $latitude, $distance) {
173 $long = deg2rad($longitude);
174 $lat = deg2rad($latitude);
175 $radius = self::earthRadius($latitude);
176
177 $angle = $distance / $radius;
178 $minLat = $lat - $angle;
179 $maxLat = $lat + $angle;
180 $rightangle = pi() / 2.0;
181
182 // wrapped around the south pole
183 if ($minLat < -$rightangle) {
184 $overshoot = -$minLat - $rightangle;
185 $minLat = -$rightangle + $overshoot;
186 if ($minLat > $maxLat) {
187 $maxLat = $minLat;
188 }
189 $minLat = -$rightangle;
190 }
191
192 // wrapped around the north pole
193 if ($maxLat > $rightangle) {
194 $overshoot = $maxLat - $rightangle;
195 $maxLat = $rightangle - $overshoot;
196 if ($maxLat < $minLat) {
197 $minLat = $maxLat;
198 }
199 $maxLat = $rightangle;
200 }
201
202 return array(
203 rad2deg($minLat),
204 rad2deg($maxLat),
205 );
206 }
207
208 /**
209 * @param float $latitude
210 * @param float $longitude
211 * @param float $distance
212 * @param string $tablePrefix
213 *
214 * @return string
215 */
216 public static function where($latitude, $longitude, $distance, $tablePrefix = 'civicrm_address') {
217 self::initialize();
218
219 $params = array();
220 $clause = array();
221
222 list($minLongitude, $maxLongitude) = self::earthLongitudeRange($longitude, $latitude, $distance);
223 list($minLatitude, $maxLatitude) = self::earthLatitudeRange($longitude, $latitude, $distance);
224
225 // DONT consider NAN values (which is returned by rad2deg php function)
226 // for checking BETWEEN geo_code's criteria as it throws obvious 'NAN' field not found DB: Error
227 $geoCodeWhere = array();
228 if (!is_nan($minLatitude)) {
229 $geoCodeWhere[] = "{$tablePrefix}.geo_code_1 >= $minLatitude ";
230 }
231 if (!is_nan($maxLatitude)) {
232 $geoCodeWhere[] = "{$tablePrefix}.geo_code_1 <= $maxLatitude ";
233 }
234 if (!is_nan($minLongitude)) {
235 $geoCodeWhere[] = "{$tablePrefix}.geo_code_2 >= $minLongitude ";
236 }
237 if (!is_nan($maxLongitude)) {
238 $geoCodeWhere[] = "{$tablePrefix}.geo_code_2 <= $maxLongitude ";
239 }
240 $geoCodeWhereClause = implode(' AND ', $geoCodeWhere);
241
242 $where = "
243 {$geoCodeWhereClause} AND
244 ACOS(
245 COS(RADIANS({$tablePrefix}.geo_code_1)) *
246 COS(RADIANS($latitude)) *
247 COS(RADIANS({$tablePrefix}.geo_code_2) - RADIANS($longitude)) +
248 SIN(RADIANS({$tablePrefix}.geo_code_1)) *
249 SIN(RADIANS($latitude))
250 ) * 6378137 <= $distance
251 ";
252 return $where;
253 }
254
255 /**
256 * @param $query
257 * @param array $values
258 *
259 * @throws Exception
260 * @return void
261 */
262 public static function process(&$query, &$values) {
263 list($name, $op, $distance, $grouping, $wildcard) = $values;
264
265 // also get values array for all address related info
266 $proximityVars = array(
267 'street_address' => 1,
268 'city' => 1,
269 'postal_code' => 1,
270 'state_province_id' => 0,
271 'country_id' => 0,
272 'state_province' => 0,
273 'country' => 0,
274 'distance_unit' => 0,
275 );
276
277 $proximityAddress = array();
278 $qill = array();
279 foreach ($proximityVars as $var => $recordQill) {
280 $proximityValues = $query->getWhereValues("prox_{$var}", $grouping);
281 if (!empty($proximityValues) &&
282 !empty($proximityValues[2])
283 ) {
284 $proximityAddress[$var] = $proximityValues[2];
285 if ($recordQill) {
286 $qill[] = $proximityValues[2];
287 }
288 }
289 }
290
291 if (empty($proximityAddress)) {
292 return NULL;
293 }
294
295 if (isset($proximityAddress['state_province_id'])) {
296 $proximityAddress['state_province'] = CRM_Core_PseudoConstant::stateProvince($proximityAddress['state_province_id']);
297 $qill[] = $proximityAddress['state_province'];
298 }
299
300 $config = CRM_Core_Config::singleton();
301 if (!isset($proximityAddress['country_id'])) {
302 // get it from state if state is present
303 if (isset($proximityAddress['state_province_id'])) {
304 $proximityAddress['country_id'] = CRM_Core_PseudoConstant::countryIDForStateID($proximityAddress['state_province_id']);
305 }
306 elseif (isset($config->defaultContactCountry)) {
307 $proximityAddress['country_id'] = $config->defaultContactCountry;
308 }
309 }
310
311 if (isset($proximityAddress['country_id'])) {
312 $proximityAddress['country'] = CRM_Core_PseudoConstant::country($proximityAddress['country_id']);
313 $qill[] = $proximityAddress['country'];
314 }
315
316 if (
317 isset($proximityAddress['distance_unit']) &&
318 $proximityAddress['distance_unit'] == 'miles'
319 ) {
320 $qillUnits = " {$distance} " . ts('miles');
321 $distance = $distance * 1609.344;
322 }
323 else {
324 $qillUnits = " {$distance} " . ts('km');
325 $distance = $distance * 1000.00;
326 }
327
328 $qill = ts('Proximity search to a distance of %1 from %2',
329 array(
330 1 => $qillUnits,
331 2 => implode(', ', $qill),
332 )
333 );
334
335 $fnName = isset($config->geocodeMethod) ? $config->geocodeMethod : NULL;
336 if (empty($fnName)) {
337 CRM_Core_Error::fatal(ts('Proximity searching requires you to set a valid geocoding provider'));
338 }
339
340 $query->_tables['civicrm_address'] = $query->_whereTables['civicrm_address'] = 1;
341
342 require_once str_replace('_', DIRECTORY_SEPARATOR, $fnName) . '.php';
343 $fnName::format($proximityAddress);
344 if (
345 !is_numeric(CRM_Utils_Array::value('geo_code_1', $proximityAddress)) ||
346 !is_numeric(CRM_Utils_Array::value('geo_code_2', $proximityAddress))
347 ) {
348 // we are setting the where clause to 0 here, so we wont return anything
349 $qill .= ': ' . ts('We could not geocode the destination address.');
350 $query->_qill[$grouping][] = $qill;
351 $query->_where[$grouping][] = ' (0) ';
352 return NULL;
353 }
354
355 $query->_qill[$grouping][] = $qill;
356 $query->_where[$grouping][] = self::where(
357 $proximityAddress['geo_code_1'],
358 $proximityAddress['geo_code_2'],
359 $distance
360 );
361
362 return NULL;
363 }
364
365 /**
366 * @param array $input
367 * retun void
368 */
369 public static function fixInputParams(&$input) {
370 foreach ($input as $param) {
371 if (CRM_Utils_Array::value('0', $param) == 'prox_distance') {
372 // add prox_ prefix to these
373 $param_alter = array('street_address', 'city', 'postal_code', 'state_province', 'country');
374
375 foreach ($input as $key => $_param) {
376 if (in_array($_param[0], $param_alter)) {
377 $input[$key][0] = 'prox_' . $_param[0];
378
379 // _id suffix where needed
380 if ($_param[0] == 'country' || $_param[0] == 'state_province') {
381 $input[$key][0] .= '_id';
382
383 // flatten state_province array
384 if (is_array($input[$key][2])) {
385 $input[$key][2] = $input[$key][2][0];
386 }
387 }
388 }
389 }
390 return NULL;
391 }
392 }
393 }
394 }