Skip to content

Instantly share code, notes, and snippets.

@mheadd
Created January 21, 2011 21:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mheadd/790499 to your computer and use it in GitHub Desktop.
Save mheadd/790499 to your computer and use it in GitHub Desktop.
Baltimore Locator: An IM, SMS and Twitter application that finds locations in the City of Baltimore.
<?php
/**
* Baltimore Locator - an IM, SMS, Twitter and Phone application that finds
* locations in Baltimore.
*
* @copyright 2012 Mark J. Headd (http://civic.io)
* @author Mark Headd
*/
// URL for OpenBaltimore dataset on libraries.
define("OPEN_BALTIMORE_URL", "http://data.baltimorecity.gov/api/views/tgtv-wr5u/rows.json");
/**
* Function to make a call to Socrata API to get locations by zip code.
*/
function getLocationsByZipCode($zip) {
// Fetch dataset.
$json = json_decode(file_get_contents(OPEN_BALTIMORE_URL));
// A simple array to hold library locations.
$libraries = array();
// Iterate over dataset for rows matching entered zip code.
for($i=0; $i<count($json->data); $i++) {
if($zip == $json->data[$i][9]) {
// Get the address information for this location.
$address_data = json_decode($json->data[$i][13][0]);
// Popualte the results array with the location details.
array_push($libraries, array($json->data[$i][8], $json->data[$i][10], $address_data->address, $json->data[$i][9]));
}
}
return $libraries;
}
/**
* Function to read out locations to caller.
*/
function sayLocations($locations, $zip) {
// No libraries found.
if(count($locations) == 0) {
say("I could not find locations in the " . implode(" ", strsplit($zip)) . " zip code.", array("voice" => "victor"));
// Ask caller if they want to search again.
$retry = ask("Do you want to search another zip code?", array("choices" => "yes,no", "attempts" => 1, "timeout" => 5, "voice" => "victor"));
if($retry->name == "choice") {
if($retry->value == "yes") {
getZipCode();
}
}
return;
}
// Read out library locations.
else {
for($i=0; $i<count($locations); $i++) {
say($locations[$i][0] . " Library, in the " . $locations[$i][1] . " neighborhood. " . $locations[$i][2] . " Baltimore, MD " . $locations[$i][3], array("voice" => "victor"));
}
$repeat = ask("Do you want to hear this information again?", array("choices" => "yes,no", "attempts" => 1, "timeout" => 5, "voice" => "victor"));
if($repeat->name == "choice") {
if($repeat->value == "yes") {
sayLocations($locations);
}
}
return;
}
}
/**
* Function to get zip code from caller.
*/
function getZipCode() {
$zip = ask("Please say or enter a five digit zip code in the City of Baltimore.", array("choices" => "[5 DIGITS]", "attempts" => 3, "timeout" => 5, "voice" => "victor"));
if($zip->name == "choice") {
// Fetch data from Socrata API.
$locations = getLocationsByZipCode($zip->value);
// Read out library locations.
sayLocations($locations, $zip->value);
}
else {
say("You seem to be having trouble. Please try your call again later.", array("voice" => "victor"));
}
return;
}
/**
* Function to say welcome prompt - begins script execution.
*/
function sayWelcome($playGreeting=false) {
say("Welcome to the Baltimore library locater.", array("voice" => "victor"));
$zip = getZipCode();
return;
}
// Begin script execution.
sayWelcome(true);
// End script execution.
say("Goodbye.", array("voice" => "victor"));
hangup();
?>
<?php
/**
* Baltimore Locator - an IM, SMS, Twitter and Phone application that finds
* locations in Baltimore.
*
* @copyright 2012 Mark J. Headd (http://civic.io)
* @author Mark Headd
*/
// Constants used to geocode an address.
define("MAPS_LOOKUP_BASE_URL", "http://maps.googleapis.com/maps/api/geocode/json");
// CouchDB Details.
define("GEO_COUCH_BASE_URL", "http://voiceingov.iriscouch.com:5984/");
define("GEO_COUCH_DESIGN_DOC_PATH", "_design/geojson/_spatial/points");
// URL for OpenBaltimore dataset on libraries.
define("OPEN_BALTIMORE_URL", "http://data.baltimorecity.gov/api/views/tgtv-wr5u/rows.json");
// Exception classes.
class GeoCodeException extends Exception {}
class GeoCouchException extends Exception {}
// Get list of DB's in CouchDB instance.
function getDatabaseNames() {
$url = GEO_COUCH_BASE_URL."_all_dbs";
return makeCurlCall($url, GeoCouchException, "Could not connect to BaltAPI instance. :-(");
}
// Get the lat / lon for an address.
function geoCodeAddress($address) {
// Format address and URL.
$address = str_replace(" ", "+", $address);
$url = MAPS_LOOKUP_BASE_URL."?address=".$address."&sensor=false";
return makeCurlCall($url, GeoCodeException, "Could not Geocode that address. Try again.");
}
// Make call to GeoCouch and get locations by address.
function getLocationsByAddress($bb_array, $db_name) {
// Format URL to GeoCouch instance.
$url = GEO_COUCH_BASE_URL."$db_name/".GEO_COUCH_DESIGN_DOC_PATH."?bbox=";
$url .= implode(",", $bb_array);
return makeCurlCall($url, GeoCouchException, "Could not connect to BaltAPI instance. :-(");
}
// Make call to Socrata API to get locations by zip code.
function getLocationsByZipCode($zip) {
// Fetch dataset.
$json = json_decode(file_get_contents(OPEN_BALTIMORE_URL));
// A simple array to hold library locations.
$libraries = array();
// Iterate over dataset for rows matching entered zip code.
for($i=0; $i<count($json->data); $i++) {
if($zip == $json->data[$i][9]) {
// Get the address information for this location.
$address_data = json_decode($json->data[$i][13][0]);
// Popualte the results array with the location details.
array_push($libraries, array($json->data[$i][8], $json->data[$i][10], $address_data->address, $json->data[$i][9]));
}
}
return $libraries;
}
// Heler method to make an HTTP request.
function makeCurlCall($url, $exceptionType, $exceptionMessage) {
// Set up cURL call.
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute.
$output = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Return results.
if($code != '200') {
throw new $exceptionType($exceptionMessage);
}
else {
return $output;
}
}
// Make a bounding box from a lat / lon pair
function makeBoundingBox($lon, $lat, $factor=.015) {
$bb_array = Array($lon-$factor, $lat-$factor, $lon+$factor, $lat+$factor);
return $bb_array;
}
// Calculate the distance between 2 points.
function calculateDistance($lat1, $lon1, $lat2, $lon2) {
return 3958*3.1415926*sqrt(($lat2-$lat1)*($lat2-$lat1) + cos($lat2/57.29578)*cos($lat1/57.29578)*($lon2-$lon1)*($lon2-$lon1))/180;
}
function formatAddress($address) {
if(stristr($address, "baltimore")) {
return trim($address);
}
else {
return trim($address) . " Baltimore, MD";
}
}
try {
// Get the address submited by the user.
$result = ask("", array("choices" => "[ANY]"));
// User entered zip code.
if(strlen($result->value) == 5) {
// Fetch data from Socrata API.
$locations = getLocationsByZipCode($result->value);
if(count($locations) == 0) {
say("I could not find locations in the " . $result->value . " zip code.");
}
else {
for($i=0; $i<count($locations); $i++) {
say($locations[$i][0] . " Library, in the " . $locations[$i][1] . " neighborhood. " . $locations[$i][2] . " Baltimore, MD " . $locations[$i][3]);
}
}
say("Visit www.askusnow.info to chat with a librarian.");
}
// User entered an address.
else {
// Set the location type (default is libraries).
if(strstr($result->value, " #")) {
$message = explode(" #", $result->value);
$address = formatAddress($message[0]);
$location_type = trim($message[1]);
}
else {
$address = $result->value;
$location_type = "libraries";
}
// Get all of the CouchDB DB names that hold locational data.
$all_db_names = json_decode(getDatabaseNames(), true);
$db_names = array();
foreach($all_db_names as $key => $value) {
if(substr($value, 0, 1) == "_") {
continue;
}
array_push($db_names, $value);
}
// If the user sent in a valid DB name, get the locations.
if(in_array($location_type, $db_names)) {
// Geocode address and get lat / lon.
$geocoded_address = json_decode(geoCodeAddress($address));
$lat = $geocoded_address->results[0]->geometry->location->lat;
$lon = $geocoded_address->results[0]->geometry->location->lng;
// Create a bound box from the lat / lon pair.
$bb_array = makeBoundingBox($lon, $lat);
// Get the locations from geocouch.
$locations = json_decode(getLocationsByAddress($bb_array, $location_type));
// If locations are found,iterate over JSON and render to user.
if($locations->rows) {
foreach($locations->rows as $location) {
$property = $location->value->properties;
$geometry = $location->value->geometry->coordinates;
$place = "Location: ";
foreach($property as $key => $value) {
$place .= $value.", ";
}
$place = substr($place, 0, strlen($place)-2);
$place = "$place.";
$distance = round(calculateDistance($lat, $lon, $geometry[1], $geometry[0]), 2);
$place .= " Distance: $distance miles.";
say($place);
}
}
// Otherwise, tell user no locations found.
else {
say("No locations found.");
}
if($location_type == "libraries") {
say("Visit www.askusnow.info to chat with a librarian.");
}
}
// Otherwise, tell them to only send valid resource names.
else {
say("Send address followed by #".implode(", #", $db_names));
}
}
}
// If address can not be Geocoded.
catch (GeoCodeException $ex) {
say("An error occured. ". $ex->getMessage());
hangup();
}
// If call can not be made to GeoCouch
catch (GeoCouchException $ex) {
say("An error occured. ". $ex->getMessage());
hangup();
}
// Last ditch exception handler.
catch (Exception $ex) {
say("Sorry. Something really bad happened.");
_log("*** ". $ex->getMessage() . " ***");
hangup();
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment