GeocodeLib.php 27 KB

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