designing4u.de Yet Another Coding Blog

10May/090

Zip code search using jQuery and Google maps – displaying results in a defined radius

Last time I wrote about handling a Google map event, which is triggered, when a user moves around the map. I only mentioned that I had to use this solution for one of my projects, which goal was to display companies branches in a certain distance radius. I decided that it probably will be really interesting for all of you, how to display all company branches according to user input, therefore this time I will show you, how to retrieve the addresses of the places from a MySQL database, based on the zip code and radius provided by a user. I will also split this example in two parts. Todays part will only handle retrieving the results, displaying a simple navigation and markers on the map. In the second part we will push our project a little bit farther and we will allow our user to display the directions from the zip code he or she provided to the branch of his or hers choice.

To see a working example of this tutorial you can visit this site.

As I already mentioned in my previous post, to measure the distance between two points on a sphere one can use Haversine formula. For a detailed description I suggest visiting the wikipedia site. In our example we will use a simplified version of Haversine formula, which is caused by a really simple reason. Executing a web query with the actual Haversine formula might lead to a really slow response from the database and our simplified version will retrieve results which do not differentiate that much with the original version.

For retrieving the company branches in a certain radius we will first build a php class, which will be responsible for translating the zip code into geographical lengths and then use this values to retrieve the results and return them as json_encoded() data. Because our class will be relatively long, I will split it into parts and explain it line by line.

Since we are using an AJAX request to retrieve our data and the location of our php file is easy to guess just by looking at our jQuery code (will come after php class), we add a simple check of the apache environmental variable to check, if it actually is an XMLHttpRequest. A lot of you will probably say that this security measure is not enough, but for this example, it will at least stop all the robots trying access this file and since looking up addresses might consume a lot of resources of your server, this will save you some speed lost on servers with high traffic.

1
2
if ($_SERVER["HTTP_X_REQUESTED_WITH"] != 'XMLHttpRequest')
  die('why are you doing this??');

In line 3 we create our class. Since the subject of this example is finding company branches, lets call it Branch. In lines 5 to 13, we define some variables, which we will use in our class. I think they are pretty much self explanatory, and you will understand, what is their usage just by looking at the rest of the class.

3
4
5
6
7
8
9
10
11
12
13
class Branches {
 
  private static $_key = 'YOUR GOOGLE MAPS KEY';
  private static $_host = 'localhost';
  private static $_db_user = 'root';
  private static $_db_pass = 'pass';
  private static $_db_name = 'db';
 
  private $zip, $radius, $data_set;
 
