TMT 4 Part 2 Map Plugin V2 with jQuery Google Maps Plugin Version 2

TMT4 P2 Dynamic Markers, Geocoding and jQuery Google Maps Plugin with jQuery Google Maps Plugin Version 2 (Version 3 is now available – read below)

November 18, 2011 This is the first version of this tutorial (earlier than PhoneGap 1.1.0 and jQuery Mobile RC2). The new version of the tutorial is here and uses the latest versions of AppLaud and jQuery Google Maps Plugin Version 3. To get the benefit of the latest support from PhoneGap and jQuery it is recommended to use the new tutorial.

This tutorial uses jQuery Google Maps Plugin Version 2.

Part 2 of the Google Map tutorials demonstrates two ways to dynamically add and manage markers on a Google Map: 1) device geolocation; 2) tap and/or drag events on the map. As in TMT4 Part 1, the app uses the jQuery Google Maps Plugin v2 and jQuery Mobile to easily manage Google Maps JSAPI v3. TMT4 Part 2 is an advanced tutorial, packed with more features and less step-by-step explanation. Presented in this tutorial:

  • Dynamically Add Markers to Map with Tap and Drag Events
  • Create and Add Custom Controls to Google Map
  • Geocoding Two Ways: Lat/Lng <=> Street Address
  • Extension of jQuery Google Maps Plugin to Simplify Geolocation using browser geolocation
  • Custom Icons including Custom Shadow and Shape Area
  • Dynamic Modal Dialog in jQuery Mobile for Save/Edit/Remove actions on Marker and Marker Data
  • jQuery Mobile: Multi-Pages, Dynamic Listview, Fading Message Box, Collapsible Content

The app is inspired by Marcus Österberg’s geocoding example and map service website: MapUsers, which provides a collaborative space for sharing global locations using google maps. Like Marcus’ example, this tutorial makes use of google’s geocoding service to translate a street address into latitude/longitude coordinates, and the reverse.

Custom controls are created using jQuery Mobile buttons and inserted into the google map for a consistent UX. Custom marker icons from the Portland State University’s Mash-o-matic utilities are used in place of the default google marker icons. The custom icon images are further enhanced with the addition of custom shadows and clickable region shapes created by the Google Map Custom Marker Maker for v3 API from Powerhut Net Solutions.

Sample App Overview

The first page of this two-page app is a full-screen google map that uses tap events to add markers at the indicated locations. The user may also mark their current position using device geolocation. A dialog lets the user edit and confirm the marker address, and add customized data associated with the marker. The second page of the app is a list of the markers showing the marker data as text. The list also provides links back to the map for viewing, editing or removal of markers.

Note: The markers and data in this app are stored in the html page and are not persistent across app launches. The next tutorial in this series, TMT4 Part 3 : Save Markers Using JSON and HTML5 Web Storage, adds support for persistent marker data.

Prerequisites

  • Installation of the latest MDS AppLaud plugin, see Get Started
  • Complete TMT0 or equivalent (create project, edit files, run app on device or AVD)
  • AVD or device: include or enable GPS/location finding in device settings

Prep

  • Download the attached zip file tmt4part2.zip
    • /src directory contains:
      • index.html – defines app’s two pages using jQuery Mobile multi-page markup
      • tmt4p2.js – all javascript for app
      • tmt4p2.css – important styling for map display and app
      • /images/markers – images for custom marker icons and map controls
    • /jquery.ui.map contains:
      • jquery.ui.map.min.js – minified, version 2 plugin (use for market version)
      • jquery.ui.map.js – non-minified, version 2 plugin (use for development)
      • jquery.ui.map.extensions.js – custom extension of maps plugin
  • Alternative method for plugin: include jquery.ui.map.min.js plugin from a server (see step 2 below)

Note: August 23, 2011: This tutorial uses the jQuery Google Maps Plugin Version 2. The next version of the plugin, Version 3 Alpha, is currently available. The next version of this tutorial using Version 3 will be available later this month.

1. Create a new AppLaud project

Using the project creation wizard, create a new project that includes:

  • Built-In PhoneGap
  • jQuery Mobile Libraries
  • Project Contents: locate the /src directory to populate project (see Prep step above)

