diff --git a/CURL.php b/CURL.php index 32a6747..edf5c24 100644 --- a/CURL.php +++ b/CURL.php @@ -1,5 +1,5 @@ time() )); +## hCard adr queries + +It is possible to format the result of a Context query to expose properties you would expect from +an hCard. Primarily this includes countryName, region and locality. The normalization of individual +countries' naming conventions for their regions is handled by this class. + + $adr = \SimpleGeo\adr::createFromLatLng($geo, 49.239, -123.129); + echo $adr->locality; // Vancouver + echo $adr->region; // British Columbia + echo $adr->country; // Canada ## Everything else diff --git a/SimpleGeo.php b/SimpleGeo.php index d9883d1..d34ee9c 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -1,8 +1,13 @@ -lat = $lat; + $this->lng = $lng; + } +} + +/** + A ContextResult object represents the results of the API call to the /1.0/context methods. + This object can be used to retrieve data in a more structured format than parsing through + the array on its own. + + @author Aaron Parecki +**/ +class ContextResult { + public $data; + private $_intersection = FALSE; + + public function __construct(array $data) { + $this->data = $data; + } + + /** + * Returns a single feature of the requested category type, or FALSE if none was found. + * If more than one feature was found matching the category, the best one is returned. + */ + public function getFeatureOfCategory($category) { + if(!$this->_featuresExists()) + return FALSE; + + $candidates = $this->getFeaturesOfCategory($category); + + if(count($candidates) == 0) + return FALSE; + + if(count($candidates) == 1) + return $candidates[0]; + + // Sometimes there are more than one result for a given category, in these + // cases, we want to return the one that has an "abbr" because it's usually better. + foreach($candidates as $c) { + if($c->data['abbr']) + return $c; + } + + // If none had 'abbr', then just return the first + return $candidates[0]; + } + + /** + * Returns an array of all features matching the requested category + */ + public function getFeaturesOfCategory($category) { + if(!$this->_featuresExists()) + return array(); + + $features = array(); + foreach($this->data['features'] as $f) { + if(array_key_exists('classifiers', $f) && is_array($f['classifiers'])) { + foreach($f['classifiers'] as $c) { + if($c['category'] == $category) { + $features[] = new ContextFeature($f); + } + } + } + } + + return $features; + } + + public function getIntersectionName() { + if(!$this->_intersectionsExists()) + return FALSE; + + if($this->_intersection == FALSE) + $this->_intersection = new NearestIntersection($this); + + return $this->_intersection->name; + } + + private function _featuresExists() { + return array_key_exists('features', $this->data); + } + private function _intersectionsExists() { + return array_key_exists('intersections', $this->data); + } +} + +/** + @author Aaron Parecki +**/ +class NearestIntersection { + private $_context; + private $_intersections; + + public function __construct(ContextResult $context) { + $this->_context = $context; + $this->_intersections = $context->data['intersections']; + } + + public function __get($k) { + if($k == 'name') + return $this->getName(); + + return NULL; + } + + /** + * Returns a string with the intersection name represented by this context result. + * Examples: + * SE 20th & Salmon St + * NW 6th Ave & Davis St + * 6th Ave between Davis St and Everett St + */ + public function getName() { + if(!is_array($this->_intersections) || count($this->_intersections) == 0) + return FALSE; + + $intersection = $this->_intersections[0]; // SimpleGeo returns intersections sorted by distance, closest first + + if(!array_key_exists('properties', $intersection) || !is_array($intersection['properties'])) + return FALSE; + + // Remove intersections farther than 250m + if($intersection['distance'] > 250) + return FALSE; + + if(!array_key_exists('highways', $intersection['properties']) || !is_array($intersection['properties']['highways'])) + return FALSE; + + $highways = $intersection['properties']['highways']; + + if(count($highways) == 1) { + // This case is probably never reached + return $this->_abbreviateDirection($highways[0]['name']); + } + + if(count($highways) == 2) { + $street1 = $this->_abbreviateDirection($highways[0]['name']); + $street2 = $this->_abbreviateDirection($highways[1]['name']); + + // If the direction in both street names is the same, drop it from the second one + if(($dir=$this->_getDirection($street1)) == $this->_getDirection($street2) && $dir != FALSE) { + $street2 = preg_replace('/^'.$dir.' /', '', $street2); + } + + return $street1 . ' & ' . $street2; + } + } + + // Find the street direction in the street name. + // Expected to be used on street names already run through _abbreviateDirection() + private function _getDirection($name) { + if(preg_match('/^(NE|NW|N|SE|SW|S|W|E) /', $name, $match)) { + return $match[1]; + } else { + return FALSE; + } + } + + private function _abbreviateDirection($name) { + return preg_replace(array( + '/\bnortheast\b/i', + '/\bnorthwest\b/i', + '/\bnorth\b/i', + '/\bsoutheast\b/i', + '/\bsouthwest\b/i', + '/\bsouth\b/i', + '/\beast\b/i', + '/\bwest\b/i', + ), array( + 'NE', 'NW', 'N', 'SE', 'SW', 'S', 'E', 'W' + ), $name); + } +} + +class ContextFeature { + public $data; + + public function __construct(array $data) { + $this->data = $data; + } + + public function __toString() { + if(array_key_exists('name', $this->data)); + return $this->data['name']; + + return ''; + } + + public function __get($k) { + return array_key_exists($k, $this->data) ? $this->data[$k] : NULL; + } +} + +/** + An adr object can be created from a ContextResult. It will expose properties you would + expect to exist in an hCard. See http://microformats.org/wiki/adr for more information. + + Example Usage: + $adr = \SimpleGeo\adr::createFromLatLng($geo, 49.239, -123.129); + echo $adr->countryName; + echo $adr->region; + echo $adr->locality; + echo $adr->locality->license; + + @author Aaron Parecki +**/ +class adr { + public $postOfficeBox = FALSE; + public $extendedAddress = FALSE; + public $streetAddress = FALSE; + public $locality = FALSE; + public $region = FALSE; + public $postalCode = FALSE; + public $countryName = FALSE; + + public $context; + + /** + * Create a new adr object given a lat/lng. Requires a SimpleGeo object to be passed in. + */ + public static function createFromLatLng(SimpleGeo $sg, $lat, $lng, $filter='features') { + return new adr(new ContextResult($sg->ContextCoord(new GeoPoint($lat, $lng), array( + 'filter' => $filter, + 'features__category' => 'National,Provincial,Subnational,Urban Area,Municipal,Postal Code' + )))); + } + + public function __construct(ContextResult $c) { + // Parse the context result into the appropriate fields + $this->context = $c; + + // Many non-us addresses do not include a "region" in their hCard. + // In these cases, just a "locality" and "country-name" are used. + + if($municipal = $c->getFeatureOfCategory('Municipal')) { + $this->locality = $municipal; + } elseif($urbanArea = $c->getFeatureOfCategory('Urban Area')) { + $this->locality = $urbanArea; + } + + if($subnational = $c->getFeatureOfCategory('Subnational')) { + $this->region = $subnational; + } elseif(($provincial = $c->getFeatureOfCategory('Provincial')) && ($provincial != $this->locality)) { + $this->region = $provincial; + } elseif(($urbanArea = $c->getFeatureOfCategory('Urban Area')) && ($urbanArea != $this->locality)) { + $this->region = $urbanArea; + } + + // Sometimes (esp in the UK) the "provincial" area is smaller than the "urban area". + // This isn't right, because Bexley is inside of London. Fix it by making sure the smaller + // of the locality and region ends up in the locality. + if($this->locality && $this->region) { + $localityRadius = $this->_radiusFromBounds($this->locality->data['bounds']); + $regionRadius = $this->_radiusFromBounds($this->region->data['bounds']); + + if($localityRadius > $regionRadius) { + $tmp = $this->locality; + $this->locality = $this->region; + $this->region = $tmp; + } + } + + if($country = $c->getFeatureOfCategory('National')) { + $this->countryName = $country; + } + + if($postalCode = $c->getFeatureOfCategory('Postal Code')) { + $this->postalCode = $postalCode; + } + } + + private function _radiusFromBounds($bounds) { + $response['width'] = SimpleGeo::gc_distance($bounds[1],$bounds[0], $bounds[1],$bounds[2]); + $response['height'] = SimpleGeo::gc_distance($bounds[1],$bounds[0], $bounds[3],$bounds[0]); + $response['radius'] = max($response['width'], $response['height']) / 2; + return $response['radius']; + } + + /** + * Alias property names for convenience + * Can be called with names like: + * $adr->country_name + * $adr->{'country-name'}; + */ + public function __get($k) { + if($k == 'country') + return $this->countryName; + + if($k == 'intersection') { + return $this->context->getIntersectionName(); + } + + // replace hyphens and underscores with spaces + $k = str_replace(array('-','_'), ' ', $k); + // capitalize every word + $k = ucwords($k); + // remove spaces + $k = str_replace(' ', '', $k); + // lowercase the first letter + $k = lcfirst($k); + + if(property_exists($this, $k)) + return $this->{$k}; + else + return FALSE; + } } + /** A record object contains data regarding an arbitrary object and the layer it resides in http://simplegeo.com/docs/getting-started/storage#what-record - **/ class Record { @@ -78,8 +391,10 @@ class SimpleGeo extends CURL { private $token, $secret; const BASE_URL = 'http://api.simplegeo.com/'; + + public $numRequests = 0; - public function SimpleGeo($token = false, $secret = false) { + public function __construct($token = false, $secret = false) { $this->token = $token; $this->secret = $secret; $this->consumer = new OAuthConsumer($this->token, $this->secret); @@ -137,11 +452,11 @@ public function ContextIP($ip, $opts = false) { **/ public function ContextCoord($lat, $lng = false, $opts = false) { if ($lat instanceof GeoPoint) { + if (is_array($lng)) $opts = $lng; $lng = $lat->lng; $lat = $lat->lat; - if (is_array($lng)) $opts = $lng; } - return $this->SendRequest('GET', '1.0/context/' . $lat . ',' . $lng . '.json'); + return $this->SendRequest('GET', '1.0/context/' . $lat . ',' . $lng . '.json', $opts); } @@ -382,6 +697,7 @@ public function GetResults() { private function SendRequest($method = 'GET', $endpoint, $data = array()) { + $this->numRequests++; $this->Revert(self::BASE_URL . $endpoint); $this->SetMethod($method); if (is_array($data)) $this->AddVars($data); @@ -389,13 +705,18 @@ private function SendRequest($method = 'GET', $endpoint, $data = array()) { $this->IncludeAuthHeader(); return $this->Get(); } + + public static function gc_distance($lat1, $lng1, $lat2, $lng2) { + return ( 6378100 * acos( cos( deg2rad($lat1) ) * cos( deg2rad($lat2) ) * cos( deg2rad($lng2) - deg2rad($lng1) ) + sin( deg2rad($lat1) ) * sin( deg2rad($lat2) ) ) ); + } } /* (The MIT License) -Copyright (c) 2011 Rishi Ishairzay <rishi [at] ishairzay [dot] com> +Copyright (c) 2011 Rishi Ishairzay + and (c) 2011 Aaron Parecki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -418,5 +739,3 @@ private function SendRequest($method = 'GET', $endpoint, $data = array()) { */ - -?>