]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/GeonamesPlugin.php
Merge branch 'master' of gitorious.org:statusnet/mainline into testing
[quix0rs-gnu-social.git] / plugins / 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
50 class GeonamesPlugin extends Plugin
51 {
52     const LOCATION_NS = 1;
53
54     public $host     = 'ws.geonames.org';
55     public $username = null;
56     public $token    = null;
57     public $expiry   = 7776000; // 90-day expiry
58
59     /**
60      * convert a name into a Location object
61      *
62      * @param string   $name      Name to convert
63      * @param string   $language  ISO code for anguage the name is in
64      * @param Location &$location Location object (may be null)
65      *
66      * @return boolean whether to continue (results in $location)
67      */
68
69     function onLocationFromName($name, $language, &$location)
70     {
71         $loc = $this->getCache(array('name' => $name,
72                                      'language' => $language));
73
74         if ($loc !== false) {
75             $location = $loc;
76             return false;
77         }
78
79         try {
80             $geonames = $this->getGeonames('search',
81                                            array('maxRows' => 1,
82                                                  'q' => $name,
83                                                  'lang' => $language,
84                                                  'type' => 'xml'));
85         } catch (Exception $e) {
86             $this->log(LOG_WARNING, "Error for $name: " . $e->getMessage());
87             return true;
88         }
89
90         if (count($geonames) == 0) {
91             // no results
92             $this->setCache(array('name' => $name,
93                                   'language' => $language),
94                             null);
95             return true;
96         }
97
98         $n = $geonames[0];
99
100         $location = new Location();
101
102         $location->lat              = $this->canonical($n->lat);
103         $location->lon              = $this->canonical($n->lng);
104         $location->names[$language] = (string)$n->name;
105         $location->location_id      = (string)$n->geonameId;
106         $location->location_ns      = self::LOCATION_NS;
107
108         $this->setCache(array('name' => $name,
109                               'language' => $language),
110                         $location);
111
112         // handled, don't continue processing!
113         return false;
114     }
115
116     /**
117      * convert an id into a Location object
118      *
119      * @param string   $id        Name to convert
120      * @param string   $ns        Name to convert
121      * @param string   $language  ISO code for language for results
122      * @param Location &$location Location object (may be null)
123      *
124      * @return boolean whether to continue (results in $location)
125      */
126
127     function onLocationFromId($id, $ns, $language, &$location)
128     {
129         if ($ns != self::LOCATION_NS) {
130             // It's not one of our IDs... keep processing
131             return true;
132         }
133
134         $loc = $this->getCache(array('id' => $id));
135
136         if ($loc !== false) {
137             $location = $loc;
138             return false;
139         }
140
141         try {
142             $geonames = $this->getGeonames('hierarchy',
143                                            array('geonameId' => $id,
144                                                  'lang' => $language));
145         } catch (Exception $e) {
146             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
147             return false;
148         }
149
150         $parts = array();
151
152         foreach ($geonames as $level) {
153             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
154                 $parts[] = (string)$level->name;
155             }
156         }
157
158         $last = $geonames[count($geonames)-1];
159
160         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
161             $parts[] = (string)$last->name;
162         }
163
164         $location = new Location();
165
166         $location->location_id      = (string)$last->geonameId;
167         $location->location_ns      = self::LOCATION_NS;
168         $location->lat              = $this->canonical($last->lat);
169         $location->lon              = $this->canonical($last->lng);
170
171         $location->names[$language] = implode(', ', array_reverse($parts));
172
173         $this->setCache(array('id' => (string)$last->geonameId),
174                         $location);
175
176         // We're responsible for this namespace; nobody else
177         // can resolve it
178
179         return false;
180     }
181
182     /**
183      * convert a lat/lon pair into a Location object
184      *
185      * Given a lat/lon, we try to find a Location that's around
186      * it or nearby. We prefer populated places (cities, towns, villages).
187      *
188      * @param string   $lat       Latitude
189      * @param string   $lon       Longitude
190      * @param string   $language  ISO code for language for results
191      * @param Location &$location Location object (may be null)
192      *
193      * @return boolean whether to continue (results in $location)
194      */
195
196     function onLocationFromLatLon($lat, $lon, $language, &$location)
197     {
198         // Make sure they're canonical
199
200         $lat = $this->canonical($lat);
201         $lon = $this->canonical($lon);
202
203         $loc = $this->getCache(array('lat' => $lat,
204                                      'lon' => $lon));
205
206         if ($loc !== false) {
207             $location = $loc;
208             return false;
209         }
210
211         try {
212           $geonames = $this->getGeonames('findNearbyPlaceName',
213                                          array('lat' => $lat,
214                                                'lng' => $lon,
215                                                'lang' => $language));
216         } catch (Exception $e) {
217             $this->log(LOG_WARNING, "Error for coords $lat, $lon: " . $e->getMessage());
218             return true;
219         }
220
221         if (count($geonames) == 0) {
222             // no results
223             $this->setCache(array('lat' => $lat,
224                                   'lon' => $lon),
225                             null);
226             return true;
227         }
228
229         $n = $geonames[0];
230
231         $parts = array();
232
233         $location = new Location();
234
235         $parts[] = (string)$n->name;
236
237         if (!empty($n->adminName1)) {
238             $parts[] = (string)$n->adminName1;
239         }
240
241         if (!empty($n->countryName)) {
242             $parts[] = (string)$n->countryName;
243         }
244
245         $location->location_id = (string)$n->geonameId;
246         $location->location_ns = self::LOCATION_NS;
247         $location->lat         = $this->canonical($n->lat);
248         $location->lon         = $this->canonical($n->lng);
249
250         $location->names[$language] = implode(', ', $parts);
251
252         $this->setCache(array('lat' => $lat,
253                               'lon' => $lon),
254                         $location);
255
256         // Success! We handled it, so no further processing
257
258         return false;
259     }
260
261     /**
262      * Human-readable name for a location
263      *
264      * Given a location, we try to retrieve a human-readable name
265      * in the target language.
266      *
267      * @param Location $location Location to get the name for
268      * @param string   $language ISO code for language to find name in
269      * @param string   &$name    Place to put the name
270      *
271      * @return boolean whether to continue
272      */
273
274     function onLocationNameLanguage($location, $language, &$name)
275     {
276         if ($location->location_ns != self::LOCATION_NS) {
277             // It's not one of our IDs... keep processing
278             return true;
279         }
280
281         $id = $location->location_id;
282
283         $n = $this->getCache(array('id' => $id,
284                                    'language' => $language));
285
286         if ($n !== false) {
287             $name = $n;
288             return false;
289         }
290
291         try {
292             $geonames = $this->getGeonames('hierarchy',
293                                            array('geonameId' => $id,
294                                                  'lang' => $language));
295         } catch (Exception $e) {
296             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
297             return false;
298         }
299
300         if (count($geonames) == 0) {
301             $this->setCache(array('id' => $id,
302                                   'language' => $language),
303                             null);
304             return false;
305         }
306
307         $parts = array();
308
309         foreach ($geonames as $level) {
310             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
311                 $parts[] = (string)$level->name;
312             }
313         }
314
315         $last = $geonames[count($geonames)-1];
316
317         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
318             $parts[] = (string)$last->name;
319         }
320
321         if (count($parts)) {
322             $name = implode(', ', array_reverse($parts));
323             $this->setCache(array('id' => $id,
324                                   'language' => $language),
325                             $name);
326         }
327
328         return false;
329     }
330
331     /**
332      * Human-readable URL for a location
333      *
334      * Given a location, we try to retrieve a geonames.org URL.
335      *
336      * @param Location $location Location to get the url for
337      * @param string   &$url     Place to put the url
338      *
339      * @return boolean whether to continue
340      */
341
342     function onLocationUrl($location, &$url)
343     {
344         if ($location->location_ns != self::LOCATION_NS) {
345             // It's not one of our IDs... keep processing
346             return true;
347         }
348
349         $url = 'http://www.geonames.org/' . $location->location_id;
350
351         // it's been filled, so don't process further.
352         return false;
353     }
354
355     /**
356      * Machine-readable name for a location
357      *
358      * Given a location, we try to retrieve a geonames.org URL.
359      *
360      * @param Location $location Location to get the url for
361      * @param string   &$url     Place to put the url
362      *
363      * @return boolean whether to continue
364      */
365
366     function onLocationRdfUrl($location, &$url)
367     {
368         if ($location->location_ns != self::LOCATION_NS) {
369             // It's not one of our IDs... keep processing
370             return true;
371         }
372
373         $url = 'http://sw.geonames.org/' . $location->location_id . '/';
374
375         // it's been filled, so don't process further.
376         return false;
377     }
378
379     function getCache($attrs)
380     {
381         $c = common_memcache();
382
383         if (empty($c)) {
384             return null;
385         }
386
387         $key = $this->cacheKey($attrs);
388
389         $value = $c->get($key);
390
391         return $value;
392     }
393
394     function setCache($attrs, $loc)
395     {
396         $c = common_memcache();
397
398         if (empty($c)) {
399             return null;
400         }
401
402         $key = $this->cacheKey($attrs);
403
404         $result = $c->set($key, $loc, 0, time() + $this->expiry);
405
406         return $result;
407     }
408
409     function cacheKey($attrs)
410     {
411         return common_cache_key('geonames:'.
412                                 implode(',', array_keys($attrs)) . ':'.
413                                 common_keyize(implode(',', array_values($attrs))));
414     }
415
416     function wsUrl($method, $params)
417     {
418         if (!empty($this->username)) {
419             $params['username'] = $this->username;
420         }
421
422         if (!empty($this->token)) {
423             $params['token'] = $this->token;
424         }
425
426         $str = http_build_query($params, null, '&');
427
428         return 'http://'.$this->host.'/'.$method.'?'.$str;
429     }
430
431     function getGeonames($method, $params)
432     {
433         $client = HTTPClient::start();
434
435         $result = $client->get($this->wsUrl($method, $params));
436
437         if (!$result->isOk()) {
438             throw new Exception("HTTP error code " . $result->code);
439         }
440
441         $body = $result->getBody();
442
443         if (empty($body)) {
444             throw new Exception("Empty HTTP body in response");
445         }
446
447         // This will throw an exception if the XML is mal-formed
448
449         $document = new SimpleXMLElement($body);
450
451         // No children, usually no results
452
453         $children = $document->children();
454
455         if (count($children) == 0) {
456             return array();
457         }
458
459         if (isset($document->status)) {
460             throw new Exception("Error #".$document->status['value']." ('".$document->status['message']."')");
461         }
462
463         // Array of elements, >0 elements
464
465         return $document->geoname;
466     }
467
468     function onPluginVersion(&$versions)
469     {
470         $versions[] = array('name' => 'Geonames',
471                             'version' => STATUSNET_VERSION,
472                             'author' => 'Evan Prodromou',
473                             'homepage' => 'http://status.net/wiki/Plugin:Geonames',
474                             'rawdescription' =>
475                             _m('Uses <a href="http://geonames.org/">Geonames</a> service to get human-readable '.
476                                'names for locations based on user-provided lat/long pairs.'));
477         return true;
478     }
479
480     function canonical($coord)
481     {
482         $coord = rtrim($coord, "0");
483         $coord = rtrim($coord, ".");
484
485         return $coord;
486     }
487 }