PageRenderTime 68ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/geography.class.php

http://github.com/treffynnon/Geographic-Calculations-in-PHP
PHP | 444 lines | 170 code | 32 blank | 242 comment | 18 complexity | be84f2c54d1dad72a36b4156d9fcabb5 MD5 | raw file
  1. <?php
  2. /**
  3. * Geography v0.0.2 10/12/2006
  4. * Simon Holywell
  5. * http://www.simonholywell.com/
  6. *
  7. * You may use this class as long as all
  8. * copyright information remains intact.
  9. * Be kind and stick a link to simonholywell.com
  10. * on your website.
  11. */
  12. class Geography {
  13. /**
  14. * Mean radius of the earth
  15. *
  16. * @var float
  17. */
  18. private $earthRadius = 6371; //mean radius in KM
  19. /* http://www.movable-type.co.uk/scripts/LatLongVincenty.html
  20. The most accurate and widely used globally-applicable model for the earth ellipsoid is WGS-84, used in this script. Other ellipsoids offering a better fit to the local geoid include Airy (1830) in the UK, International 1924 in much of Europe, Clarke (1880) in Africa, and GRS-67 in South America. America (NAD83) and Australia (GDA) use GRS-80, functionally equivalent to the WGS-84 ellipsoid.
  21. WGS-84 a = 6 378 137 m (2 m) b = 6 356 752.3142 m f = 1 / 298.257223563
  22. GRS-80 a = 6 378 137 m b = 6 356 752.3141 m f = 1 / 298.257222101
  23. Airy (1830) a = 6 377 563.396 m b = 6 356 256.909 m f = 1 / 299.3249646
  24. Intl 1924 a = 6 378 388 m b = 6 356 911.946 m f = 1 / 297
  25. Clarke (1880) a = 6 378 249.145 m b = 6 356 514.86955 m f = 1 / 293.465
  26. GRS-67 a = 6 378 160 m b = 6 356 774.719 m f = 1 / 298.25 */
  27. /**
  28. * Major Semiax in metres.
  29. *
  30. * @var float
  31. */
  32. private $majorSemiax = 6378137;
  33. /**
  34. * Minor Semiax in metres.
  35. *
  36. * @var float
  37. */
  38. private $minorSemiax = 6356752.3141;
  39. /**
  40. * Calculation method switch.
  41. * Used to action a variable method.
  42. *
  43. * @var string
  44. */
  45. protected $calculationMethod = 'vincenty';
  46. /**
  47. * Set the method to be used for calculating
  48. * the distance between two coordinates.
  49. *
  50. * Defaults to Vincenty's
  51. *
  52. * v - Vincenty's formula
  53. * gc - Simplified Great Circle formula
  54. * h - Haversine Formula
  55. * cs - Cosine Law Formula
  56. *
  57. * Vincenty's is the most accurate but also
  58. * contains quite involved calculations so if
  59. * you don't need the highest accuracy it maybe
  60. * best to choose a less intensive method.
  61. *
  62. * @param string $method
  63. */
  64. public function setCalculationMethod($method) {
  65. switch($method) {
  66. case 'v':
  67. $this->calculationMethod = 'vincenty';
  68. break;
  69. case 'gc':
  70. $this->calculationMethod = 'greatCircle';
  71. break;
  72. case 'h':
  73. $this->calculationMethod = 'haversine';
  74. break;
  75. case 'cs':
  76. $this->calculationMethod = 'cosineLaw';
  77. break;
  78. default:
  79. $this->calculationMethod = 'vincenty';
  80. break;
  81. }
  82. }
  83. /**
  84. * Supply a coordinate in Degrees, Minutes
  85. * and Seconds that you wish to be converted
  86. * to a decimal representation.
  87. *
  88. * The default format for input is: 52 12 17N
  89. * but you can supply a custom regex to $format
  90. * for the function to parse different input
  91. * formats.
  92. *
  93. * @param string $coord
  94. * @param string $format
  95. * @return float
  96. */
  97. public function convertToDecimal($coord, $format = false) {
  98. if(!$format)
  99. $format = '([0-9]{1,3}) ([0-9]{1,2}) ([0-9]{1,2}[.]{0,1}[0-9]*)([N,S,E,W])';
  100. preg_match('/'.$format.'/', $coord, $matches);
  101. $degrees = $matches[1];
  102. $minutes = $matches[2] * (1/60);
  103. $seconds = $matches[3] * (1/60 * 1/60);
  104. $coordinate = '';
  105. if(isset($matches[4]) and (
  106. $matches[4] == 'S' or
  107. $matches[4] == 'W')
  108. ) {
  109. $coordinate .= '-';
  110. }
  111. $coordinate .= $degrees + $minutes + $seconds;
  112. return (float)$coordinate;
  113. }
  114. /**
  115. * Helper function to convert decimal
  116. * latitudes into Degrees, Minutes,
  117. * Seconds notation.
  118. *
  119. * The default output format is: 52 12 17N
  120. * but you can supply a custom sprintf format
  121. * to $output_format for the function to parse
  122. * different output formats.
  123. *
  124. * @param float $coord
  125. * @param string $output_format
  126. * @return string
  127. */
  128. public function convertLatToDMS($coord, $output_format = false) {
  129. return $this->convertToDMS($coord, 'lat', $output_format);
  130. }
  131. /**
  132. * Helper function to convert decimal
  133. * longitudes into Degrees, Minutes,
  134. * Seconds notation.
  135. *
  136. * The default output format is: 52 12 17E
  137. * but you can supply a custom sprintf format
  138. * to $output_format for the function to parse
  139. * different output formats.
  140. *
  141. * @param float $coord
  142. * @param string $output_format
  143. * @return string
  144. */
  145. public function convertLongToDMS($coord, $output_format = false) {
  146. return $this->convertToDMS($coord, 'long', $output_format);
  147. }
  148. /**
  149. * Convert decimal coordinate to
  150. * Degrees, Minutes, Seconds notation.
  151. *
  152. * Supply $direction with either 'lat' or
  153. * 'long' for the relevant compass point
  154. * to be appended to the output.
  155. *
  156. * Give $output_format a custom sprintf
  157. * format if you would like a custom output
  158. * format.
  159. *
  160. * @param float $coord
  161. * @param string $direction
  162. * @param string $output_format
  163. * @return string
  164. */
  165. public function convertToDMS($coord, $direction = false, $output_format = false) {
  166. if(!$output_format)
  167. $output_format = '%d %d %F%s';
  168. $degrees = (integer)$coord;
  169. $compass = '';
  170. if($direction == 'lat') {
  171. if($degrees < 0)
  172. $compass = 'S';
  173. elseif($degrees > 0)
  174. $compass = 'N';
  175. }elseif($direction == 'long') {
  176. if($degrees < 0)
  177. $compass = 'W';
  178. elseif($degrees > 0)
  179. $compass = 'E';
  180. }
  181. $minutes = $coord - $degrees;
  182. if($minutes < 0)
  183. $minutes -= (2 * $minutes);
  184. if($degrees < 0)
  185. $degrees -= (2 * $degrees);
  186. $minutes = $minutes * 60;
  187. $seconds = $minutes - (integer)$minutes;
  188. $minutes = (integer)$minutes;
  189. $seconds = (float)$seconds * 60;
  190. $coordinate = sprintf($output_format, $degrees, $minutes, $seconds, $compass);
  191. return $coordinate;
  192. }
  193. /**
  194. * Calculate the distance between two
  195. * points using the Great Circle
  196. * formula.
  197. *
  198. * Supply instances of the coordinate class.
  199. *
  200. * http://www.ga.gov.au/geodesy/datums/distance.jsp#circle
  201. *
  202. * @param object $p1
  203. * @param object $p2
  204. * @return float
  205. */
  206. private function greatCircle($p1, $p2) {
  207. $degrees = rad2deg(acos(sin($p1->latRadian) * sin($p2->latRadian) + cos($p1->latRadian) * cos($p2->latRadian) * cos($p2->longRadian - $p1->longRadian)));
  208. $arcMinutes = 60 * $degrees;
  209. $metres = $arcMinutes * 1852;
  210. return number_format($metres, 3, '.', ''); //round to 1mm precision;
  211. }
  212. /**
  213. * Calculate the distance between two
  214. * points using the Haversine formula.
  215. *
  216. * Supply instances of the coordinate class.
  217. *
  218. * http://en.wikipedia.org/wiki/Haversine_formula
  219. *
  220. * @param object $p1
  221. * @param object $p2
  222. * @return float
  223. */
  224. private function haversine($p1, $p2) {
  225. $deltaLat = $p2->latRadian - $p1->latRadian;
  226. $deltaLong = $p2->longRadian - $p1->longRadian;
  227. $a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($p1->latRadian) * cos($p2->latRadian) * sin($deltaLong / 2) * sin($deltaLong / 2);
  228. $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
  229. $d = $this->earthRadius * $c * 1000;
  230. return number_format($d, 3, '.', '');
  231. }
  232. /**
  233. * Calculate the distance between two
  234. * points using the Cosine Law formula.
  235. *
  236. * Supply instances of the coordinate class.
  237. *
  238. * http://en.wikipedia.org/wiki/Cosine_law
  239. *
  240. * @param object $p1
  241. * @param object $p2
  242. * @return float
  243. */
  244. private function cosineLaw($p1, $p2) {
  245. $d = acos(sin($p1->latRadian) * sin($p2->latRadian) + cos($p1->latRadian) * cos($p2->latRadian) * cos($p2->longRadian - $p1->longRadian)) * $this->earthRadius;
  246. return number_format($d * 1000, 3, '.', '');
  247. }
  248. /**
  249. * Calculate the distance between two
  250. * points using Vincenty's formula.
  251. *
  252. * Supply instances of the coordinate class.
  253. *
  254. * http://www.movable-type.co.uk/scripts/LatLongVincenty.html
  255. *
  256. * @param object $p1
  257. * @param object $p2
  258. * @return float
  259. */
  260. private function vincenty($p1, $p2) {
  261. $a = $this->majorSemiax;
  262. $b = $this->minorSemiax;
  263. $f = ($a - $b) / $a; //flattening of the ellipsoid
  264. $L = $p2->longRadian - $p1->longRadian; //difference in longitude
  265. $U1 = atan((1 - $f) * tan($p1->latRadian)); //U is 'reduced latitude'
  266. $U2 = atan((1 - $f) * tan($p2->latRadian));
  267. $sinU1 = sin($U1);
  268. $sinU2 = sin($U2);
  269. $cosU1 = cos($U1);
  270. $cosU2 = cos($U2);
  271. $lambda = $L;
  272. $lambdaP = 2 * pi();
  273. $i = 20;
  274. while(abs($lambda - $lambdaP) > 1e-12 and
  275. --$i > 0) {
  276. $sinLambda = sin($lambda);
  277. $cosLambda = cos($lambda);
  278. $sinSigma = sqrt(($cosU2 * $sinLambda) * ($cosU2 * $sinLambda) + ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) * ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda));
  279. if($sinSigma == 0)
  280. return 0; //co-incident points
  281. $cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda;
  282. $sigma = atan2($sinSigma, $cosSigma);
  283. $sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma;
  284. $cosSqAlpha = 1 - $sinAlpha * $sinAlpha;
  285. $cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha;
  286. if(is_nan($cos2SigmaM))
  287. $cos2SigmaM = 0; //equatorial line: cosSqAlpha=0 (6)
  288. $c = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
  289. $lambdaP = $lambda;
  290. $lambda = $L + (1 - $c) * $f * $sinAlpha * ($sigma + $c * $sinSigma * ($cos2SigmaM + $c * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM)));
  291. }
  292. if($i == 0)
  293. return false; //formula failed to converge
  294. $uSq = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b);
  295. $A = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq)));
  296. $B = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq)));
  297. $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) - $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM)));
  298. $d = $b * $A * ($sigma - $deltaSigma);
  299. return number_format($d, 3, '.', ''); //round to 1mm precision
  300. }
  301. /**
  302. * Convert Metres to Kilometres.
  303. *
  304. * @param float $metres
  305. * @return float
  306. */
  307. public function mToKm($metres) {
  308. return number_format($metres / 1000, 3, '.', '');
  309. }
  310. /**
  311. * Convert Metres to Nautical Miles
  312. *
  313. * @param float $metres
  314. * @return float
  315. */
  316. public function mToNM($metres) {
  317. return number_format($metres / 1852, 3, '.', '');
  318. }
  319. /**
  320. * Convert Metres to Miles
  321. *
  322. * @param float $metres
  323. * @return float
  324. */
  325. public function mToM($metres) {
  326. return number_format($metres * 0.000621371192, 3, '.', '');
  327. }
  328. /**
  329. * Get distance in metres between the
  330. * supplied longitudes and latitudes.
  331. *
  332. * You must supply the latitudes and
  333. * longitudes as decimals.
  334. *
  335. * @param float $latitude1
  336. * @param float $longitude1
  337. * @param float $latitude2
  338. * @param float $longitude2
  339. * @return float
  340. */
  341. public function getDistance($latitude1, $longitude1, $latitude2, $longitude2, $calculation_method = false) {
  342. $point1 = new Coordinate($latitude1, $longitude1);
  343. $point2 = new Coordinate($latitude2, $longitude2);
  344. if($calculation_method)
  345. $this->setCalculationMethod($calculation_method);
  346. //use a variable function to decide the calculation method
  347. $function = $this->calculationMethod;
  348. return $this->$function($point1, $point2);
  349. }
  350. }
  351. /**
  352. * Store information about a coordinate.
  353. */
  354. class Coordinate {
  355. /**
  356. * Latitude in decimal form
  357. *
  358. * @var float
  359. */
  360. public $latitude = 0;
  361. /**
  362. * Longitude in decimal form
  363. *
  364. * @var float
  365. */
  366. public $longitude = 0;
  367. /**
  368. * Latitude as Radians
  369. *
  370. * @var float
  371. */
  372. public $latRadian = 0;
  373. /**
  374. * Longitude as Radians
  375. *
  376. * @var float
  377. */
  378. public $longRadian = 0;
  379. /**
  380. * Name of the coordinate
  381. *
  382. * @var string
  383. */
  384. public $name = '';
  385. /**
  386. * Description of the coordinate
  387. *
  388. * @var string
  389. */
  390. public $description = '';
  391. /**
  392. * Stick all the information into
  393. * the object upon instantiation.
  394. * Converting the longitude and
  395. * latitude into radians in the
  396. * process.
  397. *
  398. * @param float $latitude
  399. * @param float $longitude
  400. * @param string $name
  401. * @param string $description
  402. */
  403. public function __construct($latitude, $longitude, $name = '', $description = '') {
  404. $this->latitude = $latitude;
  405. $this->longitude = $longitude;
  406. $this->name = $name;
  407. $this->description = $description;
  408. $this->latRadian = deg2rad($this->latitude);
  409. $this->longRadian = deg2rad($this->longitude);
  410. }
  411. }
  412. ?>