Duncan's blog

February 28, 2013

Google Maps API – polylines and events

Update: a lot of the javascript in this article was based on old code I wrote a while ago… thinking about it after I initially published the article I decided to rewrite it slightly. So previously I had one array for destinations, and two global arrays for labels and polylines. It’s much simpler in this case just to have one global array which contains destinations, labels and polylines.

So following on from the previous post, Google Maps API – polylines and distances, let’s try enhancing that code a bit.

Firstly, as well as having a table showing distances, I want to show a label on each polyline with the distance in miles.  For that we can create a custom overlay by prototyping the OverlayView class.

Secondly, let’s have some events associated with the map – when I click on a polyline I want to see that label.  Also we can have an event listener on the table of distances which triggers the same event on the polylines.

Here’s what I see when I click on a polyline, or mouseover the table row that contains its data.

map label

So, here’s the Label class – this code is almost entirely from a blogpost by Marc Ridey with one tiny change on my part.  Custom Overlays need to prototype google.maps.OverlayView().  It then must define onAdd(), onRemove() and draw() functions.  The only thing I changed here from Marc’s original version was to use var pane = this.getPanes().floatPane; instead of var pane = this.getPanes().overlayLayer;  The OverlayLayer meant the labels appeared underneath polylines and markers.  The floatPane is the equivalent of using a higher z-index, so the labels now float above those items.

// Define the overlay, derived from google.maps.OverlayView
function Label(opt_options) {
	// Initialization
	this.setValues(opt_options);
	
	// Label specific
	var span = this.span_ = document.createElement('span');
	span.style.cssText = 'position: relative; left: -50%; top: -8px; ' +
	                     'white-space: nowrap; border: 1px solid blue; ' +
	                     'padding: 2px; background-color: white';
	
	var div = this.div_ = document.createElement('div');
	div.appendChild(span);
	div.style.cssText = 'position: absolute; display: none';
}
Label.prototype = new google.maps.OverlayView();

// Implement onAdd
Label.prototype.onAdd = function() {
	var pane = this.getPanes().floatPane;
	pane.appendChild(this.div_);
	
	// Ensures the label is redrawn if the text or position is changed.
	var me = this;
	this.listeners_ = [
		google.maps.event.addListener(this, 'position_changed',
			function() { me.draw(); }),
		google.maps.event.addListener(this, 'text_changed',
			function() { me.draw(); })
	];
};

// Implement onRemove
Label.prototype.onRemove = function() {
	var i, I;
	this.div_.parentNode.removeChild(this.div_);
	
	// Label is removed from the map, stop updating its position/text.
	for (i = 0, I = this.listeners_.length; i < I; ++i) {
		google.maps.event.removeListener(this.listeners_[i]);
	}
};

// Implement draw
Label.prototype.draw = function() {
	var projection = this.getProjection();
	var position = projection.fromLatLngToDivPixel(this.get('position'));
	
	var div = this.div_;
	div.style.left = position.x + 'px';
	div.style.top = position.y + 'px';
	div.style.display = 'block';
	
	this.span_.innerHTML = this.get('text').toString();
};

And so then here’s the rest of the javascript:

var arrDestinations;