Finish creating the project in the second window: fill in project name, etc. Upon completion the new project’s /assets/www/ directory should appear exactly as below.

2. Add Google Maps JS API v3, the jQuery Google Maps plugin and extension to the project

The Google Maps JS v3 APIs are available when you include a single file in index.html. No key or local file is needed. A summary of all files in the header is at the end of this section.

Edit index.html to include google maps javascript before tmt4p2.js:

<script type=”text/javascript” src=”http://maps.google.com/maps/api/js?sensor=false”></script>

Adding a jQuery Plugin is usually as simple as including a javascript file in your html file. The reference to the maps plugin can point to a local copy or the file on a server.

To use the plugin locally, import the maps plugin files:

  • Right click on the /assets/www/jquery.mobile directory in the project and select Import…
  • Navigate to the directory /jquery.ui.map (see Prep step above)
  • Select the files jquery.ui.map.min.js, jquery.ui.map.js and jquery.ui.map.extension.js and finish importing

Edit index.html to include the plugin and extension files, before the tmt4p2.js include line, and after the google maps javascript:

<script type=”text/javascript” src=”jquery.mobile/jquery.ui.map.js”></script>
<script type=”text/javascript” src=”jquery.mobile/jquery.ui.map.extensions.js”></script>

Alternate method: reference the maps plugin file on a server by adding the following to index.html:

<script type=”text/javascript” src=”http://jquery-ui-map.googlecode.com/svn/tags/2.0/ui/jquery.ui.map.min.js”></script>

Important: During project creation the wizard inserts css and js files according to what was specified. Sometimes the order of these files must be rearranged to include the javascript files in the right order. The index.html file should have an order similar to the following after project creation and the steps described above.

<link rel=”stylesheet” href=”jquery.mobile/jquery.mobile-1.0b2.css” type=”text/css”>
<link rel=”stylesheet” href=”tmt4p2.css” type=”text/css”>

<script type=”text/javascript” src=”http://maps.google.com/maps/api/js?sensor=false”></script>
<script type=”text/javascript” src=”jquery.mobile/jquery-1.6.2.min”></script>
<script type=”text/javascript” src=”jquery.mobile/jquery.mobile-1.0b2.js”></script>
<script type=”text/javascript” src=”jquery.mobile/jquery.ui.map.js”></script>
<script type=”text/javascript” src=”jquery.mobile/jquery.ui.map.extensions.js”></script>
<script type=”text/javascript” src=”tmt4p2.js”></script>

It is not necessary to include the PhoneGap JavaScript APIs in this app because geolocation is supported in Android. The commented script including phonegap.0.9.5.js is not needed unless your app uses other PhoneGap APIs.

Sanity test: The app can be deployed and run with full functionality at this point. The images below show the app with no markers and the default location. Tap the Markers button at the bottom of the first page to view the second page. Collapsible Instructions list has been opened in the image below.

3. Customize the Default Location

Open the file tmt4p2.js in the editor and locate the var ‘defaultLoc’ near line 25. This variable defines the default location for the map (initial map is centered on Dallas, Texas). If desired, change the coordinates to center the map on a different location. A useful tool to find lat/lng coordinates is GetLatLon by Simon Willison.

If you make use of geolocation to position the marker (in custom control, discussed below) the new center of the map will jump to your current location. If your current location is outside the US, additional region detection and parsing of addresses may be needed to present street addresses correctly. For more on region biasing see google documentation on region code biasing.

3. Description of App Functionality

The functionality of this app is similar to the Click and Drag Events with Geosearch example from the jQuery Google Maps Plugin examples. The instructions (in collapsible div in index.html) are as follows:

  • Tap anywhere on map to add blue marker
  • Tap Green Marker in map controls to add marker at current location
  • Tap Markers in map controls to see list of all markers and data
  • Tap marker on the map to view data, edit or remove it
  • Drag marker anywhere to change its location
  • Tap marker in listview to see it centered on map
  • Tap gear icon in listview to edit or remove marker

A short overview of the javascript code in tmt4p2.js follows. All of the javascript code is defined in event handler functions for three jQuery Mobile page events: pagehide, pageshow and pagecreate.

