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