PageRenderTime 60ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/src/php/Bairwell/Geocoder/LatLon.php

https://bitbucket.org/bairwell/geocoder
PHP | 446 lines | 244 code | 43 blank | 159 comment | 21 complexity | 97ab07ee4bcf841b397a65b9545ba918 MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. * Latitude/longitude spherical geodesy formulae & scripts
  4. *
  5. * @package Bairwell
  6. * @subpackage Geocoder
  7. * @author Richard Chiswell <richard@bairwell.com>
  8. * @copyright 2012 Bairwell Ltd
  9. * @license MIT
  10. */
  11. namespace Bairwell\Geocoder;
  12. /**
  13. * Latitude/longitude spherical geodesy formulae & scripts.
  14. *
  15. * Based on Javascript code written by and (c) Chris Veness 2002-2012
  16. * http://www.movable-type.co.uk/scripts/latlon.js
  17. * http://www.movable-type.co.uk/scripts/latlong.html
  18. */
  19. class LatLon
  20. {
  21. /**
  22. * Latitude in numeric degrees
  23. * @var float
  24. */
  25. private $lat;
  26. /**
  27. * Longitude in numeric degrees
  28. * @var float
  29. */
  30. private $lon;
  31. /**
  32. * Radius of earth
  33. * @var int
  34. */
  35. private $radius;
  36. /**
  37. * Creates a point on the earth's surface at the supplied latitude / longitude.
  38. *
  39. * @param float $lat latitude in numeric degrees
  40. * @param float $lon longitude in numeric degrees
  41. * @param float $rad radius of earth if different value is required from UGC standard 6,371km
  42. */
  43. public function __construct($lat = 0.0, $lon = 0.0, $rad = 6371.0)
  44. {
  45. if (TRUE === is_numeric($lat)) {
  46. $this->setLat(floatval($lat));
  47. }
  48. if (TRUE === is_numeric($lon)) {
  49. $this->setLon(floatval($lon));
  50. }
  51. if (TRUE === is_numeric($rad)) {
  52. $this->setRadius(floatval($rad));
  53. }
  54. }
  55. /**
  56. * Returns the distance from this point to the supplied point, in km.
  57. * (using Haversine formula)
  58. *
  59. * from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
  60. * Sky and Telescope, vol 68, no 2, 1984
  61. *
  62. * @param LatLon $point Latitude/longitude of destination point
  63. * @param int $precision no of significant digits to use for returned value. 4 sig figs reflects typical 0.3% accuracy of spherical model
  64. * @return string Distance in km between this point and destination point
  65. */
  66. public function distanceTo(LatLon $point, $precision = 4)
  67. {
  68. $R = $this->getRadius();
  69. $lat1 = deg2rad($this->getLat());
  70. $lon1 = deg2rad($this->getLon());
  71. $lat2 = deg2rad($point->getLat());
  72. $lon2 = deg2rad($point->getLon());
  73. $dLat = $lat2 - $lat1;
  74. $dLon = $lon2 - $lon1;
  75. $a = sin($dLat / 2) * sin($dLat / 2) +
  76. cos($lat1) * cos($lat2) *
  77. sin($dLon / 2) * sin($dLon / 2);
  78. $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
  79. $d = $R * $c;
  80. return FixedPrecision::toPrecisionFixed($d, $precision);
  81. }
  82. /**
  83. * Returns the (initial) bearing from this point to the supplied point, in degrees.
  84. *
  85. * see http://williams.best.vwh.net/avform.htm#Crs
  86. *
  87. * @param LatLon $point Latitude/longitude of destination point
  88. * @return float Initial bearing in degrees from North
  89. */
  90. public function bearingTo(LatLon $point)
  91. {
  92. $lat1 = deg2rad($this->getLat());
  93. $lat2 = deg2rad($point->getLat());
  94. $dLon = deg2rad($point->getLon() - $this->getLon());
  95. $y = sin($dLon) * cos($lat2);
  96. $x = cos($lat1) * sin($lat2) -
  97. sin($lat1) * cos($lat2) * cos($dLon);
  98. $brng = atan2($y, $x);
  99. return fmod(rad2deg($brng) + 360, 360);
  100. }
  101. /**
  102. * Returns final bearing arriving at supplied destination point from this point.
  103. *
  104. * The final bearing will differ from the initial bearing by varying degrees
  105. * according to distance and latitude
  106. *
  107. * @param LatLon $point Latitude/longitude of destination point
  108. * @return float Final bearing in degrees from North
  109. */
  110. public function finalBearingTo(LatLon $point)
  111. {
  112. // get initial bearing from supplied point back to this point...
  113. $lat1 = deg2rad($point->getLat());
  114. $lat2 = deg2rad($this->getLat());
  115. $dLon = deg2rad($this->getLon() - $point->getLon());
  116. $y = sin($dLon) * cos($lat2);
  117. $x = cos($lat1) * sin($lat2) -
  118. sin($lat1) * cos($lat2) * cos($dLon);
  119. $brng = atan2($y, $x);
  120. // ... & reverse it by adding 180°
  121. return fmod(rad2deg($brng) + 180, 360);
  122. }
  123. /**
  124. * Returns the midpoint between this point and the supplied point.
  125. *
  126. * see http://mathforum.org/library/drmath/view/51822.html for derivation
  127. *
  128. * @param LatLon $point Latitude/longitude of destination point
  129. * @return LatLon Midpoint between this point and the supplied point
  130. */
  131. public function midpointTo(LatLon $point)
  132. {
  133. $lat1 = deg2rad($this->getLat());
  134. $lon1 = deg2rad($this->getLon());
  135. $lat2 = deg2rad($point->getLat());
  136. $dLon = deg2rad($point->getLon() - $this->getLon());
  137. $Bx = cos($lat2) * cos($dLon);
  138. $By = cos($lat2) * sin($dLon);
  139. $lat3 = atan2(
  140. sin($lat1) + sin($lat2),
  141. sqrt((cos($lat1) + $Bx) * (cos($lat1) + $Bx) + $By * $By)
  142. );
  143. $lon3 = $lon1 + atan2($By, cos($lat1) + $Bx);
  144. $lon3 = fmod(($lon3 + 3 * pi()), (2 * pi()) - pi()); // normalise to -180..+180º
  145. return new \Bairwell\Geocoder\LatLon(rad2deg($lat3), rad2deg($lon3));
  146. }
  147. /**
  148. * Returns the destination point from this point having travelled the given distance.
  149. *
  150. * (in km) on the given initial bearing (bearing may vary before destination is reached)
  151. *
  152. * see http://williams.best.vwh.net/avform.htm#LL
  153. *
  154. * @param float $brng Initial bearing in degrees
  155. * @param float $dist Distance in km
  156. * @return LatLon Destination point
  157. */
  158. public function destinationPoint($brng, $dist)
  159. {
  160. $dist = $dist / $this->getRadius(); // convert dist to angular distance in radians
  161. $brng = deg2rad($brng);
  162. $lat1 = deg2rad($this->getLat());
  163. $lon1 = deg2rad($this->getLon());
  164. $lat2 = asin(
  165. sin($lat1) * cos($dist) +
  166. cos($lat1) * sin($dist) * cos($brng)
  167. );
  168. $lon2 = $lon1 + atan2(
  169. sin($brng) * sin($dist) * cos($lat1),
  170. cos($dist) - sin($lat1) * sin($lat2)
  171. );
  172. // normalise to -180..+180º
  173. $lon2 = fmod(($lon2 + 3 * pi()), (2 * pi()) - pi());
  174. return new \Bairwell\Geocoder\LatLon(rad2deg($lat2), rad2deg($lon2));
  175. }
  176. /**
  177. * Returns the point of intersection of two paths defined by point and bearing.
  178. *
  179. * see http://williams.best.vwh.net/avform.htm#Intersection
  180. *
  181. * @param LatLon $p1 First point
  182. * @param float $brng1 Initial bearing from first point
  183. * @param LatLon $p2 Second point
  184. * @param float $brng2 Initial bearing from second point
  185. * @return LatLon Destination point (null if no unique intersection defined)
  186. */
  187. public function intersection(LatLon $p1, $brng1, LatLon $p2, $brng2)
  188. {
  189. $lat1 = deg2rad($p1->getLat());
  190. $lon1 = deg2rad($p1->getLon());
  191. $lat2 = deg2rad($p2->getLat());
  192. $lon2 = deg2rad($p2->getLon());
  193. $brng13 = deg2rad($brng1);
  194. $brng23 = deg2rad($brng2);
  195. $dLat = $lat2 - $lat1;
  196. $dLon = $lon2 - $lon1;
  197. $dist12 = 2 * asin(
  198. sqrt(
  199. sin($dLat / 2) * sin($dLat / 2) +
  200. cos($lat1) * cos($lat2) * sin($dLon / 2) * sin($dLon / 2)
  201. )
  202. );
  203. if ($dist12 === 0) {
  204. return NULL;
  205. }
  206. // initial/final bearings between points
  207. $brngA = acos(
  208. (sin($lat2) - sin($lat1) * cos($dist12)) /
  209. (sin($dist12) * cos($lat1))
  210. );
  211. if (FALSE === is_numeric($brngA)) {
  212. // protect against rounding
  213. $brngA = 0;
  214. }
  215. $brngB = acos(
  216. (sin($lat1) - sin($lat2) * cos($dist12)) /
  217. (sin($dist12) * cos($lat2))
  218. );
  219. if (sin($lon2 - $lon1) > 0) {
  220. $brng12 = $brngA;
  221. $brng21 = 2 * pi() - $brngB;
  222. } else {
  223. $brng12 = 2 * pi() - $brngA;
  224. $brng21 = $brngB;
  225. }
  226. $alpha1 = fmod(($brng13 - $brng12 + pi()), (2 * pi())) - pi(); // angle 2-1-3
  227. $alpha2 = fmod(($brng21 - $brng23 + pi()), (2 * pi())) - pi(); // angle 1-2-3
  228. if (sin($alpha1) === 0 && sin($alpha2) === 0) {
  229. // infinite intersections
  230. return NULL;
  231. }
  232. if (sin($alpha1) * sin($alpha2) < 0) {
  233. // ambiguous intersection
  234. return NULL;
  235. }
  236. //alpha1 = Math.abs(alpha1);
  237. //alpha2 = Math.abs(alpha2);
  238. // ... Ed Williams takes abs of alpha1/alpha2, but seems to break calculation?
  239. $alpha3 = acos(
  240. -cos($alpha1) * cos($alpha2) +
  241. sin($alpha1) * sin($alpha2) * cos($dist12)
  242. );
  243. $dist13 = atan2(
  244. sin($dist12) * sin($alpha1) * sin($alpha2),
  245. cos($alpha2) + cos($alpha1) * cos($alpha3)
  246. );
  247. $lat3 = asin(
  248. sin($lat1) * cos($dist13) +
  249. cos($lat1) * sin($dist13) * cos($brng13)
  250. );
  251. $dLon13 = atan2(
  252. sin($brng13) * sin($dist13) * cos($lat1),
  253. cos($dist13) - sin($lat1) * sin($lat3)
  254. );
  255. $lon3 = $lon1 + $dLon13;
  256. $lon3 = fmod(($lon3 + 3 * pi()), (2 * pi())) - pi(); // normalise to -180..+180º
  257. return new \Bairwell\Geocoder\LatLon(rad2deg($lat3), rad2deg($lon3));
  258. }
  259. /**
  260. * Returns the distance from this point to the supplied point, in km, travelling along a rhumb line
  261. *
  262. * see http://williams.best.vwh.net/avform.htm#Rhumb
  263. *
  264. * @param LatLon $point Latitude/longitude of destination point
  265. * @return float Distance in km between this point and destination point
  266. */
  267. public function rhumbDistanceTo(LatLon $point)
  268. {
  269. $R = $this->getRadius();
  270. $lat1 = deg2rad($this->getLat());
  271. $lat2 = deg2rad($point->getLat());
  272. $dLat = deg2rad($point->getLat() - $this->getLat());
  273. $dLon = deg2rad(abs($point->getLon() - $this->getLon()));
  274. $dPhi = log(tan($lat2 / 2 + pi() / 4) / tan($lat1 / 2 + pi() / 4));
  275. // E-W line gives dPhi=0
  276. if (FALSE === is_nan($dLat / $dPhi)) {
  277. $q = $dLat / $dPhi;
  278. } else {
  279. $q = cos($lat1);
  280. }
  281. // if dLon over 180° take shorter rhumb across 180° meridian:
  282. if ($dLon > pi()) {
  283. $dLon = 2 * pi() - $dLon;
  284. }
  285. $dist = sqrt($dLat * $dLat + $q * $q * $dLon * $dLon) * $R;
  286. return FixedPrecision::toPrecisionFixed($dist, 4); // 4 sig figs reflects typical 0.3% accuracy of spherical model
  287. }
  288. /**
  289. * Returns the bearing from this point to the supplied point along a rhumb line, in degrees
  290. *
  291. * @param LatLon $point Latitude/longitude of destination point
  292. * @return float Bearing in degrees from North
  293. */
  294. public function rhumbBearingTo(LatLon $point)
  295. {
  296. $lat1 = deg2rad($this->getLat());
  297. $lat2 = deg2rad($point->getLat());
  298. $dLon = deg2rad($point->getLon() - $this->getLon());
  299. $dPhi = log(tan($lat2 / 2 + pi() / 4) / tan($lat1 / 2 + pi() / 4));
  300. if (abs($dLon) > pi()) {
  301. if ($dLon>0) {
  302. $dLon=-(2 * pi() - $dLon);
  303. } else {
  304. $dLon=(2 * pi() + $dLon);
  305. }
  306. }
  307. $brng = atan2($dLon, $dPhi);
  308. return fmod(rad2deg($brng) + 360, 360);
  309. }
  310. /**
  311. * Returns the destination point from this point having travelled the given distance (in km) on the
  312. * given bearing along a rhumb line
  313. *
  314. * @param float $brng Bearing in degrees from North
  315. * @param float $dist Distance in km
  316. * @return LatLon Destination point
  317. */
  318. public function rhumbDestinationPoint($brng, $dist)
  319. {
  320. $R = $this->getRadius();
  321. $d = floatval($dist) / $R; // d = angular distance covered on earth's surface
  322. $lat1 = deg2rad($this->getLat());
  323. $lon1 = deg2rad($this->getLon());
  324. $brng = deg2rad($brng);
  325. $lat2 = $lat1 + $d * cos($brng);
  326. $dLat = $lat2 - $lat1;
  327. $dPhi = log(tan($lat2 / 2 + pi() / 4) / tan($lat1 / 2 + pi() / 4));
  328. // E-W line gives dPhi=0
  329. if (FALSE === is_nan($dLat / $dPhi)) {
  330. $q = $dLat / $dPhi;
  331. } else {
  332. $q = cos($lat1);
  333. }
  334. $dLon = $d * sin($brng) / $q;
  335. // check for some daft bugger going past the pole
  336. if (abs($lat2) > pi() / 2) {
  337. if ($lat2>0) {
  338. $lat2=pi() - $lat2;
  339. } else {
  340. $lat2=-pi() - $lat2;
  341. }
  342. }
  343. $lon2 = fmod(($lon1 + $dLon + 3 * pi()), (2 * pi())) - pi();
  344. return new \Bairwell\Geocoder\LatLon(rad2deg($lat2), rad2deg($lon2));
  345. }
  346. /**
  347. * Sets latitude in a fluent manner
  348. *
  349. * @param float $lat
  350. * @return LatLon
  351. */
  352. public function setLat($lat)
  353. {
  354. $this->lat = $lat;
  355. return $this;
  356. }
  357. /**
  358. * Gets the latitude
  359. * @return float
  360. */
  361. public function getLat()
  362. {
  363. return $this->lat;
  364. }
  365. /**
  366. * Sets longitude in a fluent manner
  367. *
  368. * @param float $lon
  369. * @return LatLon
  370. */
  371. public function setLon($lon)
  372. {
  373. $this->lon = $lon;
  374. return $this;
  375. }
  376. /**
  377. * Gets the longitude
  378. * @return float
  379. */
  380. public function getLon()
  381. {
  382. return $this->lon;
  383. }
  384. /**
  385. * Sets radius in a fluent manner
  386. *
  387. * @param int $radius
  388. * @return LatLon
  389. */
  390. public function setRadius($radius)
  391. {
  392. $this->radius = $radius;
  393. return $this;
  394. }
  395. /**
  396. * Gets the radius
  397. * @return int
  398. */
  399. public function getRadius()
  400. {
  401. return $this->radius;
  402. }
  403. }