Merge pull request #2206 from pratik-joshi/CRM-13907
[civicrm-core.git] / CRM / Contact / BAO / ProximityQuery.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
232624b1 4 | CiviCRM version 4.4 |
6a488035
TO
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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-2013
32 * $Id$
33 *
34 */
35class CRM_Contact_BAO_ProximityQuery {
36
37 /**
38 * Trigonometry for calculating geographical distances.
6c57bee6
DL
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 *
6a488035
TO
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
48 * earth_radius_semimajor = 6378137.0;
49 * earth_flattening = 1/298.257223563;
50 * earth_radius_semiminor = $earth_radius_semimajor * (1 - $earth_flattening);
51 * earth_eccentricity_sq = 2*$earth_flattening - pow($earth_flattening, 2);
52
53 * This library is an implementation of UCB CS graduate student, Ka-Ping Yee (http://www.zesty.ca).
54 * This version has been taken from Drupal's location module: http://drupal.org/project/location
55 **/
56
57 static protected $_earthFlattening;
58 static protected $_earthRadiusSemiMinor;
59 static protected $_earthRadiusSemiMajor;
60 static protected $_earthEccentricitySQ;
61
62 static function initialize() {
63 static $_initialized = FALSE;
64
65 if (!$_initialized) {
66 $_initialized = TRUE;
67
68 self::$_earthFlattening = 1.0 / 298.257223563;
69 self::$_earthRadiusSemiMajor = 6378137.0;
70 self::$_earthRadiusSemiMinor = self::$_earthRadiusSemiMajor * (1.0 - self::$_earthFlattening);
71 self::$_earthEccentricitySQ = 2 * self::$_earthFlattening - pow(self::$_earthFlattening, 2);
72 }
73 }
74
75 /**
76 * Latitudes in all of U. S.: from -7.2 (American Samoa) to 70.5 (Alaska).
77 * Latitudes in continental U. S.: from 24.6 (Florida) to 49.0 (Washington).
78 * Average latitude of all U. S. zipcodes: 37.9.
79 */
80
81 /*
82 /**
83 * Estimate the Earth's radius at a given latitude.
84 * Default to an approximate average radius for the United States.
85 */
86
87 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 static function earthXYZ($longitude, $latitude, $height = 0) {
100 $long = deg2rad($longitude);
101 $lat = deg2rad($latitude);
102
103 $cosLong = cos($long);
104 $cosLat = cos($lat);
105 $sinLong = sin($long);
106 $sinLat = sin($lat);
107
108 $radius = self::$_earthRadiusSemiMajor / sqrt(1 - self::$_earthEccentricitySQ * $sinLat * $sinLat);
109
110 $x = ($radius + $height) * $cosLat * $cosLong;
111 $y = ($radius + $height) * $cosLat * $sinLong;
112 $z = ($radius * (1 - self::$_earthEccentricitySQ) + $height) * $sinLat;
113
114 return array($x, $y, $z);
115 }
116
117 /**
118 * Convert a given angle to earth-surface distance.
119 */
120 static function earthArcLength($angle, $latitude) {
121 return deg2rad($angle) * self::earthRadius($latitude);
122 }
123
6a488035
TO
124 /**
125 * Estimate the min and max longitudes within $distance of a given location.
126 */
127 static function earthLongitudeRange($longitude, $latitude, $distance) {
128 $long = deg2rad($longitude);
129 $lat = deg2rad($latitude);
130 $radius = self::earthRadius($latitude);
131
132 $angle = $distance / $radius;
133 $diff = asin(sin($angle) / cos($lat));
134 $minLong = $long - $diff;
135 $maxLong = $long + $diff;
136
137 if ($minLong < - pi()) {
138 $minLong = $minLong + pi() * 2;
139 }
140
141 if ($maxLong > pi()) {
142 $maxLong = $maxLong - pi() * 2;
143 }
144
145 return array(rad2deg($minLong),
146 rad2deg($maxLong),
147 );
148 }
149
150 /**
151 * Estimate the min and max latitudes within $distance of a given location.
152 */
153 static function earthLatitudeRange($longitude, $latitude, $distance) {
154 $long = deg2rad($longitude);
155 $lat = deg2rad($latitude);
156 $radius = self::earthRadius($latitude);
157
158 $angle = $distance / $radius;
159 $minLat = $lat - $angle;
160 $maxLat = $lat + $angle;
161 $rightangle = pi() / 2.0;
162
163 // wrapped around the south pole
164 if ($minLat < - $rightangle) {
165 $overshoot = -$minLat - $rightangle;
166 $minLat = -$rightangle + $overshoot;
167 if ($minLat > $maxLat) {
168 $maxLat = $minLat;
169 }
170 $minLat = -$rightangle;
171 }
172
173 // wrapped around the north pole
174 if ($maxLat > $rightangle) {
175 $overshoot = $maxLat - $rightangle;
176 $maxLat = $rightangle - $overshoot;
177 if ($maxLat < $minLat) {
178 $minLat = $maxLat;
179 }
180 $maxLat = $rightangle;
181 }
182
183 return array(rad2deg($minLat),
184 rad2deg($maxLat),
185 );
186 }
187
6a488035
TO
188 static function where($latitude, $longitude, $distance, $tablePrefix = 'civicrm_address') {
189 self::initialize();
190
191 $params = array();
192 $clause = array();
193
84f81538
DL
194 list($minLongitude, $maxLongitude) =
195 self::earthLongitudeRange($longitude,
196 $latitude,
197 $distance
198 );
199 list($minLatitude, $maxLatitude) =
200 self::earthLatitudeRange(
201 $longitude,
202 $latitude,
203 $distance
204 );
6a488035
TO
205
206 $where = "
207{$tablePrefix}.geo_code_1 >= $minLatitude AND
208{$tablePrefix}.geo_code_1 <= $maxLatitude AND
209{$tablePrefix}.geo_code_2 >= $minLongitude AND
210{$tablePrefix}.geo_code_2 <= $maxLongitude AND
84f81538
DL
211ACOS(
212 COS(RADIANS({$tablePrefix}.geo_code_1)) *
213 COS(RADIANS($latitude)) *
214 COS(RADIANS({$tablePrefix}.geo_code_2) - RADIANS($longitude)) +
215 SIN(RADIANS({$tablePrefix}.geo_code_1)) *
216 SIN(RADIANS($latitude))
217 ) * 6378137 <= $distance
6a488035
TO
218";
219
220 return $where;
221 }
222
223 static function process(&$query, &$values) {
224 list($name, $op, $distance, $grouping, $wildcard) = $values;
225
226 // also get values array for all address related info
227 $proximityVars = array(
228 'street_address' => 1,
229 'city' => 1,
230 'postal_code' => 1,
231 'state_province_id' => 0,
232 'country_id' => 0,
233 'state_province' => 0,
234 'country' => 0,
235 'distance_unit' => 0,
236 );
237
238 $proximityAddress = array();
239 $qill = array();
240 foreach ($proximityVars as $var => $recordQill) {
241 $proximityValues = $query->getWhereValues("prox_{$var}", $grouping);
242 if (!empty($proximityValues) &&
243 !empty($proximityValues[2])
244 ) {
245 $proximityAddress[$var] = $proximityValues[2];
246 if ($recordQill) {
247 $qill[] = $proximityValues[2];
248 }
249 }
250 }
251
252 if (empty($proximityAddress)) {
253 return;
254 }
255
256 if (isset($proximityAddress['state_province_id'])) {
257 $proximityAddress['state_province'] = CRM_Core_PseudoConstant::stateProvince($proximityAddress['state_province_id']);
258 $qill[] = $proximityAddress['state_province'];
259 }
260
b05e28de
DL
261 $config = CRM_Core_Config::singleton();
262 if (!isset($proximityAddress['country_id'])) {
263 // get it from state if state is present
264 if (isset($proximityAddress['state_province_id'])) {
265 $proximityAddress['country_id'] = CRM_Core_PseudoConstant::countryForState($proximityAddress['state_province_id']);
266 }
a5175a12 267 elseif (isset($config->defaultContactCountry)) {
b05e28de
DL
268 $proximityAddress['country_id'] = $config->defaultContactCountry;
269 }
270 }
271
6a488035
TO
272 if (isset($proximityAddress['country_id'])) {
273 $proximityAddress['country'] = CRM_Core_PseudoConstant::country($proximityAddress['country_id']);
274 $qill[] = $proximityAddress['country'];
275 }
276
6a488035 277
b05e28de
DL
278 if (
279 isset($proximityAddress['distance_unit']) &&
6a488035
TO
280 $proximityAddress['distance_unit'] == 'miles'
281 ) {
282 $qillUnits = " {$distance} " . ts('miles');
283 $distance = $distance * 1609.344;
284 }
285 else {
286 $qillUnits = " {$distance} " . ts('km');
287 $distance = $distance * 1000.00;
288 }
289
290 $qill = ts('Proximity search to a distance of %1 from %2',
291 array(
292 1 => $qillUnits,
293 2 => implode(', ', $qill)
294 )
295 );
296
b05e28de
DL
297 $fnName = isset($config->geocodeMethod) ? $config->geocodeMethod : NULL;
298 if (empty($fnName)) {
299 CRM_Core_Error::fatal(ts('Proximity searching requires you to set a valid geocoding provider'));
300 }
301
6d6c8253
DL
302 $query->_tables['civicrm_address'] = $query->_whereTables['civicrm_address'] = 1;
303
b05e28de
DL
304 require_once (str_replace('_', DIRECTORY_SEPARATOR, $fnName) . '.php');
305 $fnName::format($proximityAddress);
306 if (
307 !is_numeric(CRM_Utils_Array::value('geo_code_1', $proximityAddress)) ||
308 !is_numeric(CRM_Utils_Array::value('geo_code_2', $proximityAddress))
309 ) {
310 // we are setting the where clause to 0 here, so we wont return anything
311 $qill .= ': ' . ts('We could not geocode the destination address.');
312 $query->_qill[$grouping][] = $qill;
313 $query->_where[$grouping][] = ' (0) ';
314 return;
315 }
316
317 $query->_qill[$grouping][] = $qill;
b05e28de
DL
318 $query->_where[$grouping][] = self::where(
319 $proximityAddress['geo_code_1'],
6a488035
TO
320 $proximityAddress['geo_code_2'],
321 $distance
322 );
b05e28de 323
6a488035
TO
324 return;
325 }
326
327 static function fixInputParams(&$input) {
328 foreach ($input as $param) {
329 if (CRM_Utils_Array::value('0', $param) == 'prox_distance') {
330 // add prox_ prefix to these
331 $param_alter = array('street_address', 'city', 'postal_code', 'state_province', 'country');
332
333 foreach ($input as $key => $_param) {
334 if (in_array($_param[0], $param_alter)) {
335 $input[$key][0] = 'prox_' . $_param[0];
336
337 // _id suffix where needed
338 if ($_param[0] == 'country' || $_param[0] == 'state_province') {
339 $input[$key][0] .= '_id';
340
341 // flatten state_province array
342 if (is_array($input[$key][2])) {
343 $input[$key][2] = $input[$key][2][0];
344 }
345 }
346 }
347 }
348 return;
349 }
350 }
351 }
352}
353