Last week I was able the get the Beer Locator function of my Beer App up and running. The Beer Locator allows Beer App users to enter an address while viewing a beer. A Google Map will return; displaying any bars/restaurants, retail stores or bottle shops within a 25 mile radius, that stock or serve the selected beer. To make all this work, I had to invoke the power of the Google Maps API. This was my first experience using the API. Since I ran into a few bumps along the way, I figured I’d take some time to walk you; the soon to be enlightened reader, through the process of using the Google Maps API to create a store locator.
I started off by reading Google’s Creating a Store Locator article. This is one of the few articles they have released for version 3 of their API. I decided to go with V3 over the tried and trusted V2 because of Google’s claims about the speed improvement. V3 is supposedly much more streamlined than V2; resulting in better load times, especially on mobile devices. This is important to me, as I plan to port the Beer App to mobile platforms in the future.
Next, I entered the design tank. I needed to outline how my Beer Locator was going to work. I decided to base the Locator off the Beer Information page. When the Beer Information page loads, I want to check if there are any known locations for the beer. Then I want to give the user the option to view locations near them. This is where I made my first big decision. I can either give the user the option to enter their address, or I can have them associate an address to their user account; allowing only registered users access to the Beer Locator. Being a proponent of privacy, I decided to go with the first option. I don’t want to force people to register, nor do I want to force those who do register to be required to give me their address information. I made a note than when go back to writing the User Login function, I will give registered users the option to assign locations to their account. Users who choose this option will see a dropdown box with their preset locations instead of a text input field; negating the need to retype their address for each search. This should keep all the camps from warring.
Once I have the user’s address, I need to geocode it; find the latitude and longitude. This will be the first process that uses the Google Maps API. After the coordinates are found, I need to compare them to the coordinates of all locations where the beer they are searching is available. While doing this, I also need to decide which locations are close to the user. For this to work, I will need to have the latitude and longitude of all locations stored in my database. I decided to hard code a 25 mile radius into my locations query. I may change this distance in the future, or give users a selection of distances. In any case, for testing, it is 25 miles. After finding all locations that meet the distance requirement, I need to display them to the user. This is the second place the API will be used. I will display the results as both markers on a Google Map and as an HTML table. Most of the articles Google provides about their API suggest storing query results in an XML file and using javascript to parse the file and pass information to the API. I did not want to deal with having users create XML files, so instead I will be deviating from the suggested method. I will be putting most of the javascript that interacts with the API in the page headers and calling it directly from the PHP.
With a rough outline in place, I could start creating the database tables that will hold the location information. Since I already had a beer table, I added a location table and a beer/location cross reference table.
The location table has the following layout:
location id – integer field, auto incrementing
location name – character field, location name
location address – character field, address
location type – integer field, type of business (retail store, restaurant, etc)
location latitude - decimal field, latitude
location longitude – decimal field, longitude
location website – character field, web address
location phone number – character field, formatted phone number
location user – integer field, id from user table of user who submitted location
location active – bit field, y = active location, n = inactive location
The cross reference table has the following layout:
beerloc id – integer field, auto incrementing
beerloc beer id – integer field, beer id from beer table
beerloc location id – integer field, id of location from location table
beerloc price – decimal field, price of beer at this location
beerloc type – integer field, type of container (22oz. bottle, 6 pack 12oz. bottles, on tap)
beerloc date – date field, last date the beer was found at the location
beerloc user – id from user table of user who submitted beer
beerloc active – bit field, y = active location, n = inactive location
I also created 2 addition reference tables that tie the integer values from location type and beerloc type to text descriptions. Once the tables were created, I inserted a few entries so there was data to test with.
Next, it was time to start working on the pages. First, I added a MYSQL query to my existing beer information page. The new query reads the beerloc table for all entries where the beer id field matches the id field from the beer table of the beer being viewed. If there are any matches, my PHP code adds an extra row to the HTML table; telling the user the number of locations where the beer is available. A form is also added that contains a text input for the user’s address and a “Locate” button that fires the onclink event.
echo “<td><input type=\”text\” id=\”name_of_variable_for_user_address\” size=\”10\”/>”;
echo “<input type=\”button\” onclick=\”name_of_geocode_address_function({$beer_id},{$search_type})\” value=\”label_to_be_displayed_on_button\”/></td>”;
When the user enters an address and clicks the ‘Locate’ button, the onclick event is fired. This calls the javascript function named in the event, which geocodes the user’s address. This is where WordPress first started acting like a little bitch. I needed to put some javascript in the header of the Beer Information page to include the definitions for the Google Maps API and I needed to create the function that is called by the onclick event. By default, you cannot place code in the header area of a page with WordPress’ interface. You can put code in the header.php file (or your theme’s equivalent), but this is not the optimal answer, since anything placed in header.php will execute on every page of your site. I did some searching around and found a plugin called Head Meta. This plugin give you the option to create a custom field for a particular page called head-meta. Anything placed in this field will be inserted into the header area of that page. Stupidly unnecessary that a plugin is required, but workable. Thanks to John Blackbourn for the plugin. And thanks to WordPress for once again being almost useable. With my header issue sorted, I was ready to write my function that will geocode the address.
First, I needed to set up the environment for the API.
<script src=”http://maps.google.com/maps/api/js?sensor=false”></script>
This javascript tag points to the js file that contains the definitions for the API. The sensor parameter is used to set whether or not you are using a sensor to track the user’s position.
Next, I wrote the function for geocoding the user’s address.
<script>
function name_of_geocode_address_function(beer_id, search_type){
var address = document.getElementById(“name_of_variable_for_user_address”).value;
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { ‘address’: address}, function(results, status) {
The function first retrieves the user’s address from the text box, into variable named ‘address’. Next, the geocoder is initialized and passed the user’s address. I also pass it a function that is called if the geocoder is successful.
if (status == google.maps.GeocoderStatus.OK) {
var lat = results[0].geometry.location.lat();
var lng = results[0].geometry.location.lng();
location.href = “../page_where_results_are_determined?lat=” + lat + “&lng=” + lng + “&beer_id=” + beer_id +”&search_type=” + search_type;
}
});
}
</script>
If the geocoder returns a status of ‘OK’, I retrieve the user’s latitude and longitude from the geocoder’s ‘results’ array. Since I am only passing one address to the geocoder, all of the information being returned will be in the first dimension of the array. After placing the latitude and longitude into variables, I use location.href to call the Locations page. This page is where the locations query is performed. Since this is WordPress, I have to start the URL path with ‘../’ to get me back to the root of my site.
Next I started work on the Locations page. Since I am using the API again, I started off by putting the same javascript tag in the header; or in my case, the head-meta field. I also added a meta tag that sets up my map to run at full screen. The ‘user-scalable=no’ option prevents it from being resized.
<meta content=”initial-scale=1.0, user-scalable=no” />
<script src=”http://maps.google.com/maps/api/js?sensor=false”></script>
At the top of the page’s body, I added a div element where the map will be displayed.
<div style=”width: 100%; height: 300px;”></div>
This element reserves a space for the map and sets its size. I set my map width to 100% of the available width and the height to 300 pixels.
Before I can start my query, I need to get the user’s latitude and longitude into variables. Since they were passed to this page by location.href, they can be retrieved with the ‘GET’ function.
$user_lat_from_previous_page = @$_GET['lat'];
$user_lng_from_previous_page = @$_GET['lng'];
The ‘GET’ function is also used to retrieve beer_id and search_type. Search_type is a variable I pass to every page of the Beer App. It contains a value that tells the app whether the user has search type set to ‘basic’ or ‘advanced’. This information is passed back to the Search Page when the ‘new search’ button is press. The user is presented with the same type of search they had previously. With these variables, I am able to set up my query. I’m not going into detail about the SQL query statement, but I will share the piece of the select that checks to see if the location is within 25 miles of the user.
SELECT…( 3959 * acos( cos( radians({$user_lat_from_previous_page}) ) * cos( radians( location_lat_from_db ) ) * cos( radians( location_lng_from_db ) – radians({$user_lng_from_previous_page}) ) + sin( radians({$user_lat_from_previous_page}) ) * sin( radians( location_lat_from_db ) ) ) ) AS distance…
This piece of the select statement was taken directly from Google’s Creating a Store Locator article. Using the formula above, distance is the number of miles between the user and the location being queried. Google’s article also gives the value that can be substituted in place of ‘3959’ to convert the search to kilometers. To limit locations to those within 25 miles, the following is added to the statement.
…HAVING distance < 25…
The number can be changed to any value. If I decide to allow users to select the distance of their search, it will be replaced with a variable containing that distance.
Now that the query is complete, I need to generate the Google Map and turn the resulting locations into markers. I start by setting up the map variable, setting up the ‘infoWindow’ variable and centering the map on the user’s address.
echo ‘<script>’;
echo ‘var map = new google.maps.Map(document.getElementById(“name_of_your_map”), {‘;
echo “center: new google.maps.LatLng({$user_lat_from_previous_page}, {$user_lng_from_previous_page}),”;
echo ‘zoom: 12,’;
echo ‘mapTypeId: google.maps.MapTypeId.ROADMAP’;
echo ‘});’;
echo ‘var infoWindow = new google.maps.InfoWindow;’;
I created the variable ‘map’ as a new google map. I gave it an id that matches the id used in the div element. In the ‘center’ option, I pass the user’s latitude and longitude. The ‘zoom’ option seta the initial zoom level and the ‘mapTypeId’ option displays the map as a road map. I also created a variable for the InfoWindow. The InfoWindow is used to create tags that appear when users click on map markers.
Next, I went back to the header and created two functions; one to insert map markers and one to create information windows for the markers.
function marker_function (id, name, address, site, phone, icon, lat, lng, map, infoWindow) {
var html = name + “<br> ” + “<a href=\”http://” + site + “\”>” + site + “</a>” + “<br>” + address + “<br>” + phone;
var point = new google.maps.LatLng(
parseFloat(lat),
parseFloat(lng));
var marker = new google.maps.Marker({
map: map,
position: point,
title: name,
icon: icon
});
bindInfoWindow(marker, map, infoWindow, html);
}
function bindInfoWindow(marker, map, infoWindow, html) {
google.maps.event.addListener(marker, ‘click’, function() {
infoWindow.setContent(html);
infoWindow.open(map, marker);
});
}
The first function takes all the information retrieved from the MYSQL query and creates a map marker. First it formats several pieces of information into the variable ‘html’. Next, it creates a LatLng point from the location’s latitude and longitude. Then a marker is created using the LatLng point and the variables that were passed into the function. The title option assigns a name that is displayed when the cursor is placed over a marker. The icon option is used to specify the URL of an image to display as the marker. If no icon path is specified, the default red push-pin is used. Finally, the bindInfoWindow function is called to create an information window for the marker. The bindInfoWindow function binds the information window to a click event for the marker. When the marker is clicked, the window displays the information in the ‘html’ variable.
Back in the PHP code, I call the marker function for the first time to create a marker for the user.
echo “marker_function(”,’Thirsty User’, ”, ”, ”, ‘http://location.of.image.for.user.com’, ‘{$user_lat_from_previous_page}’, ‘{$user_lng_from_previous_page}’, map, infoWindow);”;
This call to ‘marker_function’ creates a marker named ‘Thirsty User’ at the LatLng point of the user’s address. I also pass it the URL of the image I want to use for the marker icon. The rest of the parameters are not used for the user’s marker.
Next, I set up a fetch statement to retrieve the results from the location query. Inside the fetch statement, I make a call to ‘marker_function’.
while ($stmt->fetch()){
echo ” marker_function(‘{$location_id}’, ‘{$location_name}’, ‘{$location_address}’, ‘{$location_site}’, ‘{$location_phone}’, ‘http://location.of.image.for.location.com, ‘{$location_lat}’, ‘{$location_lng}’, map, infoWindow);”;…
Since this call is made within a fetch statement, it will be executed for each result the query returns. All the location information is passed to ‘marker_function’, which in turn creates the marker and binds the information window.
I now have a working store locator. Like I mentioned in the design area, I am also displaying the results in an HTML table below the Google Map. I chose to include a table for a couple reasons. First, there are still some browsers that cannot properly display a Google Map. Secondly, I want to display some information that does not translate well to a map. My query retrieves beer price, serving type and date last updated; comparative data than makes more sense in a table, rather than individual information windows. To get the information needed for this table, I create an array before executing the MYSQL query. During the fetch statement, I format the variables I want to display into a table row and save the information in the array. Once all the results have been fetched, I create a table and insert all the array information.
All in all, the process of creating the Beer Locator was a smooth affair. WordPress tried to fight me a couple times and I had to brush up on my Javascript. Jesse provided his expertise a few times when I got stumped. The Beer Locator is currently up and running in the Beer App. I only have a couple locations in Eugene, Oregon mapped so far. At this time, the following beers have been mapped to at least 1 location.
Total Domination – Ninkasi Brewing
Oakshire Amber – Oakshire Brewing
Overcast Espresso Stout – Oakshire Brewing
Watershed IPA – Oaksire Brewing
Hazelnut Brown Nectar – Rogue Ales
The links above will take you to Beer Information pages where you can perform a search. You can enter ‘97401’ in the address box to perform a search of locations in the Eugene area. As always, comments and suggestions are welcome. If you are having issues with a similar project and need some help, you can post here or shoot an e-mail to kevin@beerandcoding.com. I am more than willing to offer my limited insight.
Cheers!
Kevin