Category: JavaScript & jQuery Tags: jQuery, Google Maps
Note on April 30th: This article was updated do to feedback from Google Maps API team to use the Client Geocoder instead of encoding on the server.
Continuing the series on jQuery and Google Maps, I will teach you how to store and retrieve points with using AJAX and a server-side language. This tutorial will use PHP/MySQL on the server, but it is basic enough that re-writing in another language should not be difficult.
First, let me share with you the design-pattern behind the tutorial. My design pattern has two steps. The first is to use a simple HTML form to create new locations by posting the data to the server via AJAX. The second step is to fetch those locations from the server also via AJAX. Sound simple? Well, then lets get started...
Note: This tutorial builds on the first tutorial's code, so all code I am showing you here will be added onto it.
To allow users to add locations to the map, let's create a basic form. This will include the parts of an address, as well as the name of the location.
<form id="add-point" action="map-service.php" method="POST"> <input type="hidden" name="action" value="savepoint" id="action"> <fieldset> <legend>Add a Point to the Map</legend> <div class="error" style="display:none;"></div> <div class="input"> <label for="name">Location Name</label> <input type="text" name="name" id="name" value=""> </div> <div class="input"> <label for="address">Address</label> <input type="text" name="address" id="address" value=""> </div> <button type="submit">Add Point</button> </fieldset> </form>
A couple things to note about the form:
<input type="hidden" name="action" value="savepoint" id="action">
will be used on the server to flag that we want to save a point to the database. This is just a personal preference on how to do things, there are many other ways to flag the intended action.<div class="error" style="display:none;"></div>
is placed in the form to be used in a later step to display errors.By adding a few CSS rules to our page, we will set our form next to the map and spruce up the form a bit.
#add-point { float:left; } div.input { padding:3px 0; } label { display:block; font-size:80%; } input, select { width:150px; } button { float:right; } div.error { color:red; font-weight:bold; }
At this point we'll override the form's default submit action by selecting the form $("#add-point")
, then using jQuery's submit event method. This method accepts a function that will run on submit of the form.
$("#add-point").submit(function(){ geoEncode(); return false; });
Then, inside the submit we will post the form data with AJAX using jQuery's ajax post method.
var geo = new GClientGeocoder(); var reasons=[]; reasons[G_GEO_SUCCESS] = "Success"; reasons[G_GEO_MISSING_ADDRESS] = "Missing Address"; reasons[G_GEO_UNKNOWN_ADDRESS] = "Unknown Address."; reasons[G_GEO_UNAVAILABLE_ADDRESS]= "Unavailable Address"; reasons[G_GEO_BAD_KEY] = "Bad API Key"; reasons[G_GEO_TOO_MANY_QUERIES] = "Too Many Queries"; reasons[G_GEO_SERVER_ERROR] = "Server error";
function geoEncode() { var address = $("#add-point input[name=address]").val(); geo.getLocations(address, function (result){ if (result.Status.code == G_GEO_SUCCESS) { geocode = result.Placemark[0].Point.coordinates; savePoint(geocode); } else { var reason="Code "+result.Status.code; if (reasons[result.Status.code]) { reason = reasons[result.Status.code] } $("#add-point .error").html(reason).fadeIn(); geocode = false; } }); }
function savePoint(geocode) { var data = $("#add-point :input").serializeArray(); data[data.length] = { name: "lng", value: geocode[0] }; data[data.length] = { name: "lat", value: geocode[1] }; $.post($("#add-point").attr('action'), data, function(json){ $("#add-point .error").fadeOut(); if (json.status == "fail") { $("#add-point .error").html(json.message).fadeIn(); } if (json.status == "success") { $("#add-point :input[name!=action]").val(""); var location = json.data; addLocation(location); zoomToBounds(); } }, "json"); }
The $.post
method accepts parameters.
$(this).attr('action')
will get the action attribute from the form that was submitted in the previous step.{ name: "inputname", value: "inputvalue" }
we will get all the inputs using the :input selector in jQuery, then use the serialize array function to turn those inputs into name, value pairs. Then add the two geocode name/value pairs to the data object.Once the data is posted with jQuery, we can handle it on the server with PHP.
Let's simply check if the action variable is posted as, "savepoint". Then validate that the name has the proper characters with a regular expression and also that it is not empty. If any data is invalid, let's call a fail method (defined in next sub-step) with the message we want to show to the user.
<?php if ($_POST['action'] == 'savepoint') { $name = $_POST['name']; if(preg_match('/[^\w\s]/i', $name)) { fail('Invalid name provided.'); } if(empty($name)) { fail('Please enter a name.'); } } ?>
Save the file as map-service.php or whatever you named your form's action attribute.
Our fail function will use PHP's die method to stop the script from executing and output an error message to the client. Since the front-end (jQuery) is expecting a JSON object, we want to make sure to always send back a JSON response. To output JSON with PHP, you simply pass an array into the json encode method (json_encode is PHP 5.2+ only, if you are using less than 5.2 then use the JSON PHP library).
function fail($message) { die(json_encode(array('status' => 'fail', 'message' => $message))); }
For the JSON array we want to use the JSEND specification for sending back a response. Basically, you have a key/value pair of status equals success or fail. That way the response can easily be checked on the front-end. I'm deviating from the JSEND spec a little bit by only sending a string back instead of a key/value pair of messages.
Using Firebug and Firefox, we can inspect the Ajax requests easily within the browser.
You can see here I submitted the form without entering a name and it sent me back an error message in the form of JSON.
Hopping back to the jQuery code, we will write the error handling.
Inside the post code, we will first use the hide method to hide the error div in case it is already displaying. Then check if json.status
is showing, "fail". If it is, we'll place the json.message
inside the error div with jQuery's html attribute method and then fade it in with the fade in method.
$("#add-point .error").hide(); if (json.status == "fail") { $("#add-point .error").html(json.message).fadeIn(); }
Using SQL, create a database table named locations which has a "name", "latitude", "longitude" and an "id" in it. If you need help with this, you will have to consult w3schools php and mysql for more help.
CREATE TABLE `locations` ( `id` int(11) unsigned NOT NULL auto_increment, `name` varchar(100) default NULL, `lat` float(15,11) default NULL, `lng` float(15,11) default NULL, PRIMARY KEY (`id`) )
We will use PHP and MySQL to insert the new location into the database. Directly after, we will either flag a success or fail message to the user.
$query = "INSERT INTO locations SET name='$_POST[name]', lat='$lat', lng='$lng'"; $result = map_query($query); if ($result) { success(array('lat' => $_POST['lat'], 'lng' => $_POST['lng'], 'name' => $name)); } else { fail('Failed to add point.'); }
If you noticed, I created a custom function called map_query
to abstract out the database stuff. Here is the function definition. Make sure to update the, "MYSQL" stuff with your credentials.
function map_query($query) { mysql_connect('MYSQL_HOST', 'MYSQL_USER', 'MYSQL_PASSWORD') OR die(fail('Could not connect to database.')); mysql_select_db ('MYSQL_DATABASE'); return mysql_query($query); }
I also created a similar method to "fail" called "success" which looks like:
function success($data) { die(json_encode(array('status' => 'success', 'data' => $data))); }
An example of a succesful response in firebug:
Going back to the jQuery code, we can now add the success response handling. The response is a JSON object with "lat", "lng" and "name" properties. I'll give you the code inside the success handling, then later show you what each custom function is doing.
if (json.status == "success") { $("#add-point :input[name!=action]").val(""); var location = json.data; addLocation(location); zoomToBounds(); }
After a location is successfully added to the database, we want to clear the form to prevent duplicate entry. Do this by selecting the inputs with the :input
selector. Then we need to filter out the action input, do this by using the attribute not equal selector [name!=action]
.
My addLocation(location)
function is simply our code from the last tutorial placed into a function to be reusable later.
function addLocation(location) { var point = new GLatLng(location.lat, location.lng); var marker = new GMarker(point); map.addOverlay(marker); bounds.extend(marker.getPoint()); $("<li />") .html(location.name) .click(function(){ showMessage(marker, location.name); }) .appendTo("#list"); GEvent.addListener(marker, "click", function(){ showMessage(this); }); }
It has a few things you might want to note:
bounds.extend(marker.getPoint());
and zoomToBounds
for now or skip to #13 quickly to find out what they do.When the page initially loads, we want to load all of our stored points. The simplest way to do this (in my opinion) is to do a GET request to fetch a JSON object from the server after the page loads.
To make a, "GET" request to the server, we can use jQuery's getJson method. We will send the server a, "get" variable called action with value, "listpoints".
$.getJSON("php/map-service.php?action=listpoints", function(json) { // do stuff in step #11 });
Simply check the, "GET" action in the PHP and run this code to fetch the locations records. Pretty straight-forward code here. We are creating an array of points and then sending them back to the client as JSON.
if ($_GET['action'] == 'listpoints') { $query = "SELECT * FROM locations"; $result = map_query($query); $points = array(); while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) { array_push($points, array('name' => $row['name'], 'lat' => $row['lat'], 'lng' => $row['lng'])); } echo json_encode(array("Locations" => $points)); exit; }
Iterate through the JSON object that contains the locations inside the getJson response function.
if (json.Locations.length > 0) { for (i=0; i<json.Locations.length; i++) { var location = json.Locations[i]; addLocation(location); } zoomToBounds(); }
Here we are simply looping through the Locations inside the json object and calling out addLocation
method.
Let's define that mysterious zoomToBounds
method that I keep calling and not telling you what it does.
First, define a variable called, "bounds" above the getJson method.
var bounds = new GLatLngBounds();
Note that in step #8's addLocation
method we are extending the bounds with bounds.extend(marker.getPoint());
This takes the bounds and adds each point to it. Then we can set the map to show all the points later by using the bounds that contains each point on the map.
function zoomToBounds() { map.setCenter(bounds.getCenter()); map.setZoom(map.getBoundsZoomLevel(bounds)-1); }
This function sets the center of the map, and the zoom minus 1 to set the map's viewport based on that bounds variable we declared and extended earlier.
Congrats! You just create a Google Maps mashup that you can add and remove points to dynamically with Ajax, PHP and MySQL.