Create GPS-enabling web applications

Build a location-enabled news feed with PHP, XML, jQuery, and an in-browser GPS

This article walks you through building the back end and front end of a GPS-enabled web application using PHP.

Jack D. Herrington, Senior Software Engineer, Fortify Software, Inc.

Photo of Jack HerringtonJack Herrington is an engineer, author, and presenter who lives and works in the Bay Area. You can keep up with his work and his writing at http://jackherrington.com.



02 August 2011

Also available in Chinese Russian Japanese

GPS-enabling web applications

Frequently used acronyms

  • Ajax: Asynchronous JavaScript + XML
  • API: Application programming interface
  • DOM: Document Object Mode
  • GPS: Global positioning system
  • HTML: HyperText Markup Language
  • HTTP: Hypertext Transfer Protocol
  • PDO: PHP Data Object
  • SQL: Structured Query Language
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium
  • XML: Extensible Markup Language

Web sites that provide services based on your geographic location are quite popular on the Internet. Sites such as Foursquare, Yelp, and Google Maps all make use of where you are to give you information relevant to you right in that location. You can easily get someone’s location and give them information based on that locale.

In this article, you will build both the back end and the front end of an application that uses GPS location to provide news content to the user. The back end is written in PHP and uses MySQL to hold a list of articles and their related location and its coordinates. The front-end web page uses the location services supported by webkit browsers to get the GPS coordinates of the user. It then sends those coordinates to the back end in an Ajax request. The PHP back-end system responds with an XML listing of articles, which the front end then renders dynamically.

You use locations in two different ways in this article. The first is when you add new articles to the database. You give the PHP back end a location (for example, Fremont, CA or Washington, DC), and the page uses a geolocation service provided by Yahoo! to turn that location into GPS coordinates. The second way you use location is when the web page makes a request to the browser to get the location of the user and then uses that information to query the database using Ajax.


Building the back end

The back end code starts at the database, which in this case is MySQL. Listing 1 shows a schema for the single table database.

Listing 1. db.sql
DROP TABLE IF EXISTS articles;
CREATE TABLE articles(
     lon FLOAT,
     lat FLOAT,
     address TEXT,
     title TEXT,
     url TEXT,
     summary TEXT );

The table has six columns, starting with the latitude and longitude values and the original address provided for the article (for example, Fremont, CA). There is then some biographic information about the article including the title, URL, and summary.

To build the database, first use mysqladmin to create it and then use the mysql command to run the db.sql script:

% mysqladmin --user=root --password=foo create articles
% mysql --user=root --password=foo articles < db.sql

With the database created, you can build the PHP page that you will use to add records to the database. Listing 2 shows the code for the insert.php page. (Note: The value for $url on lines 5 and 6 should appear as a single character string. It appears as two shorter strings for formatting purposes only.)

Listing 2. insert.php
<?php
$dd = new PDO('mysql:host=localhost;dbname=articles', 'root', '');
if ( isset( $_POST['url'] ) ) {
// You need a Yahoo! PlaceFinder application key
// Go to: http://developer.yahoo.com/geo/placefinder/
  $url = "http://where.yahooapis.com/geocode?q=".urlencode($_POST['address']).
         "&appid=[yourappid] ";

  $ch = curl_init(); 
  curl_setopt($ch, CURLOPT_URL, $url); 
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
  $body = curl_exec($ch); 

  preg_match( "/\<latitude\>(.*?)\<\/latitude\>/", $body, $lat );
  preg_match( "/\<longitude\>(.*?)\<\/longitude\>/", $body, $lon );

  $sql = 'INSERT INTO articles VALUES ( ?, ?, ?, ?, ?, ? )';
  $sth = $dd->prepare($sql);
  $sth->execute( array( 
    $lon[1],
    $lat[1],
    $_POST['address'],
    $_POST['title'],
    $_POST['url'],
    $_POST['summary']
    ) );
}
?>
<html>
<body>
<form method="post">
<table>
<tr><th>Title</td><th><input type="text" name="title" /></td></tr>
<tr><th>Summary</th><td><input type="text" name="summary" /></td></tr>
<tr><th>Address</th><td><input type="text" name="address" /></td></tr>
<tr><th>URL</th><td><input type="text" name="url" /></td></tr>
</table>
<input type="submit" value="Add Article" />
</form>
</body>
</html>