  public $results = array();

In our constructor we connect to the database and set the radius and zip properties. You could notice that I convert the zip property to integer as well and I additionally add leading zero for strings, which length equals 4. Well, this class was written for German zip codes. You might want to change this parts so it suits your needs. After all it will be important in the part where we will convert the zip code to geographical length, so Google can find this zip code in their database.

14
15
16
17
18
19
20
21
22
23
24
25
public function __construct()
{
  $sql = mysql_connect(self::$_host, self::$_db_user, self::$_db_pass) or die(mysql_error());
  mysql_select_db(self::$_db_name, $sql) or die(mysql_error());
 
  $this->radius = (int)$_GET['radius'];
  $this->zip = (int)$_GET['zip'];
 
  // add leading zeros in case of 0... zip codes
  if (strlen($this->zip) == 4)
    $this->zip = sprintf("%05s", $this->zip);  
}

In line 26 we define a public method translateZipToLatLng() and in line 39 a private method getGeolocation(), which both will be responsible for translating the zip code into the geographical length. To get a detailed description, what this two methods actually do, refer to my other post: Geolocation class - retrieving longitude and latitude using google maps.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public function translateZipToLatLng()
{
  $url = 'http://maps.google.com/maps/geo?q=' . urlencode($this->zip . ', Germany') . '&output=csv&key=' . self::$_key;
 
  $response = $this->getGeolocation($url);
 
  if (substr($response, 0, 3) === '200') {
    $geo = explode(',', $response);
    return $data = array($geo[2], $geo[3]);
  }
  return false;
}
 
private function getGeolocation($url)
{
  $init = curl_init();
  curl_setopt($init, CURLOPT_URL, $url);
  curl_setopt($init, CURLOPT_HEADER,0);
  curl_setopt($init, CURLOPT_USERAGENT, $_SERVER["HTTP_USER_AGENT"]);
  curl_setopt($init, CURLOPT_RETURNTRANSFER, 1);
  $response = curl_exec($init);
  curl_close($init);
 
  return $response;
}

In line 51 we define a private method getResults, which, as you can see, will be responsible for retrieving all the information from the “branches" table and additionally measure the approximate distance from the zip code provided by the user to our actual company branches. In line 56 we define private method populate results, which will be responsible for creating an array of the results retrieved from the database. As you can see, if our query returns zero rows we define an error, if our query returns at least one row, we create an array with all the information about the company. The last public method in line 75, findBranches() will be the one, which we will call after instantiating our class and translating zip code into geographical length.

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
private function getResults($latlng)
{
  return mysql_query("SELECT *, (POW((69.1*(longitude-{$latlng[1]})*COS({$latlng[0]}/57.3)), '2') + POW((69.1*(latitude-{$latlng[0]})), '2')) AS distance FROM branches HAVING distance < {$this->radius} ORDER BY distance ASC;");
}
 
private function populateResults()
{
  if (!mysql_num_rows($this->data_set)) {
    $this->results = array(array('error' => 'none'));
  } else {
    while ($row = mysql_fetch_assoc($this->data_set)) {
      $this->results[] = array(
        'name'      => utf8_encode($row['name']),
        'zip'       => $row['zip'],
        'city'      => utf8_encode($row['city']),
        'latitude'  => $row['latitude'],
        'longitude' => $row['longitude'],
        'distance'  => round(sqrt($row['distance']),1),
      );
    }
  }
  return $this->results;
}
 
public function findBranches($latlng)
{
  $this->data_set = $this->getResults($latlng);
  return $this->populateResults();
}

Our class for retrieving results is done. For the simplicity I did not assume the case, when the database query returns zero rows, in which radius should be automatically extended. You can easily do this, by writing one more method, which will be responsible for extending the radius and adding a simple if...else check in our findBranches() method, to make it loop until your query returns any results. As I said before though, in a database with many entries, executing a web query with a formula complicated like the one we are using, retrieving results might consume a lot of time, and creating a loop, in which you will repeat the same query might lead to a really long time a user will have to wait for a response. In my case, the database was a little bit over 1000 entries and extending the radius and repeating the query did not result in any remarkable performance lost though.

The last part is the actual usage of the class we just wrote. Basically in line 84 we instantiate our class, in line 85 we translate the zip code to geographical length and according to the value returned by this method we use json_encode() function to return the results as a json representation of the array we just created or an error.

84
85
$branches = new Branches();
echo ($latlng = $branches->translateZipToLatLng()) ? json_encode($branches->findBranches($latlng)) : json_encode(array('error' => 'zip_invalid'));

Since our class for retrieving the results in a certain radius is ready, we should move to our client side code.

To make this simple, I will just show you the whole jQuery code and discuss it line by line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
$(document).ready(function() {  
  var mapContainer = $('#map')[0];
  if (GBrowserIsCompatible()) {
    if (mapContainer) {
      var map = new GMap2(mapContainer);
      map.addControl(new GLargeMapControl);
      map.setCenter(new GLatLng(52.519015140666,13.419671058654785), 14);
 
      $('#submit').click(function() {
        var zip = $('#zip').val();
        var radius = $('#radius').val();
        if (zip == '') {
          alert("You need to provide a zip code");
          return;
        }
        var rand = new Date().getTime();
        $.getJSON('zip.php?rand=' + rand, { zip: zip, radius: radius }, function(data) {
          var markers = [];
          var descriptions = [];
          var bounds = new GLatLngBounds();
 
          map.clearOverlays();
          $('#error').html('');
          $('#locations').html('');
          $('#closest_location').html('');
          var flag = true;
 
          $.each(data, function(i, item) {
            if (item.error == 'none') {
              alert('No results found or ZIP incorrect');
              $('#error').append('No results found or ZIP incorrect');
              flag = false
              return false;
            }
            var description =
              '<div class="branch" style="float: left;">' +
              item.name + ' (~ ' + item.distance + ' km)<br />' +
              item.zip + ' ' + item.city + '<br />' +
              '<span class="branch_show" style="cursor: pointer; color: blue; text-decoration: underline;" id="' + i + '">Show</span></div>';
            var lat = item.latitude;
            var lng = item.longitude;
            var point = new GLatLng(lat, lng);
            var marker = new GMarker(point);
            markers[i] = marker;
            descriptions[i] = description;
            $('#locations').append(description);
            map.addOverlay(marker);
            bounds.extend(point);
            GEvent.addListener(marker, 'click', function() {
              marker.openInfoWindowHtml(description);
            });
          });
 
          $('#closest_location').append(descriptions[0]);
 
          map.setZoom(map.getBoundsZoomLevel(bounds));
          // just in case we did not find anything
          if (!flag)
            map.setCenter(new GLatLng(51.08282186160976,10.26123046875),6);
          else
            map.setCenter(bounds.getCenter());
 
          $('.branch_show').click(function() {
            var id = $(this).attr('id');
            markers[id].openInfoWindowHtml(descriptions[id]);
          });
        });
      });
    }
  }
});

As usually we want to execute the code on onLoad of our document and we do that in line 1. In line 2 we define our container for the map and in line 3 and 4 we check if browser is compatible and if container was found respectively.

In line 5 we instantiate the Gmap2 inside of our map container, we add some controls in line 6 and set the center in line 7. In line 9 we add a event listener which triggers after a user clicks submit button.

In lines 10 to 15 we add a simple check, if the field with the zip code is not empty and it if is we alert an according message and we break the execution of the code. Additionally we define a zip variable in line 10 and radius variable in line 11, which will hold according information to use in our json request.

In line 16 we define rand variable which we pass during our json request to prevent internet explorer to cache the resource. If we would not do that, and changes you would do to zip.php file would not be visible, because IE would use the cached version of this file.

In line 17 we make a request to the zip.php file, which we wrote earlier and we pass the zip code and the radius to our php file. After the calculation is done and you do not have any errors in your php file we retrieve the information, which we can finally process.

In line 18 and 19 we define two variables as arrays, which will be responsible for holding the information about markers and their corresponding descriptions and in line 20 we instantiate GLatLngBounds object, which we will later use to display the information on our map with matching zoom level.

In line 22 to 26 we remove all markers and we empty all containers holding the information about the places from a previous request. We do that to prevent displaying of the same markers after a user requests the zip.php file again. This way, if a user changes his or hers query, we will be sure that we are displaying only the information relevant to the current request and we wont have any trash from the previous requests. In line 26 we define a flag as boolean to use it later to display error information and redisplay the center of the map.

In line 28 we loop through the information we received in response. If an error was found, we break our loop and we display and alert according information to the user. We also set the flag to false to redisplay center of the map later in the code. This would be lines 29 to 34.

In lines 35 to 39 we put together the HTML code which we will use to display the information about the branches we are going to display on the map.

In lines 40 to 43 we define two variables lat and lng and use them to instantiate GLatLng object which we define as point variable. In line 44 we define marker variable as an instance of GMarker object and in lines 45 and 46 we add it to the arrays we defined earlier. In line 47 we append the description to the container we need to define in our HTML and in line 47 we add markers to the map. In line 48 we extend the bounds of our map by using a method extend() of GLatLngBounds object we defined earlier. This amazing functionality of google maps will let us display a square with all our markers and google map will take care of setting the correct zoom level.

In lines 49 to 51 we add a listener which will be triggered on a click event and will display the description in an info cloud. In line 52 we close the loop and in line 54 we append the first description to the closest_location container. To assure that the first description is really the closest place to the zip code provided by the user, you cannot forget about putting the “ORDER BY distance ASC” in the php code of zip.php file.

In line 56 we set the zoom of our map using our bound object I was mentioning before by utilizing getBoundsZoomLevel method of GMap. As you can see in the description in line 57, if our php code did not return any results, we override the zoom level we defined in line 56 be setting it again to the default value (line 59). On the other hand, if our php code returned any results we set the center of the map using our bounds object again (line 61).

The last part of this code is just a listener which is triggered when a user clicks on the pseudo link show. We retrieve the id of this link and display the according info window on the map.

That would be it. The code is done. I know it is not perfect and accurate either. You can still use it in many different ways. It does not necessarily need to be a zip code search. Using it you can display distances between users and any other stuff connected with distances. In the second part I will show you how you can make this code a little bit more accurate using google maps build in api to show the directions, how to get from the zip code provided by the user to one of the company branches.

If you do not want to copy all the code, here is a zip package for you with working files.

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

(required)

No trackbacks yet.