]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Geonames/GeonamesPlugin.php
[TRANSLATION] Update license and copyright notice in translation files
[quix0rs-gnu-social.git] / plugins / Geonames / GeonamesPlugin.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Plugin to convert string locations to Geonames IDs and vice versa
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Action
23  * @package   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @copyright 2009 StatusNet Inc.
26  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27  * @link      http://status.net/
28  */
29
30 if (!defined('STATUSNET')) {
31     exit(1);
32 }
33
34 /**
35  * Plugin to convert string locations to Geonames IDs and vice versa
36  *
37  * This handles most of the events that Location class emits. It uses
38  * the geonames.org Web service to convert names like 'Montreal, Quebec, Canada'
39  * into IDs and lat/lon pairs.
40  *
41  * @category Plugin
42  * @package  StatusNet
43  * @author   Evan Prodromou <evan@status.net>
44  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
45  * @link     http://status.net/
46  *
47  * @seeAlso  Location
48  */
49 class GeonamesPlugin extends Plugin
50 {
51     const PLUGIN_VERSION = '2.0.0';
52
53     const LOCATION_NS = 1;
54
55     public $host     = 'ws.geonames.org';
56     public $username = null;
57     public $token    = null;
58     public $expiry   = 7776000; // 90-day expiry
59     public $timeout  = 2;       // Web service timeout in seconds.
60     public $timeoutWindow = 60; // Further lookups in this process will be disabled for N seconds after a timeout.
61     public $cachePrefix = null; // Optional shared memcache prefix override
62                                 // to share lookups between local instances.
63
64     protected $lastTimeout = null; // timestamp of last web service timeout
65
66     /**
67      * convert a name into a Location object
68      *
69      * @param string   $name      Name to convert
70      * @param string   $language  ISO code for anguage the name is in
71      * @param Location &$location Location object (may be null)
72      *
73      * @return boolean whether to continue (results in $location)
74      */
75     function onLocationFromName($name, $language, &$location)
76     {
77         $loc = $this->getCache(array('name' => $name,
78                                      'language' => $language));
79
80         if ($loc !== false) {
81             $location = $loc;
82             return false;
83         }
84
85         try {
86             $geonames = $this->getGeonames('search',
87                                            array('maxRows' => 1,
88                                                  'q' => $name,
89                                                  'lang' => $language,
90                                                  'type' => 'xml'));
91         } catch (Exception $e) {
92             $this->log(LOG_WARNING, "Error for $name: " . $e->getMessage());
93             return true;
94         }
95
96         if (count($geonames) == 0) {
97             // no results
98             $this->setCache(array('name' => $name,
99                                   'language' => $language),
100                             null);
101             return true;
102         }
103
104         $n = $geonames[0];
105
106         $location = new Location();
107
108         $location->lat              = $this->canonical($n->lat);
109         $location->lon              = $this->canonical($n->lng);
110         $location->names[$language] = (string)$n->name;
111         $location->location_id      = (string)$n->geonameId;
112         $location->location_ns      = self::LOCATION_NS;
113
114         $this->setCache(array('name' => $name,
115                               'language' => $language),
116                         $location);
117
118         // handled, don't continue processing!
119         return false;
120     }
121
122     /**
123      * convert an id into a Location object
124      *
125      * @param string   $id        Name to convert
126      * @param string   $ns        Name to convert
127      * @param string   $language  ISO code for language for results
128      * @param Location &$location Location object (may be null)
129      *
130      * @return boolean whether to continue (results in $location)
131      */
132     function onLocationFromId($id, $ns, $language, &$location)
133     {
134         if ($ns != self::LOCATION_NS) {
135             // It's not one of our IDs... keep processing
136             return true;
137         }
138
139         $loc = $this->getCache(array('id' => $id));
140
141         if ($loc !== false) {
142             $location = $loc;
143             return false;
144         }
145
146         try {
147             $geonames = $this->getGeonames('hierarchy',
148                                            array('geonameId' => $id,
149                                                  'lang' => $language));
150         } catch (Exception $e) {
151             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
152             return false;
153         }
154
155         $parts = array();
156
157         foreach ($geonames as $level) {
158             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
159                 $parts[] = (string)$level->name;
160             }
161         }
162
163         $last = $geonames[count($geonames)-1];
164
165         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
166             $parts[] = (string)$last->name;
167         }
168
169         $location = new Location();
170
171         $location->location_id      = (string)$last->geonameId;
172         $location->location_ns      = self::LOCATION_NS;
173         $location->lat              = $this->canonical($last->lat);
174         $location->lon              = $this->canonical($last->lng);
175
176         $location->names[$language] = implode(', ', array_reverse($parts));
177
178         $this->setCache(array('id' => (string)$last->geonameId),
179                         $location);
180
181         // We're responsible for this namespace; nobody else
182         // can resolve it
183
184         return false;
185     }
186
187     /**
188      * convert a lat/lon pair into a Location object
189      *
190      * Given a lat/lon, we try to find a Location that's around
191      * it or nearby. We prefer populated places (cities, towns, villages).
192      *
193      * @param string   $lat       Latitude
194      * @param string   $lon       Longitude
195      * @param string   $language  ISO code for language for results
196      * @param Location &$location Location object (may be null)
197      *
198      * @return boolean whether to continue (results in $location)
199      */
200     function onLocationFromLatLon($lat, $lon, $language, &$location)
201     {
202         // Make sure they're canonical
203
204         $lat = $this->canonical($lat);
205         $lon = $this->canonical($lon);
206
207         $loc = $this->getCache(array('lat' => $lat,
208                                      'lon' => $lon));
209
210         if ($loc !== false) {
211             $location = $loc;
212             return false;
213         }
214
215         try {
216           $geonames = $this->getGeonames('findNearbyPlaceName',
217                                          array('lat' => $lat,
218                                                'lng' => $lon,
219                                                'lang' => $language));
220         } catch (Exception $e) {
221             $this->log(LOG_WARNING, "Error for coords $lat, $lon: " . $e->getMessage());
222             return true;
223         }
224
225         if (count($geonames) == 0) {
226             // no results
227             $this->setCache(array('lat' => $lat,
228                                   'lon' => $lon),
229                             null);
230             return true;
231         }
232
233         $n = $geonames[0];
234
235         $parts = array();
236
237         $location = new Location();
238
239         $parts[] = (string)$n->name;
240
241         if (!empty($n->adminName1)) {
242             $parts[] = (string)$n->adminName1;
243         }
244
245         if (!empty($n->countryName)) {
246             $parts[] = (string)$n->countryName;
247         }
248
249         $location->location_id = (string)$n->geonameId;
250         $location->location_ns = self::LOCATION_NS;
251         $location->lat         = $this->canonical($n->lat);
252         $location->lon         = $this->canonical($n->lng);
253
254         $location->names[$language] = implode(', ', $parts);
255
256         $this->setCache(array('lat' => $lat,
257                               'lon' => $lon),
258                         $location);
259
260         // Success! We handled it, so no further processing
261
262         return false;
263     }
264
265     /**
266      * Human-readable name for a location
267      *
268      * Given a location, we try to retrieve a human-readable name
269      * in the target language.
270      *
271      * @param Location $location Location to get the name for
272      * @param string   $language ISO code for language to find name in
273      * @param string   &$name    Place to put the name
274      *
275      * @return boolean whether to continue
276      */
277     function onLocationNameLanguage($location, $language, &$name)
278     {
279         if ($location->location_ns != self::LOCATION_NS) {
280             // It's not one of our IDs... keep processing
281             return true;
282         }
283
284         $id = $location->location_id;
285
286         $n = $this->getCache(array('id' => $id,
287                                    'language' => $language));
288
289         if ($n !== false) {
290             $name = $n;
291             return false;
292         }
293
294         try {
295             $geonames = $this->getGeonames('hierarchy',
296                                            array('geonameId' => $id,
297                                                  'lang' => $language));
298         } catch (Exception $e) {
299             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
300             return false;
301         }
302
303         if (count($geonames) == 0) {
304             $this->setCache(array('id' => $id,
305                                   'language' => $language),
306                             null);
307             return false;
308         }
309
310         $parts = array();
311
312         foreach ($geonames as $level) {
313             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
314                 $parts[] = (string)$level->name;
315             }
316         }
317
318         $last = $geonames[count($geonames)-1];
319
320         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
321             $parts[] = (string)$last->name;
322         }
323
324         if (count($parts)) {
325             $name = implode(', ', array_reverse($parts));
326             $this->setCache(array('id' => $id,
327                                   'language' => $language),
328                             $name);
329         }
330
331         return false;
332     }
333
334     /**
335      * Human-readable URL for a location
336      *
337      * Given a location, we try to retrieve a geonames.org URL.
338      *
339      * @param Location $location Location to get the url for
340      * @param string   &$url     Place to put the url
341      *
342      * @return boolean whether to continue
343      */
344     function onLocationUrl($location, &$url)
345     {
346         if ($location->location_ns != self::LOCATION_NS) {
347             // It's not one of our IDs... keep processing
348             return true;
349         }
350
351         $url = 'http://www.geonames.org/' . $location->location_id;
352
353         // it's been filled, so don't process further.
354         return false;
355     }
356
357     /**
358      * Machine-readable name for a location
359      *
360      * Given a location, we try to retrieve a geonames.org URL.
361      *
362      * @param Location $location Location to get the url for
363      * @param string   &$url     Place to put the url
364      *
365      * @return boolean whether to continue
366      */
367     function onLocationRdfUrl($location, &$url)
368     {
369         if ($location->location_ns != self::LOCATION_NS) {
370             // It's not one of our IDs... keep processing
371             return true;
372         }
373
374         $url = 'http://sws.geonames.org/' . $location->location_id . '/';
375
376         // it's been filled, so don't process further.
377         return false;
378     }
379
380     function getCache($attrs)
381     {
382         $c = Cache::instance();
383
384         if (empty($c)) {
385             return null;
386         }
387
388         $key = $this->cacheKey($attrs);
389
390         $value = $c->get($key);
391
392         return $value;
393     }
394
395     function setCache($attrs, $loc)
396     {
397         $c = Cache::instance();
398
399         if (empty($c)) {
400             return null;
401         }
402
403         $key = $this->cacheKey($attrs);
404
405         $result = $c->set($key, $loc, 0, time() + $this->expiry);
406
407         return $result;
408     }
409
410     function cacheKey($attrs)
411     {
412         $key = 'geonames:' .
413                implode(',', array_keys($attrs)) . ':'.
414                Cache::keyize(implode(',', array_values($attrs)));
415         if ($this->cachePrefix) {
416             return $this->cachePrefix . ':' . $key;
417         } else {
418             return Cache::key($key);
419         }
420     }
421
422     function wsUrl($method, $params)
423     {
424         if (!empty($this->username)) {
425             $params['username'] = $this->username;
426         }
427
428         if (!empty($this->token)) {
429             $params['token'] = $this->token;
430         }
431
432         $str = http_build_query($params, null, '&');
433
434         return 'http://'.$this->host.'/'.$method.'?'.$str;
435     }
436
437     function getGeonames($method, $params)
438     {
439         if ($this->lastTimeout && (time() - $this->lastTimeout < $this->timeoutWindow)) {
440             // TRANS: Exception thrown when a geo names service is not used because of a recent timeout.
441             throw new Exception(_m('Skipping due to recent web service timeout.'));
442         }
443
444         $client = HTTPClient::start();
445         $client->setConfig('connect_timeout', $this->timeout);
446         $client->setConfig('timeout', $this->timeout);
447
448         try {
449             $result = $client->get($this->wsUrl($method, $params));
450         } catch (Exception $e) {
451             common_log(LOG_ERR, __METHOD__ . ": " . $e->getMessage());
452             $this->lastTimeout = time();
453             throw $e;
454         }
455
456         if (!$result->isOk()) {
457             // TRANS: Exception thrown when a geo names service does not return an expected response.
458             // TRANS: %s is an HTTP error code.
459             throw new Exception(sprintf(_m('HTTP error code %s.'),$result->getStatus()));
460         }
461
462         $body = $result->getBody();
463
464         if (empty($body)) {
465             // TRANS: Exception thrown when a geo names service returns an empty body.
466             throw new Exception(_m('Empty HTTP body in response.'));
467         }
468
469         // This will throw an exception if the XML is mal-formed
470
471         $document = new SimpleXMLElement($body);
472
473         // No children, usually no results
474
475         $children = $document->children();
476
477         if (count($children) == 0) {
478             return array();
479         }
480
481         if (isset($document->status)) {
482             // TRANS: Exception thrown when a geo names service return a specific error number and error text.
483             // TRANS: %1$s is an error code, %2$s is an error message.
484             throw new Exception(sprintf(_m('Error #%1$s ("%2$s").'),$document->status['value'],$document->status['message']));
485         }
486
487         // Array of elements, >0 elements
488
489         return $document->geoname;
490     }
491
492     function onPluginVersion(array &$versions)
493     {
494         $versions[] = array('name' => 'Geonames',
495                             'version' => self::PLUGIN_VERSION,
496                             'author' => 'Evan Prodromou',
497                             'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Geonames',
498                             'rawdescription' =>
499                             // TRANS: Plugin description.
500                             _m('Uses <a href="http://geonames.org/">Geonames</a> service to get human-readable '.
501                                'names for locations based on user-provided lat/long pairs.'));
502         return true;
503     }
504
505     function canonical($coord)
506     {
507         $coord = rtrim($coord, "0");
508         $coord = rtrim($coord, ".");
509
510         return $coord;
511     }
512 }