This page is a fairly standard PHP page for inserting records into a database. At the top, you check to see if the user has posted this page to you; if so, you add a record to the database using the PDO library. At the bottom of the script is an HTML form used to enter the fields.

The interesting thing here is in the request to the Yahoo! location web service using the Curl library. To get this code to run, you need an application ID provided by Yahoo! for its PlaceFinder service. For the URL of that service, see Resources and the code in Listing 2.

The PlaceFinder service returns an XML response for a given location. The response includes a latitude and longitude (plus a great many other bits of interesting information). A simple regular expression is used to extract the latitude and longitude from the XML.

This code is written as if an address is always resolved to a coordinate. If you choose to use this code in your own system be sure to check if a location is returned and present an informative error message to the user if a location isn’t resolved.

To test this code, install the insert.php page on the PHP server and bring it up in the browser. You should see something like the form in Figure 1.

Figure 1. The article input form
Screen capture of an article input form with title, summary, address, and URL fields plus 'Add Article' button

Figure 1 shows a simple four-field web form with title, summary, address, and URL fields and a button to add the article to the database. Figure 2 shows the field populated with the relevant values.

Figure 2. The article input form populated
Screen capture of an article input form populated with values

In Figure 2, the fields are populated with the title, summary, and URL of a local article on a judge’s ruling. The location is specified as "fremont, ca" in the Address field. You could specify a street address, a zip code, or anything else that can be resolved to a latitude/longitude pair.

At this point, you should populate the database using the insert.php page with some news articles about your local area. Or just use some articles from anywhere, but put in your address so that the GPS location provided by your browser returns some results.

With the database set up and populated, you are ready to build the search service that the front end will use.


What you need from a search service is a PHP page that takes a latitude and longitude pair, along with a radius, and returns any articles that have a location with that circle. The return should be encoded as XML so a web page (or any other application) can easily read it. Listing 3 shows just such a PHP page.

Listing 3. find.php
<?php
define( 'LATMILES', 1 / 69 );
define( 'LONMILES', 1 / 53 );

// Change these default coordinates to your current location
$lat = 37.3328;
$lon = -122.036;
$radius = 1.0;

if ( isset( $_GET['lat'] ) ) { $lat = (float)$_GET['lat']; }
if ( isset( $_GET['lon'] ) ) { $lon = (float)$_GET['lon']; }
if ( isset( $_GET['radius'] ) ) { $radius = (float)$_GET['radius']; }

$minlat = $lat - ( $radius * LONMILES );
$minlon = $lon - ( $radius * LATMILES );
$maxlat = $lat + ( $radius * LONMILES );
$maxlon = $lon + ( $radius * LATMILES );

$dbh = new PDO('mysql:host=localhost;dbname=articles', 'root', '');

$sql = 'SELECT * FROM articles WHERE lat >= ? AND lat <= ? AND lon >= ? AND lon <= ?';

$params = array( $minlat, $maxlat, $minlon, $maxlon );

if ( isset( $_GET['q'] ) ) {
  $sql .= " AND name LIKE ?";
  $params []= '%'.$_GET['q'].'%';
}

$q = $dbh->prepare( $sql );
$q->execute( $params );

$doc = new DOMDocument();
$r = $doc->createElement( "locations" );
$doc->appendChild( $r );

foreach ( $q->fetchAll() as $row) {
  $dlat = ( (float)$row['lat'] - $lat ) / LATMILES;
  $dlon = ( (float)$row['lon'] - $lon ) / LONMILES;
  $d = sqrt( ( $dlat * $dlat ) + ( $dlon * $dlon ) );
  if ( $d <= $radius ) {
    $e = $doc->createElement( "article" );
    $e->setAttribute( 'lat', $row['lat'] );
    $e->setAttribute( 'lon', $row['lon'] );
    $te = $doc->createElement('title');
    $te->appendChild( $doc->createTextNode( utf8_encode( $row['title'] ) ) );
    $e->appendChild( $te );
    $se = $doc->createElement('summary');
    $se->appendChild( $doc->createTextNode( utf8_encode( $row['summary'] ) ) );
    $e->appendChild( $se );
    $ue = $doc->createElement('url');
    $ue->appendChild( $doc->createTextNode( utf8_encode( $row['url'] ) ) );
    $e->appendChild( $ue );
    $ae = $doc->createElement('address');
    $ae->appendChild( $doc->createTextNode( utf8_encode( $row['address'] ) ) );
    $e->appendChild( $ae );
    $e->setAttribute( 'd', $d );
    $r->appendChild( $e );
  }
}

