GeocodeLib.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. <?php
  2. App::uses('String', 'Utility');
  3. App::uses('Xml', 'Utility');
  4. App::uses('HttpSocketLib', 'Tools.Lib');
  5. /**
  6. * geocode via google (UPDATE: api3)
  7. * @see DEPRECATED api2: http://code.google.com/intl/de-DE/apis/maps/articles/phpsqlgeocode.html
  8. * @see http://code.google.com/intl/de/apis/maps/documentation/geocoding/#Types
  9. *
  10. * @author Mark Scherer
  11. * @cakephp 2.x
  12. * @licence MIT
  13. * 2010-06-25 ms
  14. */
  15. class GeocodeLib {
  16. const BASE_URL = 'http://{host}/maps/api/geocode/{output}?';
  17. const DEFAULT_HOST = 'de';
  18. const ACC_COUNTRY = 0;
  19. const ACC_AAL1 = 1;
  20. const ACC_AAL2 = 2;
  21. const ACC_AAL3 = 3;
  22. const ACC_POSTAL = 4;
  23. const ACC_LOC = 5;
  24. const ACC_SUBLOC = 6;
  25. const ACC_ROUTE = 7;
  26. const ACC_INTERSEC = 8;
  27. const ACC_STREET = 9;
  28. # First tries with curl, then cake, then php
  29. public $use = array('curl' => true, 'cake'=> true, 'php' => true);
  30. public $units = array('K' => 1.609344, 'N' => 0.868976242, 'F' => 5280, 'I' => 63360, 'M' => 1);
  31. /**
  32. * validation and retrieval options
  33. * - use:
  34. * - log: false logs only real errors, true all activities
  35. * - pause: timeout to prevent blocking
  36. * - ...
  37. *
  38. * 2010-06-25 ms
  39. */
  40. public $options = array(
  41. 'log' => false,
  42. 'pause' => 10000, # in ms
  43. 'min_accuracy' => self::ACC_COUNTRY,
  44. 'allow_inconclusive'=> true,
  45. 'expect' => array(), # see accuracyTypes for details
  46. # static url params
  47. 'output' => 'xml',
  48. 'host' => self::DEFAULT_HOST, # results in maps.google.com - use if you wish to obtain the closest address
  49. );
  50. /**
  51. * url params
  52. * 2010-06-25 ms
  53. */
  54. protected $params = array(
  55. 'address' => '', # either address or latlng required!
  56. 'latlng' => '',
  57. 'region' => '', # country tlds
  58. 'language' => 'de',
  59. 'bounds' => '',
  60. 'sensor' => 'false', # device with gps module sensor
  61. //'key' => '' # not neccessary anymore
  62. );
  63. protected $error = array();
  64. protected $result = null;
  65. /**
  66. * The Maps geocoder is programmed to bias its results depending on from which domain it receives requests. For example, entering "syracuse" in the search box on maps.google.com will geocode the city of "Syracuse, NY", while entering the same query on maps.google.it (Italy's domain) will find the city of "Siracusa" in Sicily. You would get the same results by sending that query through HTTP geocoding to maps.google.it instead of maps.google.com, which you can do by modifying the MAPS_HOST constant in the sample code below. Note: You cannot send a request to a non-existent maps.google.* server, so ensure that a country domain exists before redirecting your geocoding queries to it.
  67. */
  68. protected $hosts = array(
  69. 'us' => 'maps.google.com', # only one for "allow_inconclusive" = true
  70. 'gb' => 'maps.google.co.uk',
  71. 'de' => 'maps.google.de',
  72. 'ch' => 'maps.google.ch',
  73. 'at' => 'maps.google.at',
  74. 'it' => 'maps.google.it',
  75. //ADD MORE - The two-letter codes are iso2 country codes and are mapped to top level domains (ccTLDs)
  76. );
  77. protected $statusCodes = array(
  78. self::CODE_SUCCESS => 'Success',
  79. self::CODE_BAD_REQUEST => 'Sensor param missing',
  80. self::CODE_MISSING_QUERY => 'Adress/LatLng missing',
  81. self::CODE_UNKNOWN_ADDRESS => 'Success, but to address found',
  82. self::CODE_TOO_MANY_QUERIES => 'Limit exceeded',
  83. );
  84. protected $accuracyTypes = array(
  85. self::ACC_COUNTRY => 'country',
  86. self::ACC_AAL1 => 'administrative_area_level_1', # provinces/states
  87. self::ACC_AAL2 => 'administrative_area_level_2 ',
  88. self::ACC_AAL3 => 'administrative_area_level_3',
  89. self::ACC_POSTAL => 'postal_code',
  90. self::ACC_LOC => 'locality',
  91. self::ACC_SUBLOC => 'sublocality',
  92. self::ACC_ROUTE => 'route',
  93. self::ACC_INTERSEC => 'intersection',
  94. self::ACC_STREET => 'street_address'
  95. //neighborhood premise subpremise natural_feature airport park point_of_interest colloquial_area political ?
  96. );
  97. public function __construct($options = array()) {
  98. $this->defaultParams = $this->params;
  99. $this->defaultOptions = $this->options;
  100. if (Configure::read('debug') > 0) {
  101. $this->options['log'] = true;
  102. }
  103. $this->setOptions($options);
  104. }
  105. public function setParams($params) {
  106. foreach ($params as $key => $value) {
  107. if ($key == 'sensor' && $value != 'false' && $value != 'true') {
  108. $value = !empty($value) ? 'true' : 'false';
  109. }
  110. $this->params[$key] = urlencode((string)$value);
  111. }
  112. }
  113. public function setOptions($options) {
  114. foreach ($options as $key => $value) {
  115. if ($key == 'output' && $value != 'xml' && $value != 'json') {
  116. continue;
  117. }
  118. if ($key == 'host' && !array_key_exists($value, $this->hosts)) {
  119. continue;
  120. }
  121. $this->options[$key] = $value;
  122. }
  123. }
  124. public function setError($error) {
  125. if (empty($error)) {
  126. return;
  127. }
  128. $this->error[] = $error;
  129. }
  130. public function error($asString = true, $separator = ', ') {
  131. if (!$asString) {
  132. return $this->error;
  133. }
  134. return implode(', ', $this->error);
  135. }
  136. public function reset($full = true) {
  137. $this->error = array();
  138. $this->result = null;
  139. if ($full) {
  140. $this->params = $this->defaultParams;
  141. $this->options = $this->defaultOptions;
  142. }
  143. }
  144. /**
  145. * build and return url
  146. * 2010-06-29 ms
  147. */
  148. public function url() {
  149. $params = array(
  150. 'host' => $this->hosts[$this->options['host']],
  151. 'output' => $this->options['output']
  152. );
  153. $url = String::insert(self::BASE_URL, $params, array('before'=>'{', 'after'=>'}', 'clean'=>true));
  154. $params = array();
  155. foreach ($this->params as $key => $value) {
  156. if (!empty($value)) {
  157. $params[] = $key.'='.$value;
  158. }
  159. }
  160. return $url.implode('&', $params);
  161. }
  162. public function isInconclusive() {
  163. if ($this->result === null) {
  164. return null;
  165. }
  166. if (!isset($this->result[0])) {
  167. return false;
  168. }
  169. return count($this->result) > 0;
  170. }
  171. /**
  172. * @return array $result
  173. * 2010-06-25 ms
  174. */
  175. public function getResult() {
  176. if ($this->result !== null) {
  177. if (isset($this->result[0])) {
  178. $res = array();
  179. foreach ($this->result as $tmp) {
  180. $res[] = $this->options['output'] == 'json' ? $this->_transformJson($tmp) : $this->_transformXml($tmp);
  181. }
  182. return $res;
  183. }
  184. if ($this->options['output'] == 'json') {
  185. return $this->_transformJson($this->result);
  186. } else {
  187. return $this->_transformXml($this->result);
  188. }
  189. }
  190. return false;
  191. }
  192. /**
  193. * results usually from most accurate to least accurate result (street_address
  194. , ..., country)
  195. * @param float $lat
  196. * @param float $lng
  197. * @param array $options
  198. * - allow_inconclusive
  199. * - min_accuracy
  200. * @return boolean $success
  201. * 2010-06-29 ms
  202. */
  203. public function reverseGeocode($lat, $lng, $settings = array()) {
  204. $this->reset(false);
  205. $latlng = $lat.','.$lng;
  206. $this->setParams(array_merge($settings, array('latlng'=>$latlng)));
  207. $count = 0;
  208. $request_url = $this->url();
  209. while (true) {
  210. $result = $this->_fetch($request_url);
  211. if ($result === false || $result === null) {
  212. $this->setError('Could not retrieve url');
  213. CakeLog::write('geocode', 'Geocoder could not retrieve url with \''.$latlng.'\'');
  214. return false;
  215. }
  216. if ($this->options['output'] == 'json') {
  217. //$res = json_decode($result);
  218. } else {
  219. $res = Xml::build($result);
  220. }
  221. if (!is_object($res)) {
  222. $this->setError('XML parsing failed');
  223. CakeLog::write('geocode', 'Geocoder failed with XML parsing of \''.$latlng.'\'');
  224. return false;
  225. }
  226. $xmlArray = Xml::toArray($res);
  227. $xmlArray = $xmlArray['GeocodeResponse'];
  228. $status = $xmlArray['status'];
  229. if ($status == self::CODE_SUCCESS) {
  230. # validate
  231. if (isset($xmlArray['result'][0]) && !$this->options['allow_inconclusive']) {
  232. $this->setError('Inconclusive result (total of '.count($xmlArray['result']).')');
  233. $this->result = $xmlArray['result'];
  234. return false;
  235. }
  236. if (isset($xmlArray['result'][0])) {
  237. //$xmlArray['result'] = $xmlArray['result'][0];
  238. $accuracy = $this->_parse('type', $xmlArray['result'][0]);
  239. } else {
  240. $accuracy = $this->_parse('type', $xmlArray['result']);
  241. }
  242. if ($this->_isNotAccurateEnough($accuracy)) {
  243. $this->setError('Accuracy not good enough ('.implode(', ', (array)$accuracy).' instead of at least '.$this->accuracyTypes[$this->options['min_accuracy']].')');
  244. $this->result = $xmlArray['result'];
  245. return false;
  246. }
  247. # save Result
  248. if ($this->options['log']) {
  249. CakeLog::write('geocode', 'Address \''.$latlng.'\' has been geocoded');
  250. }
  251. break;
  252. } elseif ($status == self::CODE_TOO_MANY_QUERIES) {
  253. // sent geocodes too fast, delay +0.1 seconds
  254. if ($this->options['log']) {
  255. CakeLog::write('geocode', 'Delay neccessary for \''.$latlng.'\'');
  256. }
  257. $count++;
  258. } else {
  259. # something went wrong
  260. $this->setError('Error '.$status.(isset($this->statusCodes[$status]) ? ' ('.$this->statusCodes[$status].')' : ''));
  261. if ($this->options['log']) {
  262. CakeLog::write('geocode', 'Geocoder could not geocode \''.$latlng.'\'');
  263. }
  264. return false; # for now...
  265. }
  266. if ($count > 5) {
  267. if ($this->options['log']) {
  268. CakeLog::write('geocode', 'Geocoder aborted after too many trials with \''.$latlng.'\'');
  269. }
  270. $this->setError('Too many trials - abort');
  271. return false;
  272. }
  273. $this->pause(true);
  274. }
  275. $this->result = $xmlArray['result'];
  276. return true;
  277. }
  278. /**
  279. * trying to avoid "TOO_MANY_QUERIES" error
  280. * 2010-06-29 ms
  281. */
  282. public function pause($raise = false) {
  283. usleep($this->options['pause']);
  284. if ($raise) {
  285. $this->options['pause'] += 10000;
  286. }
  287. }
  288. /**
  289. * @param string $address
  290. * @param array $settings
  291. * - allow_inconclusive
  292. * - min_accuracy
  293. * @return boolean $success
  294. * 2010-06-25 ms
  295. */
  296. public function geocode($address, $settings = array()) {
  297. $this->reset(false);
  298. $this->setParams(array_merge($settings, array('address'=>$address)));
  299. if ($this->options['allow_inconclusive']) {
  300. # only host working with this setting
  301. //$this->options['host'] = self::DEFAULT_HOST;
  302. }
  303. $count = 0;
  304. $request_url = $this->url();
  305. while (true) {
  306. $result = $this->_fetch($request_url);
  307. if ($result === false || $result === null) {
  308. $this->setError('Could not retrieve url');
  309. CakeLog::write('geocode', 'Geocoder could not retrieve url with \''.$address.'\'');
  310. return false;
  311. }
  312. if ($this->options['output'] == 'json') {
  313. //$res = json_decode($result);
  314. } else {
  315. try {
  316. $res = Xml::build($result);
  317. } catch (Exception $e) {
  318. CakeLog::write('geocode', $e->getMessage());
  319. $res = array();
  320. }
  321. }
  322. if (!is_object($res)) {
  323. $this->setError('XML parsing failed');
  324. CakeLog::write('geocode', 'Geocoder failed with XML parsing of \''.$address.'\'');
  325. return false;
  326. }
  327. $xmlArray = Xml::toArray($res);
  328. $xmlArray = $xmlArray['GeocodeResponse'];
  329. $status = $xmlArray['status'];
  330. if ($status == self::CODE_SUCCESS) {
  331. # validate
  332. if (isset($xmlArray['result'][0]) && !$this->options['allow_inconclusive']) {
  333. $this->setError('Inconclusive result (total of '.count($xmlArray['result']).')');
  334. $this->result = $xmlArray['result'];
  335. return false;
  336. }
  337. if (isset($xmlArray['result'][0])) {
  338. //$xmlArray['result'] = $xmlArray['result'][0];
  339. $accuracy = $this->_parse('type', $xmlArray['result'][0]);
  340. } else {
  341. $accuracy = $this->_parse('type', $xmlArray['result']);
  342. }
  343. //echo returns($accuracy);
  344. if ($this->_isNotAccurateEnough($accuracy)) {
  345. $this->setError('Accuracy not good enough ('.implode(', ', (array)$accuracy).' instead of at least '.$this->accuracyTypes[$this->options['min_accuracy']].')');
  346. $this->result = $xmlArray['result'];
  347. return false;
  348. }
  349. if (!empty($this->options['expect'])) {
  350. $types = array($accuracy); # TODO: maybe check more than just first?
  351. $validExpectation = false;
  352. foreach ($types as $type) {
  353. if (in_array($type, (array)$this->options['expect'])) {
  354. $validExpectation = true;
  355. break;
  356. }
  357. }
  358. if (!$validExpectation) {
  359. $this->setError('Expectation not good reached ('.$accuracy.' instead of '.implode(', ', (array)$this->options['expect']).')');
  360. $this->result = $xmlArray['result'];
  361. return false;
  362. }
  363. }
  364. # save Result
  365. if ($this->options['log']) {
  366. CakeLog::write('geocode', 'Address \''.$address.'\' has been geocoded');
  367. }
  368. break;
  369. } elseif ($status == self::CODE_TOO_MANY_QUERIES) {
  370. // sent geocodes too fast, delay +0.1 seconds
  371. if ($this->options['log']) {
  372. CakeLog::write('geocode', 'Delay neccessary for \''.$address.'\'');
  373. }
  374. $count++;
  375. } else {
  376. # something went wrong
  377. $this->setError('Error '.$status.(isset($this->statusCodes[$status]) ? ' ('.$this->statusCodes[$status].')' : ''));
  378. if ($this->options['log']) {
  379. CakeLog::write('geocode', 'Geocoder could not geocode \''.$address.'\'');
  380. }
  381. return false; # for now...
  382. }
  383. if ($count > 5) {
  384. if ($this->options['log']) {
  385. CakeLog::write('geocode', 'Geocoder aborted after too many trials with \''.$address.'\'');
  386. }
  387. $this->setError('Too many trials - abort');
  388. return false;
  389. }
  390. $this->pause(true);
  391. }
  392. $this->result = $xmlArray['result'];
  393. return true;
  394. }
  395. protected function _isNotAccurateEnough($accuracy = null) {
  396. if ($accuracy === null) {
  397. if (isset($this->result[0])) {
  398. $accuracy = $this->result[0]['type'];
  399. } else {
  400. $accuracy = $this->result['type'];
  401. }
  402. }
  403. if (is_array($accuracy)) {
  404. $accuracy = array_shift($accuracy);
  405. }
  406. if (!in_array($accuracy, $this->accuracyTypes)) {
  407. return null;
  408. }
  409. foreach ($this->accuracyTypes as $key => $type) {
  410. if ($type == $accuracy) {
  411. $accuracy = $key;
  412. break;
  413. }
  414. }
  415. //echo returns($accuracy);
  416. //echo returns('XXX'.$this->options['min_accuracy']);
  417. return $accuracy < $this->options['min_accuracy'];
  418. }
  419. protected function _transformJson($record) {
  420. $res = array();
  421. //TODO
  422. return $res;
  423. }
  424. /**
  425. * try to find the correct path
  426. * - type (string)
  427. * - Type (array[string, ...])
  428. * 2010-06-29 ms
  429. */
  430. protected function _parse($key, $array) {
  431. if (isset($array[$key])) {
  432. return $array[$key];
  433. }
  434. if (isset($array[($key = ucfirst($key))])) {
  435. return $array[$key][0];
  436. }
  437. return null;
  438. }
  439. /**
  440. * flattens result array and returns clean record
  441. * keys:
  442. * - formatted_address, type, country, country_code, country_province, country_province_code, locality, sublocality, postal_code, route, lat, lng, location_type, viewport, bounds
  443. * 2010-06-25 ms
  444. */
  445. protected function _transformXml($record) {
  446. $res = array();
  447. $components = array();
  448. if (!isset($record['address_component'][0])) {
  449. $record['address_component'] = array($record['address_component']);
  450. }
  451. foreach ($record['address_component'] as $c) {
  452. $types = array();
  453. if (isset($c['type'])) { //!is_array($c['Type'])
  454. if (!is_array($c['type'])) {
  455. $c['type'] = (array)$c['type'];
  456. }
  457. $type = $c['type'][0];
  458. array_shift($c['type']);
  459. $types = $c['type'];
  460. } elseif (isset($c['type'])) {
  461. $type = $c['type'];
  462. } else {
  463. # error?
  464. continue;
  465. }
  466. if (array_key_exists($type, $components)) {
  467. $components[$type]['name'] .= ' '.$c['long_name'];
  468. $components[$type]['abbr'] .= ' '.$c['short_name'];
  469. $components[$type]['types'] += $types;
  470. }
  471. $components[$type] = array('name'=>$c['long_name'], 'abbr'=>$c['short_name'], 'types'=>$types);
  472. }
  473. $res['formatted_address'] = $record['formatted_address'];
  474. $res['type'] = $this->_parse('type', $record);
  475. if (array_key_exists('country', $components)) {
  476. $res['country'] = $components['country']['name'];
  477. $res['country_code'] = $components['country']['abbr'];
  478. } else {
  479. $res['country'] = $res['country_code'] = '';
  480. }
  481. if (array_key_exists('administrative_area_level_1', $components)) {
  482. $res['country_province'] = $components['administrative_area_level_1']['name'];
  483. $res['country_province_code'] = $components['administrative_area_level_1']['abbr'];
  484. } else {
  485. $res['country_province'] = $res['country_province_code'] = '';
  486. }
  487. if (array_key_exists('postal_code', $components)) {
  488. $res['postal_code'] = $components['postal_code']['name'];
  489. } else {
  490. $res['postal_code'] = '';
  491. }
  492. if (array_key_exists('locality', $components)) {
  493. $res['locality'] = $components['locality']['name'];
  494. } else {
  495. $res['locality'] = '';
  496. }
  497. if (array_key_exists('sublocality', $components)) {
  498. $res['sublocality'] = $components['sublocality']['name'];
  499. } else {
  500. $res['sublocality'] = '';
  501. }
  502. if (array_key_exists('route', $components)) {
  503. $res['route'] = $components['route']['name'];
  504. if (array_key_exists('street_number', $components)) {
  505. $res['route'] .= ' '.$components['street_number']['name'];
  506. }
  507. } else {
  508. $res['route'] = '';
  509. }
  510. //TODO: add more
  511. $res['lat'] = $record['geometry']['location']['lat'];
  512. $res['lng'] = $record['geometry']['location']['lng'];
  513. $res['location_type'] = $record['geometry']['location_type'];
  514. if (!empty($record['geometry']['viewport'])) {
  515. $res['viewport'] = array('sw'=>$record['geometry']['viewport']['southwest'], 'ne'=>$record['geometry']['viewport']['northeast']);
  516. }
  517. if (!empty($record['geometry']['bounds'])) {
  518. $res['bounds'] = array('sw'=>$record['geometry']['bounds']['southwest'], 'ne'=>$record['geometry']['bounds']['northeast']);
  519. }
  520. # manuell corrections
  521. $array = array(
  522. 'Berlin' => 'BE',
  523. );
  524. if (!empty($res['country_province_code']) && array_key_exists($res['country_province_code'], $array)) {
  525. $res['country_province_code'] = $array[$res['country_province_code']];
  526. }
  527. return $res;
  528. }
  529. /**
  530. * fetches url with curl if available
  531. * fallbacks: cake and php
  532. * note: expects url with json encoded content
  533. * @access private
  534. **/
  535. protected function _fetch($url) {
  536. $this->HttpSocket = new HttpSocketLib($this->use);
  537. if ($res = $this->HttpSocket->fetch($url, 'CakePHP Geocode Lib')) {
  538. return $res;
  539. }
  540. $this->setError($this->HttpSocket->error());
  541. return false;
  542. }
  543. /**
  544. * debugging
  545. * 2009-11-27 ms
  546. */
  547. public function debug() {
  548. return $this->result;
  549. }
  550. /**
  551. * Calculates Distance between two points - each: array('lat'=>x,'lng'=>y)
  552. * DB:
  553. '6371.04 * ACOS( COS( PI()/2 - RADIANS(90 - Retailer.lat)) * ' .
  554. 'COS( PI()/2 - RADIANS(90 - '. $data['Location']['lat'] .')) * ' .
  555. 'COS( RADIANS(Retailer.lng) - RADIANS('. $data['Location']['lng'] .')) + ' .
  556. 'SIN( PI()/2 - RADIANS(90 - Retailer.lat)) * ' .
  557. 'SIN( PI()/2 - RADIANS(90 - '. $data['Location']['lat'] . '))) ' .
  558. 'AS distance'
  559. * @param array pointX
  560. * @param array pointY
  561. * @param float $unit (M=miles, K=kilometers, N=nautical miles, I=inches, F=feet)
  562. * @return int distance: in km
  563. * 2009-03-06 ms
  564. */
  565. public function distance($pointX, $pointY, $unit = null) {
  566. if (empty($unit) || !array_key_exists(($unit = strtoupper($unit)), $this->units)) {
  567. $unit = array_keys($this->units);
  568. $unit = $unit[0];
  569. }
  570. /*
  571. $res = 6371.04 * ACOS( COS( PI()/2 - rad2deg(90 - $pointX['lat'])) *
  572. COS( PI()/2 - rad2deg(90 - $pointY['lat'])) *
  573. COS( rad2deg($pointX['lng']) - rad2deg($pointY['lng'])) +
  574. SIN( PI()/2 - rad2deg(90 - $pointX['lat'])) *
  575. SIN( PI()/2 - rad2deg(90 - $pointY['lat'])));
  576. $res = 6371.04 * acos(sin($pointY['lat'])*sin($pointX['lat'])+cos($pointY['lat'])*cos($pointX['lat'])*cos($pointY['lng'] - $pointX['lng']));
  577. */
  578. # seems to be the only working one (although slightly incorrect...)
  579. $res = 69.09 * rad2deg(acos(sin(deg2rad($pointX['lat'])) * sin(deg2rad($pointY['lat'])) + cos(deg2rad($pointX['lat'])) * cos(deg2rad($pointY['lat'])) * cos(deg2rad($pointX['lng'] - $pointY['lng']))));
  580. if (isset($this->units[$unit])) {
  581. $res *= $this->units[$unit];
  582. }
  583. return ceil($res);
  584. }
  585. public function convert($value, $fromUnit, $toUnit) {
  586. if (!isset($this->units[($fromUnit = strtoupper($fromUnit))]) || !isset($this->units[($toUnit = strtoupper($toUnit))])) {
  587. return false;
  588. }
  589. if ($fromUnit == 'M') {
  590. $value *= $this->units[$toUnit];
  591. } elseif ($toUnit == 'M') {
  592. $value /= $this->units[$fromUnit];
  593. } else {
  594. $value /= $this->units[$fromUnit];
  595. $value *= $this->units[$toUnit];
  596. }
  597. return $value;
  598. }
  599. /**
  600. * Fuzziness filter for coordinates (lat or lng).
  601. * Useful if you store other users' locations and want to grant some
  602. * privacy protection. This way the coordinates will be slightly modified.
  603. *
  604. * @param float coord
  605. * @param int level (0 = nothing to 5 = extrem)
  606. * - 1:
  607. * - 2:
  608. * - 3:
  609. * - 4:
  610. * - 5:
  611. * 2011-03-16 ms
  612. */
  613. public static function blur($coord, $level = 0) {
  614. if (!$level) {
  615. return $coord;
  616. }
  617. //TODO:
  618. switch ($level) {
  619. case 1:
  620. break;
  621. case 2:
  622. break;
  623. case 3:
  624. break;
  625. case 4:
  626. break;
  627. case 5:
  628. break;
  629. default:
  630. # wrong level, return value
  631. trigger_error('Invalid level \''.h($level).'\'');
  632. return $coord;
  633. }
  634. $scrambleVal = 0.000001 * mt_rand(1000,2000) * (mt_rand(0,1) === 0 ? 1 : -1);
  635. return ($coord + $scrambleVal);
  636. //$scrambleVal *= (mt_rand(0,1) === 0 ? 1 : 2);
  637. //$scrambleVal *= (float)(2^$level);
  638. //die($scrambleVal.' '.$coord);
  639. # TODO: + - by chance!!!
  640. return $coord + $scrambleVal;
  641. }
  642. const TYPE_ROOFTOP = 'ROOFTOP';
  643. const TYPE_RANGE_INTERPOLATED = 'RANGE_INTERPOLATED';
  644. const TYPE_GEOMETRIC_CENTER = 'GEOMETRIC_CENTER';
  645. const TYPE_APPROXIMATE = 'APPROXIMATE';
  646. const CODE_SUCCESS = 'OK'; //200;
  647. const CODE_TOO_MANY_QUERIES = 'OVER_QUERY_LIMIT'; //620;
  648. const CODE_BAD_REQUEST = 'REQUEST_DENIED'; //400;
  649. const CODE_MISSING_QUERY = 'INVALID_REQUEST';//601;
  650. const CODE_UNKNOWN_ADDRESS = 'ZERO_RESULTS'; //602;
  651. /*
  652. const CODE_SERVER_ERROR = 500;
  653. const CODE_UNAVAILABLE_ADDRESS = 603;
  654. const CODE_UNKNOWN_DIRECTIONS = 604;
  655. const CODE_BAD_KEY = 610;
  656. */
  657. }
  658. /*
  659. TODO:
  660. http://code.google.com/intl/de-DE/apis/maps/documentation/geocoding/
  661. - whats the difference to "http://maps.google.com/maps/api/geocode/output?parameters"
  662. */
  663. /*
  664. Example: NEW:
  665. Array
  666. (
  667. [status] => OK
  668. [Result] => Array
  669. (
  670. [type] => postal_code
  671. [formatted_address] => 74523, Deutschland
  672. [AddressComponent] => Array
  673. (
  674. [0] => Array
  675. (
  676. [long_name] => 74523
  677. [short_name] => 74523
  678. [type] => postal_code
  679. )
  680. [1] => Array
  681. (
  682. [long_name] => Schwaebisch Hall
  683. [short_name] => SHA
  684. [Type] => Array
  685. (
  686. [0] => administrative_area_level_2
  687. [1] => political
  688. )
  689. )
  690. [2] => Array
  691. (
  692. [long_name] => Baden-Wuerttemberg
  693. [short_name] => BW
  694. [Type] => Array
  695. (
  696. [0] => administrative_area_level_1
  697. [1] => political
  698. )
  699. )
  700. [3] => Array
  701. (
  702. [long_name] => Deutschland
  703. [short_name] => DE
  704. [Type] => Array
  705. (
  706. [0] => country
  707. [1] => political
  708. )
  709. )
  710. )
  711. [Geometry] => Array
  712. (
  713. [Location] => Array
  714. (
  715. [lat] => 49.1257616
  716. [lng] => 9.7544127
  717. )
  718. [location_type] => APPROXIMATE
  719. [Viewport] => Array
  720. (
  721. [Southwest] => Array
  722. (
  723. [lat] => 49.0451477
  724. [lng] => 9.6132550
  725. )
  726. [Northeast] => Array
  727. (
  728. [lat] => 49.1670260
  729. [lng] => 9.8756350
  730. )
  731. )
  732. [Bounds] => Array
  733. (
  734. [Southwest] => Array
  735. (
  736. [lat] => 49.0451477
  737. [lng] => 9.6132550
  738. )
  739. [Northeast] => Array
  740. (
  741. [lat] => 49.1670260
  742. [lng] => 9.8756350
  743. )
  744. )
  745. )
  746. )
  747. )
  748. Example OLD:
  749. Array
  750. (
  751. [name] => 74523 Deutschland
  752. [Status] => Array
  753. (
  754. [code] => 200
  755. [request] => geocode
  756. )
  757. [Result] => Array
  758. (
  759. [id] => p1
  760. [address] => 74523, Deutschland
  761. [AddressDetails] => Array
  762. (
  763. [Accuracy] => 5
  764. [xmlns] => urn:oasis:names:tc:ciq:xsdschema:xAL:2.0
  765. [Country] => Array
  766. (
  767. [CountryNameCode] => DE
  768. [CountryName] => Deutschland
  769. [AdministrativeArea] => Array
  770. (
  771. [AdministrativeAreaName] => Baden-Wuerttemberg
  772. [SubAdministrativeArea] => Array
  773. (
  774. [SubAdministrativeAreaName] => Schwaebisch Hall
  775. [PostalCode] => Array
  776. (
  777. [PostalCodeNumber] => 74523
  778. )
  779. )
  780. )
  781. )
  782. )
  783. [ExtendedData] => Array
  784. (
  785. [LatLonBox] => Array
  786. (
  787. [north] => 49.1670260
  788. [south] => 49.0451477
  789. [east] => 9.8756350
  790. [west] => 9.6132550
  791. )
  792. )
  793. [Point] => Array
  794. (
  795. [coordinates] => 9.7544127,49.1257616,0
  796. )
  797. )
  798. ) {
  799. "status": "OK",
  800. "results": [ {
  801. "types": [ "street_address" ],
  802. "formatted_address": "Krebenweg 20, 74523 Schwäbisch Hall, Deutschland",
  803. "address_components": [ {
  804. "long_name": "20",
  805. "short_name": "20",
  806. "types": [ "street_number" ]
  807. }, {
  808. "long_name": "Krebenweg",
  809. "short_name": "Krebenweg",
  810. "types": [ "route" ]
  811. }, {
  812. "long_name": "Bibersfeld",
  813. "short_name": "Bibersfeld",
  814. "types": [ "sublocality", "political" ]
  815. }, {
  816. "long_name": "Schwäbisch Hall",
  817. "short_name": "Schwäbisch Hall",
  818. "types": [ "locality", "political" ]
  819. }, {
  820. "long_name": "Schwäbisch Hall",
  821. "short_name": "SHA",
  822. "types": [ "administrative_area_level_2", "political" ]
  823. }, {
  824. "long_name": "Baden-Württemberg",
  825. "short_name": "BW",
  826. "types": [ "administrative_area_level_1", "political" ]
  827. }, {
  828. "long_name": "Deutschland",
  829. "short_name": "DE",
  830. "types": [ "country", "political" ]
  831. }, {
  832. "long_name": "74523",
  833. "short_name": "74523",
  834. "types": [ "postal_code" ]
  835. } ],
  836. "geometry": {
  837. "location": {
  838. "lat": 49.0817369,
  839. "lng": 9.6908451
  840. },
  841. "location_type": "RANGE_INTERPOLATED", //ROOFTOP //APPROXIMATE
  842. "viewport": {
  843. "southwest": {
  844. "lat": 49.0785954,
  845. "lng": 9.6876999
  846. },
  847. "northeast": {
  848. "lat": 49.0848907,
  849. "lng": 9.6939951
  850. }
  851. },
  852. "bounds": {
  853. "southwest": {
  854. "lat": 49.0817369,
  855. "lng": 9.6908451
  856. },
  857. "northeast": {
  858. "lat": 49.0817492,
  859. "lng": 9.6908499
  860. }
  861. }
  862. },
  863. "partial_match": true
  864. } ]
  865. }
  866. */