GeocodeLib.php 26 KB

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