print $doc->saveXML();
?>

The code looks long and complicated, but it’s really not. You can divide it into two discreet sections. At the top, the script runs a query that finds all the articles within a bounding square using the latitude and longitude and min/max constraints in the SELECT statement.

The second part of the code further constrains the results by iterating through each article and checking to see whether it falls within the circle. That’s the little bit of trigonometry code at the beginning of the foreach loop. If it falls within the circle, the result is added to an XML DOM document that is created on the fly. The final bit of code just prints out the resulting XML document.

The XML creation does get a bit lengthy because of all the nodes that are created. All the article features besides the latitude and longitude are expressed as subnodes so that they can be of unlimited size. This approach is particularly important with the summary field that is quite lengthy as it should contain a synopsis of the article that is several paragraphs long.

To test the find service, first install it in the server, then navigate to it in the browser. You should see something like the result in Figure 3.

Figure 3. The find result XML rendered in the browser
Screen capture of the find result XML rendered in the browser

Figure 3 shows the browser displaying the XML results returned as a kind of web page of sorts though it looks ugly, squished together and not at all formatted. To see what it looks like as XML, select View Source in the browser to display something similar to Figure 4.

Figure 4. Viewing the source of the find.php result
Screen capture of the XML source of the find.php result

Figure 4 shows the result of the query in its original XML format.

If you see no results, there are several possible issues. It might be that the script could not connect to the database. The location might be a problem. You should change the default location in the find.php script to be close to where your articles have specified, or you should add lat and lon parameters to the web page URL. Another possibility is that your articles have invalid locations. To check for valid locations, log into the MySQL server and query the articles table yourself to see if the lat and lon fields are filled in appropriately.

Assuming that the find.php script returns some results, you are now ready to put the front end on this system.


Building the first version of the user interface

The first version of the user interface is very simple. It gets the GPS coordinates from the browser, sends them to the server with a hard-coded search radius of 20 miles, and then formats the returned data for presentation. You can see this first version of the code in Listing 4.

Listing 4. index.php
<html>
<head>
<script src="jquery-1.6.1.min.js"></script>
<script>
$(document).ready(function() {
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
  } else {
    alert('GPS not supported');
  }
});

function successCallback(position) {
  loadArticles( position.coords.latitude, position.coords.longitude );
}

function loadArticles( lat, lon ) {
  $.ajax({
    url: "find.php",
    dataType: 'xml',
    data: {
    lat:lat,
    lon:lon,
    radius:20
    },
    success: buildArticlePage
  });
}

function buildArticlePage( data ) {
  $(data).find("article").each(function() {
  var title = $(this).find("title").text();
  var summary = $(this).find("summary").text();
  var url = $(this).find("url").text();
  var address = $(this).find("address").text();
    $("#output").append( "<a target=\"_blank\" href=\""+url+"\">"+title+"</a><br/>");
    $("#output").append( "<i>"+address+"</i><br />");
    $("#output").append( summary+"<br /><br />");
  });
}

function errorCallback(error) {
  alert('Error getting GPS:'+error);
}
</script>
</head>
<body>
<div id="output">
</div>
</body> 
</html>

In Listing 4, the most interesting part of the code is right at the top of the file. One of the first things the code does after it loads is to look for navigator.geolocation, then run the getCurrentPosition method on it if it exists. That method takes two parameters, both of which are functions. The first function is called if the location can be found. The second is called if a location cannot be found.

When a location is found, the successCallback is called, which in turn calls loadArticles with the specified coordinates. I broke these two function calls apart to help with debugging if you have an issue getting your browser to give you coordinates. You can always call loadArticles directly with whatever coordinates you choose.

The loadArticles function makes the Ajax request to the server using the jQuery Ajax wrapper. And the buildArticlePage is called if the web server responds successfully. You might need to change the URL in loadArticles to point to wherever find.php is located if it is located in a different directory than this page.

The buildArticlePage uses a series of jQuery calls to both parse through the XML and add new HTML tags to the "output" div located in the body section of the HTML.

And that’s really all there is to it. Believe me, writing GPS support into a web page is much easier than it is to use proprietary APIs to get the location on iOS, Android, or other platforms. The geolocation service in the browser event supports tracking the location if you expect it to change rapidly.

