From 7b6d1c4787a743112cafd76ddf1dc302dc56da17 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 30 Jul 2011 14:00:29 -0700 Subject: [PATCH 1/7] Puts all classes in the SimpleGeo namespace to avoid naming conflicts. Add "use SimpleGeo\SimpleGeo;" before creating the SimpleGeo object. --- CURL.php | 2 +- OAuth.php | 6 ++++-- SimpleGeo.php | 13 +++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CURL.php b/CURL.php index 32a6747..edf5c24 100644 --- a/CURL.php +++ b/CURL.php @@ -1,5 +1,5 @@ token = $token; $this->secret = $secret; $this->consumer = new OAuthConsumer($this->token, $this->secret); From 3f901415ed61a44485ce758475fef7ab301e4d2f Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 30 Jul 2011 16:04:08 -0700 Subject: [PATCH 2/7] Fixed ContextCoord() method --- SimpleGeo.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/SimpleGeo.php b/SimpleGeo.php index 0c4c184..092166b 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -15,6 +15,11 @@ class GeoPoint { public $lat; public $lng; + + public function __construct($lat, $lng) { + $this->lat = $lat; + $this->lng = $lng; + } } /** @@ -142,11 +147,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); } From 64b9b3e061b02df23f3285c2673cb42dada4caed Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 30 Jul 2011 17:45:06 -0700 Subject: [PATCH 3/7] Adds an "adr" class to get hCard-compatible results from the Context query. Adds ContextResult and ContextFeature helper classes. --- SimpleGeo.php | 174 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/SimpleGeo.php b/SimpleGeo.php index 092166b..d72da0c 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -22,11 +22,178 @@ public function __construct($lat, $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 { + private $data; + + 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; + } + + private function _featuresExists() { + return array_key_exists('features', $this->data); + } +} + +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. + + @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; + + private $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) { + return new adr(new ContextResult($sg->ContextCoord(new GeoPoint($lat, $lng), array( + 'filter' => 'features', + '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; + $this->countryName = 'test'; + + // 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; + } elseif($provincial = $c->getFeatureOfCategory('Provincial')) { + $this->locality = $provincial; + } + + if($subnational = $c->getFeatureOfCategory('Subnational')) { + $this->region = $subnational; + } elseif(($provincial = $c->getFeatureOfCategory('Provincial')) && ($provincial != $this->locality)) { + $this->region = $provincial; + } + + if($country = $c->getFeatureOfCategory('National')) { + $this->countryName = $country; + } + + if($postalCode = $c->getFeatureOfCategory('Postal Code')) { + $this->postalCode = $postalCode; + } + } + + /** + * Alias property names for convenience + * Can be called with names like: + * $adr->country_name + * $adr->{'country-name'}; + */ + public function __get($k) { + // 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 { @@ -405,7 +572,8 @@ private function SendRequest($method = 'GET', $endpoint, $data = array()) { (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 @@ -428,5 +596,3 @@ private function SendRequest($method = 'GET', $endpoint, $data = array()) { */ - -?> From 7b19158032469dc58a4dbf2d7319eea1f768038b Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 30 Jul 2011 17:50:17 -0700 Subject: [PATCH 4/7] Added example usage --- README.md | 10 ++++++++++ SimpleGeo.php | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 3fc42ac..1827c17 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,16 @@ There are a couple of different ways to retrieve nearby records - either by IP, 'end' => 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 d72da0c..04ae04e 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -112,6 +112,13 @@ public function __get($k) { 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 { From 96c3c47daaf5f67a00a7d16b38f626bb6df372c1 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Wed, 3 Aug 2011 10:59:42 -0700 Subject: [PATCH 5/7] First pass at retrieving intersection names from lat/lngs. So far works pretty well within city limits --- SimpleGeo.php | 118 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/SimpleGeo.php b/SimpleGeo.php index 04ae04e..f135111 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -30,7 +30,8 @@ public function __construct($lat, $lng) { @author Aaron Parecki **/ class ContextResult { - private $data; + public $data; + private $_intersection = FALSE; public function __construct(array $data) { $this->data = $data; @@ -84,9 +85,110 @@ public function getFeaturesOfCategory($category) { 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 { @@ -130,14 +232,14 @@ class adr { public $postalCode = FALSE; public $countryName = FALSE; - private $context; + 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) { + public static function createFromLatLng(SimpleGeo $sg, $lat, $lng, $filter='features') { return new adr(new ContextResult($sg->ContextCoord(new GeoPoint($lat, $lng), array( - 'filter' => 'features', + 'filter' => $filter, 'features__category' => 'National,Provincial,Subnational,Urban Area,Municipal,Postal Code' )))); } @@ -145,7 +247,6 @@ public static function createFromLatLng(SimpleGeo $sg, $lat, $lng) { public function __construct(ContextResult $c) { // Parse the context result into the appropriate fields $this->context = $c; - $this->countryName = 'test'; // Many non-us addresses do not include a "region" in their hCard. // In these cases, just a "locality" and "country-name" are used. @@ -180,6 +281,13 @@ public function __construct(ContextResult $c) { * $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 From c45a88e8ad4991cf8e9d1312866a1acd55cee21b Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Thu, 4 Aug 2011 18:45:17 -0700 Subject: [PATCH 6/7] Count number of requests sent to SimpleGeo. --- SimpleGeo.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SimpleGeo.php b/SimpleGeo.php index f135111..15fe2ec 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -255,8 +255,6 @@ public function __construct(ContextResult $c) { $this->locality = $municipal; } elseif($urbanArea = $c->getFeatureOfCategory('Urban Area')) { $this->locality = $urbanArea; - } elseif($provincial = $c->getFeatureOfCategory('Provincial')) { - $this->locality = $provincial; } if($subnational = $c->getFeatureOfCategory('Subnational')) { @@ -370,6 +368,8 @@ class SimpleGeo extends CURL { private $token, $secret; const BASE_URL = 'http://api.simplegeo.com/'; + + public $numRequests = 0; public function __construct($token = false, $secret = false) { $this->token = $token; @@ -674,6 +674,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); From 913efcb2d9c18a57ce06a6aa1075e85d12fb5e09 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Thu, 4 Aug 2011 19:13:33 -0700 Subject: [PATCH 7/7] Updates for handling edge cases where the region was smaller than the locality --- SimpleGeo.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/SimpleGeo.php b/SimpleGeo.php index 15fe2ec..d34ee9c 100644 --- a/SimpleGeo.php +++ b/SimpleGeo.php @@ -261,8 +261,24 @@ public function __construct(ContextResult $c) { $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; } @@ -272,6 +288,13 @@ public function __construct(ContextResult $c) { } } + 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: @@ -682,6 +705,10 @@ 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) ) ) ); + } } /*