A Google Maps Store Locator in Only Javascript

* UPDATE: I’ve re-worked this code and have created a jQuery plugin based on it.

Recently, I was tasked with creating a “store” locator for a local financial institution that has several branches in the Twin Cities area. The problem was that there wasn’t a chance I’d receive access to their server or database and would need to make a special request if I needed access to a server-side programming language. Because of a tight deadline, I decided not to bother working with a server-side language or a database and just create the locator with only Javascript. I knew I wanted to use the Google Maps API and figured there would be at least one example out there I could work off of. However, my presumption was incorrect and I ended up creating it myself along with some helpful snippets of code I came across along the way. It wasn’t an overly complex thing to complete but I figured I’d post it so that anyone else looking to do the same has an example to follow. I made the example very thorough in case you aren’t familiar with the Google Maps API or jQuery.

The locator that I ended up with used an XML file to store all of the location data, which I changed to local Chipotle locations for this example (there are quite a few in the area). I figured that because there weren’t more than 20 branches, I might as well just create the XML by hand and have all the geocoding done with the latitude and longitude in the file. There are obviously pros and cons to this and I wouldn’t recommend it if you have more than 20 or so locations. The speed is nice with everything pre-entered and geocoded and there really aren’t any security concerns, but it takes some time to type everything out. The code I came up with geocodes only the user’s location input but it could definitely be modified to geocode all the store locations as well if needed.

Note that because I’m familiar with Version 2 of the Google Maps API, I decided to use that instead of Version 3 so that I could quickly finish it. I may update this post with a Version 3 example at some point in the future.

I started by going to the Google Maps API site and looking at documentation for a PHP/MySQL example. The first portion this example just discusses how to create the XML file with PHP so, because I was already had the file completed and wasn’t looking to make one dynamically, I skipped down to the portion after “Checking that XML output works.” They give a useful example of what the XML should look like:

<markers>
   <marker name="Pan Africa Market" address="1521 1st Ave, Seattle, WA" lat="47.608940" lng="-122.340141" type="restaurant"/>
   <marker name="Buddha Thai & Bar" address="2222 2nd Ave, Seattle, WA" lat="47.613590" lng="-122.344391" type="bar"/>
   <marker name="The Melting Pot" address="14 Mercer St, Seattle, WA" lat="47.624561" lng="-122.356445" type="restaurant"/>
   <marker name="Ipanema Grill" address="1225 1st Ave, Seattle, WA" lat="47.606365" lng="-122.337654" type="restaurant"/>
   <marker name="Sake House" address="2230 1st Ave, Seattle, WA" lat="47.612823" lng="-122.345673" type="bar"/>
</markers>

The XML file I created by hand is at the following page.

http://www.bjornblog.com/map/locations.xml

The rest of the documentation is what I used as a guideline and the API reference page also came in handy. My first step was to load the XML file. Because jQuery can easily parse XML, and because I knew I was going to be using jQuery for some other things, I decided to include the library. jQuery is amazing and I’m using it on almost every new project. It’s easy to learn and I highly recommend it. Anyways, onto the parsing:

//Parse xml with jQuery
$.ajax({
type: "GET",
url: "locations.xml",
dataType: "xml",
success: function(xml) {

After the XML was loaded, all the data needed to be sucked out. I decided to use jQuery’s each() function to loop through the data and create a multi-dimensional array for each location.

var i = 0;
$(xml).find('marker').each(function(){
   //Take the lat lng from the user, geocoded above
   var lat = $(this).attr('lat');
   var lng = $(this).attr('lng');
   var name = $(this).attr('name');
   var address = $(this).attr('address');
   var address2 = $(this).attr('address2');
   var city = $(this).attr('city');
   var state = $(this).attr('state');
   var postal = $(this).attr('postal');
   var distance = GeoCodeCalc.CalcDistance(orig_lat,orig_lng,lat,lng, GeoCodeCalc.EarthRadiusInMiles);

   //Create the array
   locationset[i] = new Array (distance, name, lat, lng, address, address2, city, state, postal);

   i++;
});

Most of the code above should be straightforward. I’m taking the data from the XML file, assigning it do different variables, and creating the array. The one thing that you’re probably wondering about though is the GeoCodeCalc.CalcDistance function. I needed to find a way to calculate the distance from the user’s location to all of the company locations/branches. After a quick Google search I found a calculation script that seems to work perfectly on Chris Pietschmann’s blog. I used that exact code at the top of the file:

var GeoCodeCalc = {};
GeoCodeCalc.EarthRadiusInMiles = 3956.0;
GeoCodeCalc.EarthRadiusInKilometers = 6367.0;
GeoCodeCalc.ToRadian = function(v) { return v * (Math.PI / 180);};
GeoCodeCalc.DiffRadian = function(v1, v2) {
return GeoCodeCalc.ToRadian(v2) - GeoCodeCalc.ToRadian(v1);
};
GeoCodeCalc.CalcDistance = function(lat1, lng1, lat2, lng2, radius) {
return radius * 2 * Math.asin( Math.min(1, Math.sqrt( ( Math.pow(Math.sin((GeoCodeCalc.DiffRadian(lat1, lat2)) / 2.0), 2.0) + Math.cos(GeoCodeCalc.ToRadian(lat1)) * Math.cos(GeoCodeCalc.ToRadian(lat2)) * Math.pow(Math.sin((GeoCodeCalc.DiffRadian(lng1, lng2)) / 2.0), 2.0) ) ) ) );
};

The next step in the Google documentation moves on to creating the map but before that I needed to create a sorting function. Each of the locations in the multi-dimensional array have to be sorted by the distance from the user’s location. Sorting a multi-dimensional array seemed difficult at first but a co-worker pointed me in the right direction with the following simple function:

//Sort the multi-dimensional array numerically
locationset.sort(function(a, b) {
   var x = a[0];
   var y = b[0];
   return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});

Next, I needed to actually create the map. Google still uses the body onload() function in its Google Maps tutorials but since I was already including jQuery there’s a better way to handle it. Fellow Minnesotan developer Marc Grabanski has a great tutorial on his blog. I created the map using this method and load it into an empty div with an id of “map” that’s in the HTML body. The orig_lat and orig_lng variables are defined by the user’s input, which I worked on last (I used a set latitude and longitude for testing while setting everything else up) – I’ll cover that at the end.

//Create the map with jQuery
$(function(){
   var map = new GMap2(document.getElementById('map'));
   map.addControl(new GSmallMapControl());
   map.addControl(new GMapTypeControl());
   var center_location = new GLatLng(orig_lat,orig_lng);
   map.setCenter(center_location, 11);

   // Create a base icon for all of our markers that specifies the shadow, icon dimensions, etc.
   var letter;
   var baseIcon = new GIcon();
   baseIcon.shadow = "http://www.google.com/mapfiles/shadow50.png";
   baseIcon.iconSize = new GSize(20, 34);
   baseIcon.shadowSize = new GSize(37, 34);
   baseIcon.iconAnchor = new GPoint(9, 34);
   baseIcon.infoWindowAnchor = new GPoint(9, 2);
   baseIcon.infoShadowAnchor = new GPoint(18, 25);

After the basic map is set up the markers need to be added. I created a simple for loop that goes through the locationset multi-dimensional array and creates the markers with the latitude and longitude of each location. At the end of the loop the markers are put into an array.

var markers = new Array();
for (var y = 0; y <= locationset.length-1; y++)
   {
      var letter = String.fromCharCode("A".charCodeAt(0) + y);
      var point = new GLatLng(locationset[y][2], locationset[y][3]);
      marker = createMarker(point, locationset[y][1], locationset[y][4], letter);
      map.addOverlay(marker);
      markers[y] = marker;
   }

Once the map and markers were sorted out, I wanted to create an ordered list of locations with the location name, address, etc. In the body HTML code I created an empty alphabetical ordered list with a containing div and another container div around both the list and map (I ended up putting the entire thing inside of a colorbox for the financial institution). Back to the JS, I first make sure the list is empty, with jQuery’s empty() function, to make sure that the list resets if someone enters a new location after already submitting the form. I then used jQuery’s each() function to loop through and create a list item with the proper information for each marker that exists. I wanted to have the map center on the location if a user clicks on one of the list items so I used jQuery’s click() function, which points to a fairly standard Google Maps displayPoint function. The list is then appended to the empty ordered list in the body. The last small function still within the each() iteration makes it so that the map centers on a marker when you click a marker on the map.

//Creat the links that focus on the related marker
$("#list").empty();
$(markers).each(function(y,marker){
   //HTML List to appear next to the map
   $('<li />').html("<div class=\"loc-name\">" + locationset[y][1] + "<\/div> <div class=\"loc-addr\">" + locationset[y][4] + "<\/div> <div class=\"loc-addr2\">" +    locationset[y][5] + "<\/div> <div class=\"loc-addr3\">" + locationset[y][6] + ", " + locationset[y][7] + " " + locationset[y][8] + "<\/div>").click(function(){
      displayPoint(marker, y);
   }).appendTo("#list");

   GEvent.addListener(marker,"click", function(){
   map.panTo(marker.getLatLng());
   });
});

The displayPoint function:

//Move the map
function displayPoint(marker, index){
   var moveEnd = GEvent.addListener(map,"moveend", function(){
      var markerOffset = map.fromLatLngToDivPixel(marker.getLatLng());
      GEvent.removeListener(moveEnd);
      });
   map.panTo(marker.getLatLng());
}

The next one line function alternates the list item background color. I occasionally use the jQuery alternate plugin to do this. It probably isn’t necessary, as there are multiple ways to accomplish this, but it just makes things easy.

//Alternate the list colors
$('#list li').alternate();

The last function in the main mapping function creates the custom icon based on the baseIcon that was previously set up. It is basically just the standard marker with a letter added.

//Custom marker function - alphabetical
function createMarker(point, name, address, letter) {
   var letteredIcon = new GIcon(baseIcon);
   letteredIcon.image = "http://www.google.com/mapfiles/marker" + letter + ".png";

   markerOptions = { icon:letteredIcon };
   var marker = new GMarker(point, markerOptions);

   return marker;
}

Once I had all this done and tested with a set latitude and longitude I needed to actually create the HTML form to allow users to input their location. In the body HTML you’ll see the code for the very basic form. I’m didn’t use a standard submit button mostly out of habit. In most cases it’s easy for a bot to take advantage of an HTML form and even though it really doesn’t matter for this functionality (since it only sends a string of data to Google). I prefer to do form submissions this way when I’m not working with a CMS. This method also makes some basic validation with JS easy.

So, back at the top of the file, you’ll see a function that handles the input. I used the jQuery serialize() function to capture the data that is submitted. I then used standard JS to replace the “address=” which is taken from the input name with the serialize function. I then did a basic check for blank input and the GoogleGeocode function geocodes the data. In the next few lines, I set the geocoded input to the olat and olng variables and then run the main mapping function. At first I didn’t separate the input from the rest of the mapping and I would get errors because the mapping would finish before the input was geocoded.

//Process form input
$(function() {
	$('#submit-btn').click(function() {
		var userinput = $('form').serialize();
		userinput = userinput.replace("address=","");
		if (userinput == "")
			{
				alert("The input box was blank.");
			}

			var g = new GoogleGeocode(apiKey);
			var address = userinput;
			g.geocode(address, function(data) {
				if(data != null) {
					olat = data.latitude;
					olng = data.longitude;
					mapping(olat, olng);
				} else {
					//Unable to geocode
					alert('ERROR! Unable to geocode address');
				}
			});

//Replace spaces in user input
userinput = userinput.replace(" ","+");

	});
});

To accomplish the geocoding I found a function on Petar Strinic’s blog, which I modified a bit. Basically, all I wanted to be returned from Google was latitude and longitude. All the rest wasn’t necessary because I had that data for all my locations. The modified geocoding code looks like this:

//Do the geocoding
function GoogleGeocode(apiKey) {
	this.apiKey = apiKey;
	this.geocode = function(address, callbackFunction) {
		$.ajax({
			dataType: 'jsonp',
			url: 'http://maps.google.com/maps/geo?output=json&oe=utf8&sensor=true'
					+ '&key=' + this.apiKey + '&q=' + address,
			cache: false,
			success: function(data){
				if(data.Status.code==200) {
					var result = {};
					result.longitude = data.Placemark[0].Point.coordinates[0];
					result.latitude = data.Placemark[0].Point.coordinates[1];
					callbackFunction(result);
				} else {
					callbackFunction(null);
				}
			}
		  });
	};
}

I think that covers just about everything. You’ll notice in the center of the mapping function a block that is commented out. I used this simply for testing – it basically appends all the data from the locationset array in an HTML block. This was useful when I was looking at the calculation numbers and to make sure the markers were corresponding with the correct set of data in the multi-dimensional array. Feel free to use, expand, or improve. I’ll try to answer questions if you have them.

View the entire JS file
View a demo
Download the demo (the Google Maps API key needs to be updated in 2 spots in the index.html file)