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