function initialize() {
	var map, i, j, latLng, stuDistances, inBetween, labelMarker;

	arrDestinations = [
		{title: 'Keswick',		lat: 54.60039,		lng: -3.13632},
		{title: 'Coniston',		lat: 54.36897,		lng: -3.07561},
		{title: 'Lake District',	lat: 54.5003526,	lng: -3.0844116},
		{title: 'Cumbria',		lat: 54.57723,		lng: -2.79748}
	];
	
	var stuHome = {title: 'Ambleside', lat: 54.42838, lng: -2.9623};
	
	var homeLatlng = new google.maps.LatLng(stuHome.lat, stuHome.lng);
	var myOptions = {
		zoom: 10,
		center: homeLatlng,
		mapTypeId: google.maps.MapTypeId.ROADMAP
	};
	map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
			
	var homeMarker = new google.maps.Marker({
		position: homeLatlng, 
		map: map, 
		title: stuHome.title
	});
	
	$('#tableNeighbours').append(
		  '<tr>'
		+ '<th>Destination</th>'
		+ '<th colspan="2">' + stuHome.title + '</th>'
		+ '</tr>'
	);
			  
	for (i = 0; i < arrDestinations.length; i++) {
		latLng = new google.maps.LatLng(arrDestinations[i].lat, arrDestinations[i].lng);
		
		arrDestinations[i].marker = new google.maps.Marker({
			position: latLng,
			map: map, 
			title: arrDestinations[i].title,
			icon: 'http://maps.google.co.uk/intl/en_ALL/mapfiles/ms/micons/green-dot.png'
		});
					
		// draw lines between each marker and home.  these are curved lines, not as the crow flies, i.e. they take into account the curvature of the earth (only noticable on longer distances)
		arrDestinations[i].polyline = new google.maps.Polyline({
			path: [homeLatlng, latLng],
			strokeColor: "#FF0000",
			strokeOpacity: 0.5,
			strokeWeight: 4,
			geodesic: true,
			map: map,
			polylineID: i
		});
					
		// calculate the distance between home and this marker
		stuDistances = calculateDistances(homeLatlng, latLng);
		$('#tableNeighbours').append(
			  '<tr id="row' + i + '">' 
			+ '<td>' + arrDestinations[i].title + '</td>'
			+ '<td>' + stuDistances.km + ' km</td>'
			+ '<td>' + stuDistances.miles + ' miles</td>'
			+ '</tr>'
		);
		
		// get the point half-way between this marker and the home marker
		inBetween = google.maps.geometry.spherical.interpolate(homeLatlng, latLng, 0.5);  
		
		// create an invisible marker
		labelMarker = new google.maps.Marker({  
			position: inBetween,  
			map: map,
			visible: false
		});
		
		arrDestinations[i].label = new Label();
		
		arrDestinations[i].label.bindTo('position', labelMarker, 'position');
		arrDestinations[i].label.set('text', stuDistances.miles + ' miles');
		
		// we'll use this ID later:
		arrDestinations[i].label.polylineID = i;
	
		// lets add an event listener, if you click the line, i'll tell you the distance
		google.maps.event.addListener(arrDestinations[i].polyline, 'click', function() {
			// remove other labels
			for (j = 0; j < arrDestinations.length; j++){
				if (this.polylineID != j) {
					if(typeof(arrDestinations[j].label) != "undefined"){
						arrDestinations[j].label.setMap(null);
					}
				} else {
					arrDestinations[j].label.setMap(map);
				}
			}
		});
	}
}

function calculateDistances(start,end) {
	var stuDistances = {};
	
	stuDistances.metres = google.maps.geometry.spherical.computeDistanceBetween(start, end);	// distance in metres rounded to 1dp
	stuDistances.km = Math.round(stuDistances.metres / 1000 *10)/10;							// distance in km rounded to 1dp
	stuDistances.miles = Math.round(stuDistances.metres / 1000 * 0.6214 *10)/10;				// distance in miles rounded to 1dp

	return stuDistances;
}

function removeAllLabels() {
	var i;
	// remove other markers
	for (i = 0; i < arrDestinations.length; i++){
		// don't remove the current label
		if(typeof(arrDestinations[i].label) != "undefined"){
			arrDestinations[i].label.setMap(null);
		}
	}
}

$(document).ready(function() {
	$('table').delegate("td", "mouseover mouseout", function(e) {
		if (e.type == 'mouseover') {
			var id = $(this).parent().attr('id').replace('row', '');
			google.maps.event.trigger(arrDestinations[id].polyline, 'click');
		} else {
			removeAllLabels();
		}
	});
});

google.maps.event.addDomListener(window, 'load', initialize);

Some other changes worth mentioning from the previous article. We’re using google.maps.geometry.spherical.interpolate to calculate a point 50% of the way along the polyline. This could also be used if you wanted to place markers at regular intervals along the line for instance.

We then create an invisible marker at that position, and bind our Label to it.  And we add all the labels into an array.

Then we have an event listener on the polyline for any ‘click’ events.  When someone clicks on a line, I want to hide all the other labels and only show this one.  Alternatively you might want to not have any event listener, and just show all the labels all the time.

Then there’s another event listener for when we mouseover / mouseout any table cells.  Because we’re adding the table rows dynamically via the JS, I’m using the jQuery .delegate() function.  Whenever you mouseout from a  TD, the marker is removed.  If you mouseover, we can simply trigger a click on that polyline, which will then execute the google maps event listener.

2 Comments »

  1. […] my previous post in this series added event listeners to polylines, this time I want to make the lines draggable.  In this case I want to have one marker I can move […]

    Pingback by Google Maps API – draggable polylines | Duncan's blog — March 1, 2013 @ 6:26 pm | Reply

  2. […] in response to user clicks on the line, displaying the coordinates.  I had previously posted an article here where I displayed labels on multiple polylines showing their distance.  I simplified that code to […]

    Pingback by Google Maps API – polyline labels | Duncan's blog — December 18, 2013 @ 6:40 pm | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: