GeocodeLib.php 27 KB

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