jQuery Mobile’s data-role=”collapsible” markup in index.html defines a fully functional collapsible div element for the instructions. When the user navigates away from page-marker, generating a pagehide event, trigger a collapse of the div element (does not require testing current state) so the element will be collapsed when the user returns to page-marker.

$(‘#page-marker’).live(“pagehide”, function() {
$(‘#instructions’).trigger(‘collapse’);
});

When the use navigates back to page-map, refresh the map.

$(‘#page-map’).live(“pageshow”, function() {
$(‘#map_canvas’).trigger(‘refresh’);
});

The rest of the javascript is in the page-map’s pagecreate event handler. As of jQuery Mobile Beta 2 the .listview() initializer can no longer be called here. Comment the line back in for earlier versions. A utility function, fadingMsg(), is created to briefly display messages on the screen.

$(‘#page-map’).live(“pagecreate”, function() {
// Uncomment for jQuery Mobile Alpha and earlier versions
//$(‘#page-marker’).find(“ul#marker-list”).listview();

function fadingMsg (locMsg) {
$(“<div class=’ui-loader ui-overlay-shadow ui-body-e ui-corner-all’><h1>” + locMsg + “</h1></div>”)
.css({ “display”: “block”, “opacity”: 0.9, “z-index” : 9999, “top”: $(window).scrollTop() + 250 })
.appendTo( $.mobile.pageContainer )
.delay( 2400 )
.fadeOut( 1200, function(){
$(this).remove();
});
}

The next block creates the map using the jQuery Google Maps Plugin .gmap() function. The map is centered at the coordinates defined in defaultLoc. Taking advantage of the maps plugin’s callback option, the custom maps control element, ‘controls’, defined in index.html is added to the map. Two handlers for ‘click’ events are added next, one for a click (or tap) on the map, and one to handle a click on the custom control button, current_pos_marker, used to place a marker at the current location.

// Define a default location and create the map
var defaultLoc = new google.maps.LatLng(32.802955, -96.769923);
$(‘#map_canvas’).gmap( {‘center’: defaultLoc, ‘zoom’ : 14, ‘zoomControlOptions’: {‘position’:google.maps.ControlPosition.LEFT_TOP},
‘callback’: function(map) {

$(‘#map_canvas’).gmap(‘addControl’, ‘controls’, google.maps.ControlPosition.BOTTOM_CENTER);
document.getElementById(‘controls’).style.display = ‘inline’;

// attach map click handler and marker event handlers
$(map).click( function(event) {
$(‘#map_canvas’).gmap(‘option’, ‘center’, event.latLng);
addNewMarker( event.latLng, ‘blue’ );
});

$(‘#current_pos_marker’).click( function() {
$(‘#mask’).css({‘width’:screen.width,’height’:screen.height});
$(‘#mask’).fadeTo(“slow”,0.6);

fadingMsg (“Using device geolocation service to find location.”);

// See extension defined in jquery.mobile/jquery.ui.map.extensions.js
$(‘#map_canvas’).gmap(‘getCurrentPosition’, function(status, pos) {
if (status === “OK”) {
var latLng = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude);
$(‘#map_canvas’).gmap(‘option’, ‘center’, latLng);
addNewMarker( latLng, ‘gr’ );

$(‘#current_pos_marker’).removeClass(“ui-btn-active”);
} else {
fadingMsg (“<span style=’color:#f33;’>Error</span> while getting current location. Not supported in browser or GPS/location disabled.”);
$(‘#mask’).hide();

$(‘#current_pos_marker’).removeClass(“ui-btn-active”);
}
}, { timeout: 4000, enableHighAccuracy: true } );
});
}});

In the previous section, notice that a custom extension of the .gmap() function was used to define a simpler api for handling the geolocation status and callback. The new method is ‘getCurrentPosition’. View jquery.ui.maps.extensions.js for an example of extending the maps plugin functionality.

Different colored marker icons were chosen to indicate if the marker was added by tap (blue) or geolocation (green). Blue and green tear-shaped markers from Mash-o-matic tools were selected, then processed on Powerhut Net Solutions’ Google Map Custom Marker Maker for JS API v3 maps to get a custom shadow and clickable region shape. This online utility takes a jpg, png or gif file and generates everything you need to include a more sophisticated marker in google maps, including sample html and javascript for the provided marker.

// Create blue, green and shadow images for custom markers
var bimage = new google.maps.MarkerImage(
‘images/markers/bimage.png’,
new google.maps.Size(20,34),
new google.maps.Point(0,0),
new google.maps.Point(10,34)
);

var gimage = new google.maps.MarkerImage(
‘images/markers/gimage.png’,
new google.maps.Size(20,34),
new google.maps.Point(0,0),
new google.maps.Point(10,34)
);

var shadow = new google.maps.MarkerImage(
‘images/markers/shadow.png’,
new google.maps.Size(40,34),
new google.maps.Point(0,0),
new google.maps.Point(10,34)
);

The addNewMarker() function takes a google maps lat/lng object and a string indicating marker color to create a new marker. It uses the maps plugin .gmap() function and the ‘addMarker’ method to supply google marker options, and makes use of the optional callback parameter to append the html form markup used to save marker data (see html in massive .append() call). Event handler functions are defined for the marker’s dragend (geocode new position and bring up dialog) and click (bring up dialog) events.

The marker options ‘icon’, ‘shadow’ and ‘shape’ use the images and values generated by the Google Map Custom Marker Maker mentioned previously.

A real application would send data to a server on form submit action. To keep this tutorial simple, it appends the data and html markup to a hidden div, markerdiv, for temporary storage, and displays or removes the div as needed.

function addNewMarker ( latLng, mrkr ) {
var mrkrIcon = (mrkr === ‘blue’) ? bimage : gimage;
$(‘#map_canvas’).gmap(‘addMarker’, {‘position’: latLng, ‘icon’: mrkrIcon, ‘shadow’ : shadow,
‘shape’ : {‘type’: ‘poly’, ‘coords’ : [13,0,15,1,16,2,17,3,18,4,18,5,19,6,19,7,19,8,19,9,19,10,19,11,19,12,19,13,18,14,18,15,17,16,16,17,15,                                                  18,14,19,14,20,13,21,13,22,12,23,12,24,12,25,12,26,11,27,11,28,11,29,11,30,11,31,11,32,11,33,8,33,8,32,
8,31,8,30,8,29,8,28,8,27,8,26,7,25,7,24,7,23,6,22,6,21,5,20,5,19,4,18,3,17,2,16,1,15,1,14,0,13,0,12,0,
11,0,10,0,9,0,8,0,7,0,6,1,5,1,4,2,3,3,2,4,1,6,0,13,0]},
‘draggable’: true, ‘bound’: false },
function(map, marker) {
var markerId = marker.__gm_id;
$(‘#markerdiv’).append(‘<div class=”mclass’ + markerId + ‘” style=”display:none;”> <form onsubmit=”return false;” method=”get” action=”/”>’
+ ‘<div data-role=”fieldcontain”><label for=”tag’ + markerId + ‘”>Marker Title<br/></label><input type=”text” size=”24″ maxlength=”30″ name=”tag’ + markerId + ‘” id=”tag’ + markerId + ‘” value=”” /></div>’
+ ‘<div data-role=”fieldcontain”><label for=”address’ + markerId + ‘”>Address<br/></label><input type=”text” size=”24″ maxlength=”30″ name=”address’ + markerId + ‘” id=”address’ + markerId + ‘” value=”” /></div>’
+ ‘<div data-role=”fieldcontain”><label for=”state’ + markerId + ‘”>City, State<br/></label><input type=”text” size=”24″ maxlength=”30″ name=”state’ + markerId + ‘” id=”state’ + markerId + ‘” value=”” /></div>’
+ ‘<div data-role=”fieldcontain”><label for=”country’ + markerId + ‘”>Country<br/></label><input type=”text” size=”24″ maxlength=”30″ name=”country’ + markerId + ‘” id=”country’ + markerId + ‘” value=”” /></div>’
+ ‘<div data-role=”fieldcontain”><label for=”comment’ + markerId + ‘”>Comment<br/></label><textarea maxlength=”64″ cols=24 rows=3 name=”comment’ + markerId + ‘” id=”comment’ + markerId + ‘” value=”” /></textarea></div>’
+ ‘</form></div>’);
getGeoData(marker);
}).dragend( function() {
// Marker == this – already has new position in it
getGeoData(this);
}).click( function() {
// Existing marker was clicked, location did not change
openMarkerDialog(this);
});
}

The next function, getGeoData(), takes a marker object parameter for geocoding, or more accurately reverse geocoding, to convert lat/lng coordinates to a street address. Again, the maps’ plugin .gmap() function is used, this time with the ‘search’ method and the ‘location’ parameter, which simplifies the call to google map’s geocoder.

This example uses US locations, so the results’ formatted_address array of strings (first four) suffice to get the complete street address. The commented code referencing address_components was not needed, but may be useful in other locations or for other geocoding purposes.

function getGeoData (marker) {
// Make Reverse Geocoding request (latlng to address)
// Note: ‘region’ option not used here, include for region code biasing
$(‘#map_canvas’).gmap(‘search’, { ‘location’: marker.getPosition() }, function(found, results) {
if ( found ) {
// Regions other than US may need to use address_components to get address etc
//$.each(results[0].address_components, function(i,v) {
//    if ( v.types[0] === “administrative_area_level_1” || v.types[0] === “administrative_area_level_2” ) {
//        city and/or state
//        $(‘#state’ + marker.__gm_id).val(v.long_name);
//    } else if ( v.types[0] === “country”) {
//        $(‘#country’ + marker.__gm_id).val(v.long_name);
//    }
//});
var addr = results[0].formatted_address.split(‘, ‘, 4);
//alert(‘[‘ + addr[0] + ‘] [‘ + addr[1] + ‘] [‘ + addr[2] + ‘] [‘ + addr[3] + ‘]’);
$(‘#address’ + marker.__gm_id).val(addr[0]);
$(‘#state’ + marker.__gm_id).val(addr[1] + “, ” + addr[2]);
$(‘#country’ + marker.__gm_id).val(addr[3]);
openMarkerDialog(marker);
} else {
fadingMsg(‘Unable to get GeoSearch data.’);
openMarkerDialog(marker)
}
});
}

The last function, openMarkerDialog(), is necessarily the biggest, as opening a dialog involves presenting the marker data (new or saved), dynamically changing the marker list, detecting user changes to the marker location, geocoding if there is a location change, and implementing the dialog’s Save and Remove buttons.

If a marker is saved, it is added to the ‘marker-list’ listview element on page-marker (2nd page). Handlers are dynamically bound to each marker on the list to enable either returning to the map centered on that marker, or returning to the map whilst bringing up a dialog to edit that marker.

The ‘mask’ div is dynamically shown to make the dialog have modal behavior by making everything else on the screen grey and unreachable. If the user taps outside the dialog’s Remove or Save buttons (i.e. on the mask), the default behavior saves the marker and closes the dialog.

function openMarkerDialog(marker) {
var markerId = marker.__gm_id;
var lastAddress = $(‘#address’ + markerId).val(), lastCityState = $(‘#state’ + markerId).val(),
lastCountry = $(‘#country’ + markerId).val();

$(‘#mask’).css({‘width’:screen.width,’height’:screen.height});
$(‘#mask’).fadeTo(“slow”,0.6);

// Remove this marker and placeholder from ul#marker-list
$(‘li#item’ + markerId).remove();
$(‘#li-placeholder’).css(‘display’, ‘none’);

$(“<div class=’ui-loader ui-overlay-shadow ui-body-e ui-corner-all’ id=’dialog” + markerId + “‘ style=’z-index:9999;’></div>”)
.append(‘<h4 style=”margin:0.2em;”>Edit &amp; Save Marker</h4>’)
.append( $(‘div.mclass’+markerId).css({ “display”: “block”})
.append(‘<div data-inline=”true” id=”dialog-btns” ><a id=”remove” class=”mbtn” style=”font-size:15px”>Remove</a>’
+ ‘<a id=”save” class=”mbtn”>&nbsp;&nbsp;&nbsp;Save&nbsp;&nbsp;&nbsp;</a></div>’))
.css({ “display”: “block”, “opacity”: 0.9, “top”: $(window).scrollTop() + 90 })
.appendTo( $.mobile.pageContainer );

// Put focus on first input field
$(‘#tag’ + markerId).focus();

$(‘#remove’).click( function () {
// If list is empty, show placeholder text again
if ($(‘ul#marker-list’).find(‘li’).length === 2) {
$(‘#li-placeholder’).css(‘display’, ‘block’);
}

// remove marker from map
marker.setMap(null);
// Remove entire dialog, including div mclass{id} (the marker data)
$(‘#dialog’ + markerId).remove();
$(‘#mask’).hide();
});

$(‘#save’).click( function () {
// Save tag value as marker title (desktop browser debugging only)
marker.setTitle($(‘#tag’ + markerId).val());

// Remove Save and Remove buttons
$(‘#dialog-btns’).remove();

// Store the div mclass{id} in markerdiv
$(‘.mclass’ + markerId)
.css({ “display”: “none”})
.appendTo(‘#markerdiv’);

// Test if user changed any part of marker address
if ( (lastAddress !== $(‘#address’ + markerId).val()) ||
(lastCityState !== $(‘#state’ + markerId).val()) ||
(lastCountry !== $(‘#country’ + markerId).val()) ) {

// Make Geocoding requestion (commas-separated address to lat/lng)
$(‘#map_canvas’).gmap(‘search’, { ‘address’: $(‘#address’ + markerId).val() + ‘, ‘
+ $(‘#state’ + markerId).val() + ‘, ‘ + $(‘#country’ + markerId).val() },
function(found, results) {
if ( found ) {
marker.setPosition(results[0].geometry.location);
$(‘#map_canvas’).gmap(‘option’, ‘center’, results[0].geometry.location);
} else {
fadingMsg(‘Unable to get GeoSearch data. Marker remains in same place.’);
}
});
}

// Put marker info in ul#marker-list on page-marker
$(‘<li id=”item’ + markerId + ‘”><a href=”#page-map”><h4>’
+ (($(‘#tag’ + markerId).val() !== “”) ? $(‘#tag’ + markerId).val() : (‘Marker ID ‘ + markerId))
+ ‘</h4><p>’ + $(‘#address’ + markerId).val() + ‘<br/>’
+ $(‘#state’ + markerId).val() + ‘  ‘
+ $(‘#country’ + markerId).val()
+ ‘<br/>’ + marker.getPosition() + ‘<br/>’
+ $(‘#comment’+ markerId).val()
+ ‘</p></a><a href=”#page-map” id=”edit’ + markerId + ‘”>Edit</a></li>’).appendTo(‘ul#marker-list’);

// Bind click handler: center map on the selected marker or open dialog to edit
$(‘li#item’ + markerId).click( function() {
$(‘#map_canvas’).gmap(‘option’, ‘center’, marker.getPosition());
marker.setAnimation(google.maps.Animation.DROP);
});
$(‘#edit’ + markerId).click( function() {
$(‘#map_canvas’).gmap(‘option’, ‘center’, marker.getPosition());
openMarkerDialog(marker);
});

try {
$(“ul#marker-list”).listview(‘refresh’);
} catch(e) { }

// Remove the remaining bits of dialog and mask
$(‘#dialog’ + markerId).remove();
$(‘#mask’).hide();
});

$(‘#mask’).click( function() {
// If user taps mask, save marker data as is by default
// Alternative: remove by default: $(‘#remove’).trigger(‘click’);
$(‘#save’).trigger(‘click’);
});
}
});

If you are still reading this, you must really be interested in learning about maps and geocoding! One way to work through the code is to start with a div in index.html (e.g. ‘controls’ or ‘marker-list’) and track how it’s used in tmt4p2.js. Review the jQuery Google Maps Plugin code examples. To get a feel for the maps plugin api, review all .gmap calls in tmt4p2.js and understand which part of the google maps functionality is being used.

Additional documentation related to features in this app:

  • jQuery Google Maps Plugin API
  • AppLaud Forum post on using Google’s Geocoding Services
  • Google Maps JavaScript API V3 Reference
  • jQuery Mobile Docs and Demos
  • Google Map Custom Marker Maker for v3 API
  • GMap Demo by AppLaud – MDS’ free Android App with many more map examples