To test the page, install the index.html page on your web server and open it in your browser, as in Figure 5.

Figure 5. The index page after getting the article results using the GPS coordinates
Screen capture of an index page listing article results based on the GPS coordinates

Figure 5 shows a web page of articles from the database that are located around the location provided by the web browser. Before you see the data, you might see a warning like that in Figure 6.

Figure 6. The Safari location warning prompt
Screen capture of the Safari location warning prompt

Figure 6 shows a warning from the server about giving your current location to the website. You can choose to accept or deny as you prefer, but for your own testing purposes you want to accept so that the JavaScript can get the coordinates, make the Ajax call, and render the results.

If you see a "GPS not supported" error, you are using a non-webkit browser, likely Microsoft® Internet Explorer®. At the writing of this article, Internet Explorer does not support location services.

If everything works, but you get no results, the search radius of twenty miles might be too small. Simply change the radius to 2000, and you should start to see some results, assuming, of course, that you put some data in the database on the same continent as your location.

The final step for this article is to upgrade it to have a radius control.


Enhancing the user interface

To add a radius control to the page, add a <select> tag to the page with options for each of the radius values that you want to support. Then add some jQuery code to monitor the value of the radius control and to update the page as shown in Listing 5.

Listing 5. index2.php
<html>
<head>
<script src="jquery-1.6.1.min.js"></script>
<script>
var startLan = null;
var startLon = null;
$(document).ready(function() {
  $('#radius').change( function() {
    loadArticles( startLan, startLon );
  } );
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
  } else {
    alert('GPS not supported');
  }
});

function successCallback(position) {
  loadArticles( position.coords.latitude, position.coords.longitude );
}

function loadArticles( lat, lon ) {
  startLan = lat;
  startLon = lon;
  $.ajax({
    url: "find.php",
    dataType: 'xml',
    data: {
    lat:lat,
    lon:lon,
    radius:$('#radius').val()
    },
    success: buildArticlePage
  });
}

function buildArticlePage( data ) {
  $('#output').empty();
  $(data).find("article").each(function() {
  var title = $(this).find("title").text();
  var summary = $(this).find("summary").text();
  var url = $(this).find("url").text();
  var address = $(this).find("address").text();
    $("#output").append( "<a target=\"_blank\" href=\""+url+"\">"+title+"</a><br/>");
    $("#output").append( "<i>"+address+"</i><br />");
    $("#output").append( summary+"<br /><br />");
  });
}

function errorCallback(error) {
  alert('Error getting GPS:'+error);
}
</script>
</head>
<body>
<select id="radius">
  <option value="5" selected>5</option>
  <option value="15">15</option>
  <option value="50">50</option>
  <option value="150">150</option>
</select>
<div id="output">
</div>
</body> 
</html>

In Listing 5, the changes start right at the top of the page where you add a change listener to the radius control. This code then invokes the loadArticles function when the control is changed. To make the code simpler, the loadArticles method stores the last location so that it can be reused when the radius is changed. The HTML section of the page is also updated to add the <select> tag and the radius options.

To test the enhancements, first install the updated web page on the server as index2.html and then navigate to it in the web browser. You should see a screen similar to Figure 7.

Figure 7. The search page now with a radius control
Screen capture of a list of articles with a drop-down field to control the search radius

Figure 7 shows the article listings along with a new radius drop-down control at the top of the page. Changing this radius control dynamically rebuilds the page by re-running the Ajax request with the updated radius value. It then empties the output <div> tag and adds the new set of found articles. In the case of Figure 8, I changed the value to 150.

Figure 8. The search results for 150 miles
Screen capture of search results within a 150-mile radius

You can see a few additional results in Figure 8 added to the bottom of the page.

Conclusion

Location-based services like FourSquare, Yelp, and Google Maps are popular because people want to know what’s good in their local area, and they want to connect to people around them. With this new location API, a little PHP, some jQuery JavaScript, a little XML, and some location support in the web browser, you have yourself a location-enabled web application. Using this code as a starting point, you can build your own location-based service with your own data model. And if your application requires a continuous feed of location data from the client, the geolocation services allow for that, too, which opens up the possibilities even further.

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Open source, Web development
ArticleID=744439
ArticleTitle=Create GPS-enabling web applications
publish-date=08022011