PageRenderTime 49ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/models/behaviors/rangeable.php

http://github.com/webtechnick/CakePHP-WebTechNick-Plugin
PHP | 261 lines | 190 code | 8 blank | 63 comment | 47 complexity | bfeadd9de76956ee1852e51c373d6cf4 MD5 | raw file
  1. <?php
  2. /**
  3. * A CakePHP behavior to facilitate a simple reteival of results via a range search on a lon/lat fields
  4. *
  5. * Copyright 2010, Alan Blount
  6. * Alan@zeroasterisk.com
  7. *
  8. * Licensed under The MIT License
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright 2010, Alan Blount
  12. * @version 1.0
  13. * @author Alan Blount <Alan@zeroasterisk.com>
  14. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  15. * @link https://github.com/zeroasterisk/cakephp-behavior-rangeable
  16. */
  17. class RangeableBehavior extends ModelBehavior {
  18. var $settings = array();
  19. var $milesToDegrees = .01445;
  20. var $mapMethods = array('/^_findRange$/' => '_findRange');
  21. var $debug = array(); // false or array()
  22. /**
  23. * Setup the Behavior
  24. */
  25. function setup(&$Model, $settings = array()) {
  26. if (!isset($this->settings[$Model->alias])) {
  27. $this->settings[$Model->alias] = array();
  28. }
  29. if (!is_array($settings)) {
  30. $settings = array();
  31. }
  32. $this->settings[$Model->alias] = array_merge(array(
  33. 'field_lat' => 'lat',
  34. 'field_lon' => 'lon',
  35. 'field_zip' => 'zip', // only used if 'lookup_zip_from_conditions' == true
  36. 'lookup_zip_from_conditions' => true, // lookup a zip from the conditions array, remove from conditions array
  37. 'lookup_zip_model_name' => 'Zip',
  38. 'lookup_zip_field_zip' => 'zip',
  39. 'lookup_zip_field_lat' => 'lat',
  40. 'lookup_zip_field_lon' => 'lon',
  41. 'range' => 20, // range in miles
  42. 'range_out_till_count_is' => 2, // keep increasing range till count >= (false or 0 to disable)
  43. 'range_out_increment' => 20, // increasing range by this increment
  44. 'range_out_limit' => 20, // maximum tries - max range = range + (range_out_increment * range_out_limit)
  45. 'order_by_distance' => true, // resort results by distance to target
  46. 'unique_only' => true, // only show unique items based on primaryKey
  47. 'limitless' => true, // removes limit for initial find, so the sort can be accurate
  48. 'query' => array(),
  49. 'countField' => 'count',
  50. ), $settings);
  51. $Model->_findMethods['range'] = true;
  52. if (!$Model->hasField($this->settings[$Model->alias]['lookup_zip_field_lat']) || !$Model->hasField($this->settings[$Model->alias]['lookup_zip_field_lon'])) {
  53. trigger_error('RangeableBehavior: You must have a valid lat and lon field for querying this Model\'s as rangeable!');
  54. die();
  55. }
  56. }
  57. /**
  58. * Custom Find Method which injects the range box (and optionally increments that range until we hit a minimum set of results) into the conditions
  59. * it also re-sorts the results by the distance to the sort point
  60. */
  61. function _findRange(&$Model, $method, $state, $query, $results = array()) {
  62. if ($state == 'before') {
  63. $query = array_merge($query, $this->settings[$Model->alias]['query']);
  64. $lat = $lon = $zip = null;
  65. if (isset($query['lat'])) { $lat = $query['lat']; }
  66. foreach ( array($this->settings[$Model->alias]['field_lat'], "{$Model->alias}.{$this->settings[$Model->alias]['field_lat']}") as $key ) {
  67. if (array_key_exists($key, $query['conditions'])) {
  68. $lat = $query['conditions'][$key];
  69. unset($query['conditions'][$key]);
  70. }
  71. }
  72. if (isset($query['lon'])) { $lon = $query['lon']; }
  73. foreach ( array($this->settings[$Model->alias]['field_lon'], "{$Model->alias}.{$this->settings[$Model->alias]['field_lon']}") as $key ) {
  74. if (array_key_exists($key, $query['conditions'])) {
  75. $lon = $query['conditions'][$key];
  76. unset($query['conditions'][$key]);
  77. }
  78. }
  79. if (isset($query['zip'])) { $zip = $query['zip']; }
  80. if ($this->settings[$Model->alias]['lookup_zip_from_conditions']) {
  81. foreach ( array($this->settings[$Model->alias]['field_zip'], "{$Model->alias}.{$this->settings[$Model->alias]['field_zip']}") as $key ) {
  82. if (array_key_exists($key, $query['conditions'])) {
  83. $zip = $query['conditions'][$key];
  84. unset($query['conditions'][$key]);
  85. }
  86. }
  87. if (empty($lat) || empty($lon)) {
  88. if (!empty($zip)) {
  89. if (!isset($this->ZipModel) || !is_object($this->ZipModel)) {
  90. App::import('Model', $this->settings[$Model->alias]['lookup_zip_model_name']);
  91. $this->ZipModel = null;
  92. $this->ZipModel =& ClassRegistry::init($this->settings[$Model->alias]['lookup_zip_model_name']);
  93. }
  94. $zipData = $this->ZipModel->find('first', array(
  95. 'recursive' => 1,
  96. 'conditions' => array("{$this->ZipModel->alias}.{$this->settings[$Model->alias]['lookup_zip_field_zip']}" => $zip)
  97. ));
  98. if (isset($zipData[$this->ZipModel->alias][($this->settings[$Model->alias]['lookup_zip_field_lat'])])) {
  99. $lat = $zipData[$this->ZipModel->alias][($this->settings[$Model->alias]['lookup_zip_field_lat'])];
  100. }
  101. if (isset($zipData[$this->ZipModel->alias][($this->settings[$Model->alias]['lookup_zip_field_lon'])])) {
  102. $lon = $zipData[$this->ZipModel->alias][($this->settings[$Model->alias]['lookup_zip_field_lon'])];
  103. }
  104. }
  105. }
  106. }
  107. if (!empty($lat) && !empty($lon)) {
  108. $range = 10;
  109. if (isset($query['range']) && !empty($query['range'])) { $range = $query['range']; }
  110. $query = $this->getRangeQuery($Model, $query, $range, $lat, $lon);
  111. $range_out_till_count_is = $this->settings[$Model->alias]['range_out_till_count_is'];
  112. if (isset($query['range_out_till_count_is'])) {
  113. $range_out_till_count_is = $query['range_out_till_count_is'];
  114. }
  115. if (!empty($range_out_till_count_is)) {
  116. // gotta count results and loop until count =>
  117. $count = $Model->find('count', $query);
  118. $reps = 0;
  119. while ($count < $range_out_till_count_is && $reps < $this->settings[$Model->alias]['range_out_limit']) {
  120. $range = $range + $this->settings[$Model->alias]['range_out_increment'];
  121. $query = $this->getRangeQuery($Model, $query, $range, $lat, $lon);
  122. $count = $Model->find('count', $query);
  123. $reps++;
  124. }
  125. }
  126. $query['lat'] = $lat;
  127. $query['lon'] = $lon;
  128. $query['range'] = $range;
  129. // possibly remove limit, from find, so that order_by_distance works right
  130. $limitless = $this->settings[$Model->alias]['limitless'];
  131. if (isset($query['limitless'])) { $limitless = $query['limitless']; }
  132. $order_by_distance = $this->settings[$Model->alias]['order_by_distance'];
  133. if (isset($query['order_by_distance'])) { $order_by_distance = $query['order_by_distance']; }
  134. if (!empty($limitless) && !empty($order_by_distance) && isset($query['limit'])) {
  135. $query['limit_'] = $query['limit'];
  136. $query['limit'] = (is_numeric($limitless) ? $limitless : 9999);
  137. }
  138. }
  139. return $query;
  140. }
  141. if (empty($results)) {
  142. return array();
  143. }
  144. // re-sort by distance
  145. $lat = $lon = null;
  146. if (isset($query['lat'])) { $lat = $query['lat']; }
  147. if (isset($query['lon'])) { $lon = $query['lon']; }
  148. $unique_only = isset($query['unique_only']) ? $query['unique_only'] : $this->settings[$Model->alias]['unique_only'];
  149. $order_by_distance = isset($query['order_by_distance']) ? $query['order_by_distance'] : $this->settings[$Model->alias]['order_by_distance'];
  150. if (!empty($lat) && !empty($lon) && !empty($order_by_distance)) {
  151. $resultsByRange = array();
  152. $foundIds = array();
  153. foreach ( $results as $result ) {
  154. $distance = $this->calculatePrecisionDistance($lat, $lon, $result[$Model->alias][($this->settings[$Model->alias]['lookup_zip_field_lat'])], $result[$Model->alias][($this->settings[$Model->alias]['lookup_zip_field_lon'])]);
  155. $result[$Model->alias]['distance'] = $distance;
  156. $distanceSortable = intval(($distance*1000));
  157. if (array_key_exists($distanceSortable, $resultsByRange)) {
  158. while (array_key_exists($distanceSortable, $resultsByRange)) {
  159. $distanceSortable++;
  160. }
  161. }
  162. //only display unique
  163. if(!$unique_only || !in_array($result[$Model->alias][$Model->primaryKey], $foundIds)){
  164. $foundIds[] = $result[$Model->alias][$Model->primaryKey];
  165. $resultsByRange[$distanceSortable] = $result;
  166. }
  167. }
  168. ksort($resultsByRange);
  169. $results = array();
  170. foreach ( $resultsByRange as $distanceSortable => $result ) {
  171. if (!isset($query['limit_']) || count($results) < $query['limit_']) {
  172. // treated as a string, so that putting into an array doesn't force type to an int.
  173. $distanceSortable = strval($distanceSortable);
  174. $distanceSortable = substr($distanceSortable, 0, strlen($distanceSortable)-3).'.'.substr($distanceSortable,-3, 4);
  175. $results[$distanceSortable] = $result;
  176. }
  177. }
  178. }
  179. return $results;
  180. }
  181. /**
  182. * Get the latitude and longitude for a single zip code.
  183. *
  184. * @access public
  185. * @param object $Model
  186. * @param array $query
  187. * @param float $range
  188. * @param float $lat
  189. * @param float $lon
  190. * @return array conditions node for this range
  191. */
  192. function getRangeQuery(&$Model, $query, $range, $lat, $lon) {
  193. $maxCoords = $this->getRangeBox($range, $lat, $lon);
  194. $return = $query;
  195. $return['conditions']["{$Model->alias}.{$this->settings[$Model->alias]['lookup_zip_field_lat']} BETWEEN ? AND ?"] = array($maxCoords['min_lat'], $maxCoords['max_lat']);
  196. $return['conditions']["{$Model->alias}.{$this->settings[$Model->alias]['lookup_zip_field_lon']} BETWEEN ? AND ?"] = array($maxCoords['min_lon'], $maxCoords['max_lon']);
  197. if (is_array($this->debug)) {
  198. $this->debug[__function__][] = compact('range', 'query', 'lat', 'lon', 'return');
  199. }
  200. return $return;
  201. }
  202. /**
  203. * Get the maximum and minimum longitude and latitude values
  204. * that our zip codes can be in.
  205. *
  206. * Not all zipcodes in this box will be with in the range.
  207. * The closest edge of this box is exactly range miles away
  208. * from the origin but the corners are sqrt(2(range^2)) miles
  209. * away. That is why we have to double check the ranges.
  210. *
  211. * @access public
  212. * @param float $range
  213. * @param float $lat
  214. * @param float $lon
  215. * @return array associative index max/min lat/lon
  216. */
  217. function getRangeBox($range, $lat, $lon) {
  218. // Calculate the degree range using the mile range
  219. $degrees = $range * $this->milesToDegrees;
  220. $lat = floatval($lat);
  221. $lon = floatval($lon);
  222. $return = array(
  223. 'max_lat' => $lat + $degrees,
  224. 'max_lon' => $lon + $degrees,
  225. 'min_lat' => $lat - $degrees,
  226. 'min_lon' => $lon - $degrees,
  227. );
  228. if (is_array($this->debug)) {
  229. $this->debug[__function__][] = compact('range', 'lat', 'lon', 'return');
  230. }
  231. return $return;
  232. }
  233. /**
  234. * this does the math to find the actual distance between two sets of lat/lon coordinates
  235. * @param float $sourceLat
  236. * @param float $sourceLon
  237. * @param float $targetLat
  238. * @param float $targetLon
  239. * @param int $precision
  240. * @return float $distanceBetweenSourceAndTarget
  241. */
  242. function calculatePrecisionDistance($sourceLat, $sourceLon, $targetLat, $targetLon, $precision = 2) {
  243. $earthsradius = 3963.19;
  244. $pi = pi();
  245. $c = sin($sourceLat/(180/$pi)) * sin($targetLat/(180/$pi)) +
  246. cos($sourceLat/(180/$pi)) * cos($targetLat/(180/$pi)) *
  247. cos($targetLon/(180/$pi) - $sourceLon/(180/$pi));
  248. $c = preg_replace("/1/", "1", $c);
  249. $distance = $earthsradius * acos($c);
  250. return round($distance, $precision);
  251. }
  252. }
  253. ?>