OK, you’ve learned to create a basic Google map and call in a static XML file. Here’s my advanced tutorial on the Google Maps API that I gave at the 2009 Investigative Reporters and Editors CAR conference in March.
Now let’s move on to the advanced stuff. We’ll cover:
- Generating XML using PHP/MySQL
- Dynamic geocoding
- Querying with geo data
- Polylines and polygons
- KML overlays
- Ground overlays
- Clustering markers
Presumed knowledge:
HTML
Moderate to advanced Javascript understanding
CSS
XML
PHP
MySQL
Google Maps API, including:
- Initialization of map
- Adding map controls
- Basic adding of points, click event listeners
- Display of map points from flat XML
- Generating XML using PHP/MySQL
We’re starting with a basic map page with basic controls:
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” xml:lang=”en” lang=”en”>
<head>
<meta http-equiv=”content-type” content=”text/html; charset=utf-8″ />
<title>My awesome advanced Google map</title>
<meta name=”generator” content=”BBEdit 8.2″ />
<!–mikejcorey.com key–>
<script src=”http://maps.google.com/maps?file=api&v=2&key=[your key here]&sensor=false” type=”text/javascript”></script>
</head>
<body onunload=”GUnload()”><div id=”map” style=”width : 500px ; height : 400px ; border : 1px solid #000 ;”></div>
</body>
<script language=”Javascript”>//<![CDATA[
if (GBrowserIsCompatible()) {
var map = new GMap2(document.getElementById("map"));
map.setCenter(new GLatLng(41.588822, -93.620309), 11);
map.addMapType(G_PHYSICAL_MAP);
map.setMapType(G_PHYSICAL_MAP);
map.addControl(new GSmallMapControl());
map.addControl(new GMenuMapTypeControl());
}
else {
alert("Sorry, the Google Maps API is not compatible with this browser");
}
//]]>
</script>
</html>- Here’s the basic way to create a marker:
var point = new GLatLng(41.55, -93.63);
var marker = new GMarker(point);GEvent.addListener(marker, ‘click’, function() {
marker.openInfoWindowHtml(“hi there!”);
});map.addOverlay(marker);
- That’s only useful to place one marker at a time, because if you write another one the event listener will overwrite the previous one. So let’s place a marker using a function instead of doing it short form:
function createMarker(point,html) {
var marker = new GMarker(point);
GEvent.addListener(marker, ‘click’, function() {
marker.openInfoWindowHtml(html);
});
return marker;
}var point = new GLatLng(41.55, -93.63);
var marker = createMarker(point,”Hi there!”);
map.addOverlay(marker); - Now that we have a function we can run over and over, we want to grab points from a larger data set. In our case we’re going to use XML. The process for doing that is a bit of a leap, and it could well take us the rest of the class getting everyone up to speed on that, so I’m going to give you the code for this, and walk you through it, but we’re not going to type it out longhand.
function loadMyLocations(whichxml) {
GDownloadUrl(whichxml, function(data) {
var xml = GXml.parse(data);
var locations = xml.documentElement.getElementsByTagName(“location”);
for (var i = 0; i < locations.length; i++) {
var restaurantname = locations[i].getAttribute("name");
var restaurantdescription = locations[i].getAttribute("description");
var latitude = parseFloat(locations[i].getAttribute("lat"));
var longitude = parseFloat(locations[i].getAttribute("long"));
markerlabel = "<div style='width : 200px ;'><b>" + restaurantname + "\</b><br>" + restaurantdescription + "\</div>";
var dynamicpoint = new GLatLng(latitude,longitude);
var dynamicmarker = createMarker(dynamicpoint,markerlabel);
map.addOverlay(dynamicmarker);
}
});
} - And then we’re going to run that function after we’ve loaded the map, like so:
loadMyLocations(“sampleXMLgenerator.php”);
- So now let’s take a look at the XML that’s driving this. You’ll notice that it’s a PHP file instead of a standard .xml file, because we’re using PHP to dynamically generate the XML. But first let’s look at what Google Maps is seeing by calling this up in your browser:
- Now let’s look at our process for using a PHP/MySQL database system to generate XML, which is our primary way of making information available to maps at DesMoinesRegister.com. Using a database instead of a static XML file allows you to easily change data without having to reformat XML. We also use PHP to do a lot of data crunching that we would otherwise have to do on the client side, using Javascript. Any time users want to search within your set of records, it’s much more efficient to send the search to PHP/MySQL, and then return only the results the user needs in the XML. Large XML datasets take a relatively long time for your users’ browsers to parse through.The New York Times whiz kids use JSON for all of their dynamic data display, and report that it’s much faster to be read for the extremely large datasets they generally work with. There’s a few reasons we still use XML. First, we don’t have those super-huge datasets to worry about (displaying every U.S. county border is fairly routine for them). Second, PHP, at least historically is very slow to write JSON (they use Django to build their JSON, I believe). Finally, they do almost all their work in Flash. Actionscript 3 is very good at parsing JSON quickly, and if we were in their shoes, we’d probably do it too.
But back to PHP. PHP has some great DOM-building functions to create a document that’s an XML document as far as the browser is concerned. Note: You must be using PHP 5 to use the DOM functions.
So let’s walk through this PHP file step-by-step. Here’s the general process:
- Get a database connection
- Run your query
- Create a new DOM document using PHP functions
- Loop through the query results, creating XML nodes for each result
- Return the XML document
<?php$dbname = “XXXX”;
require “nicar_db.php”;
$skeyword = “”;
$fkeyword = “”;if(!empty($_GET['keyword'])) {
$skeyword = mysql_real_escape_string($_GET['keyword']);
$skeyword = str_replace(“‘”,”",$skeyword);
}$query = “”;
$query = “SELECT * FROM nicar2009xml WHERE (lat is not null AND lat != ”)”;
if ($skeyword!=”") {
$query .= ” AND (name LIKE ‘%$skeyword%’ OR description LIKE ‘%$skeyword%’ OR address LIKE ‘%$skeyword%’ OR city LIKE ‘%$skeyword%’)”;
}
$dom = new DOMDocument(“1.0″);
$node = $dom->createElement(“NICARtestlocations”);
$parnode = $dom->appendChild($node);
header(“Content-type: text/xml”);if(!($result = mysql_query($query,$connection))) showerror();
// Iterate through the rows, adding XML nodes for each
while ($row = mysql_fetch_array($result)) :
$display_id=”";
$display_name=”";
$display_address=”";
$display_city=”";
$display_description=”";
$display_lat=”";
$display_long=”";
$display_id=$row['id'];
$display_name=$row['name'];
$display_address=$row['address'];
$display_city=$row['city'];
$display_description=$row['description'];
$display_lat=$row['lat'];
$display_long=$row['long'];
$node = $dom->createElement(“location”);
$newnode = $parnode->appendChild($node);
$newnode->setAttribute(“id”,$display_id);
$newnode->setAttribute(“name”,$display_name);
$newnode->setAttribute(“address”,$display_address);
$newnode->setAttribute(“city”,$display_city);
$newnode->setAttribute(“description”,$display_description);
$newnode->setAttribute(“lat”,$display_lat);
$newnode->setAttribute(“long”,$display_long);
endwhile;echo $dom->saveXML();
function cleantext($txt) {
$txt = str_replace(“””,”‘”,$txt);
return $txt;
}?>
- You’ll notice that the query is somewhat overcomplicated. This is because we can use that query to allow the user to search against the database and return only the points they need. This is where PHP greatly increases performance by letting the server doing the search work instead of the user’s browser.
So let’s write a simple search function for our Google map. You’ll see that we basically run the same function as before, but we add a query string to the end of the PHP URL, which is detected by the PHP. This function is then called when the user submits the Web form above the map:
function searchDatabase() {
loadMyLocations(“sampleXMLgenerator.php?keyword=” + document.keywordform.keywordinput.value);
} - And now you’ll see we’ve created a search box that calls that function:
<form name=”keywordform” action=”Javascript:;” onSubmit=”Javascript:searchDatabase(); return false”>
<b>Search the data:</b>
<input name=”keywordinput” style=”width : 300px ;”></input>
<input type=”submit” value=”Do search”></input>
</form> - I’m not going through the PHP too much, but it’s important to note at least one piece of the keyword detecting portion of the PHP. The mysql_real_escape_string function is a built-in PHP function that prevents many common SQL-injection attacks by testing whether the user input seems to be a legitimate text request or something that look like MySQL code. ANY VARIABLE THAT IS CREATED BY USER INPUT SHOULD BE TESTED IN THIS MANNER.
$skeyword = mysql_real_escape_string($_GET['keyword']);
- One more note: When you reload the XML, you’ll want to clear your previous markers to make room for the new ones, which may be duplicates. Just before your loop where you start adding new markers:
map.clearOverlays();
- Here’s the basic way to create a marker:
- Dynamic geocoding
Users often will want to figure out how your data relates to their own geographic location. They want to search against an address or by clicking on a point on the map. To do this kind of functionality you’ll need to use Google’s gecoder function.
- Create an instance of the geocoder to use for your searches. This should be placed in the code after you create your map but before you run any functions that might need it.
geocoder = new GClientGeocoder();
- Now let’s put the function to use. Whereas most of our actions are called against the map instance, in this case we use the geocoder. Lets do a really basic manual address query that you might collect from an HTML form. You’ll see that Google takes the address and returns a point — formatted as a lat,long pair — if it finds something, and returns as undefined if it can’t geocode the address. In this case, if it does find an address, we’ll center the map on that point at zoom level 10.
geocoder.getLatLng(“Your address here”, function(point) {
if (!point) {
alert(address + ” not found”);
}
else {
map.panTo(point,10);
}
}); - Now let’s ask Google for a lat-long pair based on a user’s click. This allows a user who doesn’t know the address for their desired location to still use a proximity feature. This involves applying an event listener to the entire map. When a user clicks on the map, we first make sure they’re not clicking on a marker. If they are, we let that marker’s previously set click handler do its work and do nothing in our function. If it’s not a marker, we re-center the map and do whatever else we might want to. Often we’ll want to pass the geocoded point to another function.
GEvent.addListener(map, ‘click’, function(marker,point) {
if (marker) {
}
else {
map.panTo(point,10);
var marker = new GMarker(point);
map.addOverlay(marker);
marker.openInfoWindowHtml(“Pssst, pass it on: ” + point);
}
});
- Create an instance of the geocoder to use for your searches. This should be placed in the code after you create your map but before you run any functions that might need it.
- Querying with geo data
At the basic level, you could leave any data you’re displaying on the map as-is and move the map’s center to wherever the user’s click or address search led them. But for large datasets, you can speed things up tremendously by using that point you retrieved to query against your own database, then returning a new XML document with only the points that are near the specified point.
I’m not going to dive deeply into that process because the query itself is complicated. Also, we use MySQL because we don’t currently have access to PostgreSQL, but PostgreSQL would be the preferred database to do this with. But here’s the process:
- Get a user-entered point from a click or search, as we did above.
- Send that point to the PHP/XML page as part of a query string, like we did for our other search, something like this:
function searchDatabase() {
loadMyLocations(“sampleXMLgenerator.php?location=” + point);
} - Have the PHP collect that variable and send it back to your database as part of a query.
- Your database can then use a geo query to return each of the points in the database that are within a given distance to your submitted point
- The PHP page writes XML with the newly returned points.
- You clear your old overlays and replace them with those from the new XML.
- Polylines and polygons
Markers are the most commonly used overlays in the Google Maps API, but polylines and polygons are next. Polylines are familar to most Google Maps users from the driving directions feature, but you can use them for other applications. Polygons are useful for showing regions and other non-point data.
Please don’t think you can throw up a Google map showing all the counties in your state. It will be slow, slow slow — this is a major failing of the Javascript API to me, and is one of the main reasons we’re planning to move to Google Maps for Flash where we can.
That said, polygons used one at a time or a few at a time work well. Let’s start by drawing a basic line:
- We’ll start by putting some points into an array:
var lineArray = new Array();
lineArray.push(new GLatLng(41.5733, -93.7655));
lineArray.push(new GLatLng(41.6216, -93.7078));
lineArray.push(new GLatLng(41.6164, -93.5911));
lineArray.push(new GLatLng(41.5548, -93.5128)); - Then we’ll spit those points back out as a line. We specify an array that’s the source of the points, the color of the line, the line width, and the opacity of the line (0.1 to 1):
var myline = new GPolyline(lineArray, “#446891″, 3, 1);
map.addOverlay(myline); - Polygons work pretty much the same, except that the last point in the array should be the same as the first one to close the polygon, and you specify a color and opacity for the fill color in addition to the stroke color and opacity:
var polygonArray = new Array();
polygonArray.push(new GLatLng(41.5733, -93.7655));
polygonArray.push(new GLatLng(41.6216, -93.7078));
polygonArray.push(new GLatLng(41.6164, -93.5911));
polygonArray.push(new GLatLng(41.5548, -93.5128));
polygonArray.push(new GLatLng(41.5733, -93.7655));var mypolygon = new GPolygon(polygonArray, “#446891″, 3, 1, “#99C1D8″, 0.2);
map.addOverlay(mypolygon);
- We’ll start by putting some points into an array:
- KML overlays
You can also overlay points, polylines and polygons from Google Earth that have been saved as a KML file. If you don’t have a very robust database setup at your location, this can be a good way to create large collections of points or storing complicated polylines or polygons. In my experience you want to keep your KMLs that you use for this as simple as possible, though, because not all features seem to be 100% supported by Google maps.
- First, save a KML from Google Earth. You do this by drawing a line in Google Earth, then CNTRL-clicking on the line (or a folder of points and lines if you have more than one object you want to overlay) in your “Places” menu. Select “Save place as,” then change the file format from KMZ (which is a zipped file) to KML.
- Upload your KML to your Web server. Please note that if you’ve made mistakes and have to change your KML file, you may not see those changes for a while even after you upload because Google’s servers cache KML requests for as long as an hour.
- Now overlay the KML onto your map:
var routekml = new GGeoXml(“http://www.mikejcorey.com/nicar2009/mykml.kml”);
map.addOverlay(routekml);
- Ground overlays
You can also overlay an image onto the map. You do this by specifying the lat-long coordinates that you want the corners of your image to be achored to.
We mostly use this to throw up a transition slide that says “map updating” while we’re retrieving points from the server. We’ve never used it for real content as far as I can remember because other than placing an image on the map, it’s not that useful. You can’t really do anything with it.
Ground overlays could be useful if you have an area that you’ve made a custom map for like plans for a new subdivision or something like that and don’t want to create real custom map tiles. The tricky part is that your image must be drawn exactly to scale to show up well, and this takes some experimentation.
I think ground overlays are actually a lot more useful in Flash, because in addition to images you can overlay Flash movie clips, which are extremely flexible and customizable.
- To deploy a ground overlay, all you really need is the lat-long corners for your image and an image on a server.
var boundaries = new GLatLngBounds(new GLatLng(41.5764, -93.6968),new GLatLng(41.6103, -93.6227));
var reggieoverlay = new GGroundOverlay(“reggiehead.png”, boundaries);
map.addOverlay(reggieoverlay); - Here’s how we make a simple “map updating” slide using a ground overlay. As you make your call to the server for new XML, you put up the image. You then remove the overlay once the function is finished looping through all the points.
var boundaries = map.getBounds();
var oldmap = new GGroundOverlay(“reloadslide.png”, boundaries);
map.addOverlay(oldmap);map.removeOverlay(oldmap);
- To deploy a ground overlay, all you really need is the lat-long corners for your image and an image on a server.
- Clustering markers
Many times you’ll have a lot of points that at wide zoom levels are too close together to distinguish. Using marker clustering is generally the best way to get around this. Google has built-in clustering functions, but we like one called ClusterMarker better because it has more flexibility. We use ClusterMarker even when we’re not clustering icons because it makes it easy to trigger clicks to a particular point.
You can download the plugin here:
http://googlemapsapi.martinpearman.co.uk/articles.php?cat_id=1
- In your HTML document’s <HEAD>, import the Javascript file that contains the clustering functions:
<script type=”text/javascript” src=”http://www.mikejcorey.com/nicar2009/ClusterMarker.js”></script>
- Create a custom icon to be used as your clustering icon. I won’t go through the whole process for this, but you essentially create a PNG image with transparent background, create another PNG for the shadow of that icon, and then tell Google maps what dimensions to use for that icon:
var groupicon = new GIcon();
groupicon.image = “severalicon.png”;
groupicon.shadow = “shadow-severalicon.png”;
groupicon.iconSize = new GSize(41.0, 20.0);
groupicon.shadowSize = new GSize(52.0, 20.0);
groupicon.iconAnchor = new GPoint(20.0, 10.0);
groupicon.infoWindowAnchor = new GPoint(20.0, 10.0); - Create a new ClusterMarker instance for your map. We’ll also tell ClusterMarker what icon to use to mark a cluster. We can also easily turn the clustering on and off by setting the second line to true or false:
var cluster=new ClusterMarker(map, { clusterMarkerIcon: groupicon });
cluster.clusteringEnabled=true; - Create an array before you run your loop through the XML or other source file to store your icons until they’re put into the cluster function all at once:
var markerGroups = new Array();
- Instead of manually adding a marker using map.addOverlay, when our marker function runs each time we’ll push each marker into the array we just created:
function createMarker(point,html) {
var marker = new GMarker(point);
markerGroups.push(marker);
GEvent.addListener(marker, ‘click’, function() {
marker.openInfoWindowHtml(html);
});
return marker;
} - Once our loop has finished running, then we’ll tell cluster marker to go to work on the array of points we’ve stored up:
cluster.addMarkers(markerGroups);
cluster.refresh();
- In your HTML document’s <HEAD>, import the Javascript file that contains the clustering functions:
Michael Corey is the digital innovations editor at the