summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Prodromou <evan@status.net>2009-10-22 15:44:36 -0400
committerEvan Prodromou <evan@status.net>2009-10-22 15:44:36 -0400
commit9c983c383029c9aaa7d0cf04a5fc9973d514dff7 (patch)
tree772364dd5437668c58645a935286a196669824d4
parent7b227fd1c06ff6f3ba078b73348dd02419d44e38 (diff)
extract Geonames stuff to a plugin
-rw-r--r--lib/location.php119
-rw-r--r--plugins/GeonamesPlugin.php281
2 files changed, 356 insertions, 44 deletions
diff --git a/lib/location.php b/lib/location.php
index f4ce7f67a..5b7f47102 100644
--- a/lib/location.php
+++ b/lib/location.php
@@ -34,6 +34,10 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
/**
* class for locations
*
+ * These are stored in the DB as part of notice and profile records,
+ * but since they're about the same in both, we have a separate class
+ * for them.
+ *
* @category Location
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
@@ -48,74 +52,94 @@ class Location
public $location_id;
public $location_ns;
- var $names;
+ var $names = array();
- const geonames = 1;
- const whereOnEarth = 2;
+ /**
+ * Constructor that makes a Location from a string name
+ *
+ * @param string $name Human-readable name (any kind)
+ * @param string $language Language, default = common_language()
+ *
+ * @return Location Location with that name (or null if not found)
+ */
- static function fromName($name, $language=null, $location_ns=null)
+ static function fromName($name, $language=null)
{
if (is_null($language)) {
$language = common_language();
}
- if (is_null($location_ns)) {
- $location_ns = common_config('location', 'namespace');
- }
$location = null;
- if (Event::handle('LocationFromName', array($name, $language, $location_ns, &$location))) {
+ // Let a third-party handle it
- switch ($location_ns) {
- case Location::geonames:
- return Location::fromGeonamesName($name, $language);
- break;
- case Location::whereOnEarth:
- return Location::fromWhereOnEarthName($name, $language);
- break;
- }
- }
+ Event::handle('LocationFromName', array($name, $language, &$location));
return $location;
}
- static function fromGeonamesName($name, $language)
+ /**
+ * Constructor that makes a Location from an ID
+ *
+ * @param integer $id Identifier ID
+ * @param integer $ns Namespace of the identifier
+ * @param string $language Language to return name in (default is common)
+ *
+ * @return Location The location with this ID (or null if none)
+ */
+
+ static function fromId($id, $ns, $language=null)
{
$location = null;
- $client = HTTPClient::start();
-
- // XXX: break down a name by commas, narrow by each
-
- $str = http_build_query(array('maxRows' => 1,
- 'q' => $name,
- 'lang' => $language,
- 'type' => 'json'));
-
- $result = $client->get('http://ws.geonames.org/search?'.$str);
-
- if ($result->code == "200") {
- $rj = json_decode($result->body);
- if (count($rj['geonames']) > 0) {
- $n = $rj['geonames'][0];
- $location = new Location();
- $location->lat = $n->lat;
- $location->lon = $n->lon;
- $location->name = $n->name;
- $location->location_id = $n->geonameId;
- $location->location_ns = Location:geonames;
- }
- }
+
+ // Let a third-party handle it
+
+ Event::handle('LocationFromId', array($id, $ns, $language, &$location));
return $location;
}
- static function fromId($location_id, $location_ns = null)
+ /**
+ * Constructor that finds the nearest location to a lat/lon pair
+ *
+ * @param float $lat Latitude
+ * @param float $lon Longitude
+ * @param string $language Language for results, default = current
+ *
+ * @return Location the location found, or null if none found
+ */
+
+ static function fromLatLon($lat, $lon, $language=null)
{
- if (is_null($location_ns)) {
- $location_ns = common_config('location', 'namespace');
+ if (is_null($language)) {
+ $language = common_language();
}
+
+ $location = null;
+
+ // Let a third-party handle it
+
+ if (Event::handle('LocationFromLatLon',
+ array($lat, $lon, $language, &$location))) {
+ // Default is just the lat/lon pair
+
+ $location = new Location();
+
+ $location->lat = $lat;
+ $location->lon = $lon;
+ }
+
+ return $location;
}
+ /**
+ * Get the name for this location in the given language
+ *
+ * @param string $language language to use, default = current
+ *
+ * @return string location name or null if not found
+ */
+
function getName($language=null)
{
if (is_null($language)) {
@@ -124,6 +148,13 @@ class Location
if (array_key_exists($this->names, $language)) {
return $this->names[$language];
+ } else {
+ $name = null;
+ Event::handle('LocationNameLanguage', array($this, $language, &$name));
+ if (!empty($name)) {
+ $this->names[$language] = $name;
+ return $name;
+ }
}
}
}
diff --git a/plugins/GeonamesPlugin.php b/plugins/GeonamesPlugin.php
new file mode 100644
index 000000000..934e998c7
--- /dev/null
+++ b/plugins/GeonamesPlugin.php
@@ -0,0 +1,281 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to convert string locations to Geonames IDs and vice versa
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin to convert string locations to Geonames IDs and vice versa
+ *
+ * This handles most of the events that Location class emits. It uses
+ * the geonames.org Web service to convert names like 'Montreal, Quebec, Canada'
+ * into IDs and lat/lon pairs.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso Location
+ */
+
+class GeonamesPlugin extends Plugin
+{
+ const NAMESPACE = 1;
+
+ /**
+ * convert a name into a Location object
+ *
+ * @param string $name Name to convert
+ * @param string $language ISO code for anguage the name is in
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromName($name, $language, &$location)
+ {
+ $client = HTTPClient::start();
+
+ // XXX: break down a name by commas, narrow by each
+
+ $str = http_build_query(array('maxRows' => 1,
+ 'q' => $name,
+ 'lang' => $language,
+ 'type' => 'json'));
+
+ $result = $client->get('http://ws.geonames.org/search?'.$str);
+
+ if ($result->code == "200") {
+ $rj = json_decode($result->body);
+ if (count($rj['geonames']) > 0) {
+ $n = $rj['geonames'][0];
+
+ $location = new Location();
+
+ $location->lat = $n['lat'];
+ $location->lon = $n['lng'];
+ $location->names[$language] = $n['name'];
+ $location->location_id = $n['geonameId'];
+ $location->location_ns = self::NAMESPACE;
+
+ // handled, don't continue processing!
+ return false;
+ }
+ }
+
+ // Continue processing; we don't have the answer
+ return true;
+ }
+
+ /**
+ * convert an id into a Location object
+ *
+ * @param string $id Name to convert
+ * @param string $ns Name to convert
+ * @param string $language ISO code for language for results
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromId($id, $ns, $language, &$location)
+ {
+ if ($ns != self::NAMESPACE) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('geonameId' => $id,
+ 'lang' => $language));
+
+ $result = $client->get('http://ws.geonames.org/hierarchyJSON?'.$str);
+
+ if ($result->code == "200") {
+
+ $rj = json_decode($result->body);
+
+ if (count($rj['geonames']) > 0) {
+
+ $parts = array();
+
+ foreach ($rj['geonames'] as $level) {
+ if (in_array($level['fcode'], array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $level['name'];
+ }
+ }
+
+ $last = $rj['geonames'][count($rj['geonames'])-1];
+
+ if (!in_array($level['fcode'], array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $last['name'];
+ }
+
+ $location = new Location();
+
+ $location->location_id = $last['geonameId'];
+ $location->location_ns = self::NAMESPACE;
+ $location->lat = $last['lat'];
+ $location->lon = $last['lng'];
+ $location->names[$language] = implode(', ', array_reverse($parts));
+ }
+ }
+
+ // We're responsible for this NAMESPACE; nobody else
+ // can resolve it
+
+ return false;
+ }
+
+ /**
+ * convert a lat/lon pair into a Location object
+ *
+ * Given a lat/lon, we try to find a Location that's around
+ * it or nearby. We prefer populated places (cities, towns, villages).
+ *
+ * @param string $lat Latitude
+ * @param string $lon Longitude
+ * @param string $language ISO code for language for results
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromLatLon($lat, $lon, $language, &$location)
+ {
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('lat' => $lat,
+ 'lng' => $lon,
+ 'lang' => $language));
+
+ $result =
+ $client->get('http://ws.geonames.org/findNearbyPlaceNameJSON?'.$str);
+
+ if ($result->code == "200") {
+
+ $rj = json_decode($result->body);
+
+ if (count($rj['geonames']) > 0) {
+
+ $n = $rj['geonames'][0];
+
+ $parts = array();
+
+ $location = new Location();
+
+ $parts[] = $n['name'];
+
+ if (!empty($n['adminName1'])) {
+ $parts[] = $n['adminName1'];
+ }
+
+ if (!empty($n['countryName'])) {
+ $parts[] = $n['countryName'];
+ }
+
+ $location->location_id = $n['geonameId'];
+ $location->location_ns = self::NAMESPACE;
+ $location->lat = $lat;
+ $location->lon = $lon;
+
+ $location->names[$language] = implode(', ', $parts);
+
+ // Success! We handled it, so no further processing
+
+ return false;
+ }
+ }
+
+ // For some reason we don't know, so pass.
+
+ return true;
+ }
+
+ /**
+ * Human-readable name for a location
+ *
+ * Given a location, we try to retrieve a human-readable name
+ * in the target language.
+ *
+ * @param Location $location Location to get the name for
+ * @param string $language ISO code for language to find name in
+ * @param string &$name Place to put the name
+ *
+ * @return boolean whether to continue
+ */
+
+ function onLocationNameLanguage($location, $language, &$name)
+ {
+ if ($location->location_ns != self::NAMESPACE) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('geonameId' => $id,
+ 'lang' => $language));
+
+ $result = $client->get('http://ws.geonames.org/hierarchyJSON?'.$str);
+
+ if ($result->code == "200") {
+
+ $rj = json_decode($result->body);
+
+ if (count($rj['geonames']) > 0) {
+
+ $parts = array();
+
+ foreach ($rj['geonames'] as $level) {
+ if (in_array($level['fcode'], array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $level['name'];
+ }
+ }
+
+ $last = $rj['geonames'][count($rj['geonames'])-1];
+
+ if (!in_array($level['fcode'], array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $last['name'];
+ }
+
+ if (count($parts)) {
+ $name = implode(', ', array_reverse($parts));
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+}