Duncan's blog

October 16, 2015

Google Maps – editable polylines

Filed under: Google Maps,Javascript — duncan @ 12:01 am
Tags: , ,

In Google Maps Javascript API v3, it’s possible to make polylines editable.  But it’s not immediately obvious what you can do with that.  In this post I try and explore some of what you can then do.

Firstly, it’s very simple to create a polyline.  According to the documentation it requires a path as part of the PolylineOptions used in its constructor.  And that path can either be a normal javascript array or Google’s own MVCArray – this distinction will come in useful later on – or even just a single LatLngLiteral.

The LatLngLiteral is where instead of constructing a point like:

x = new google.maps.LatLng(51.5286416, -0.1015987)

you can simply use:

x = {lat: 51.5286416, lng: -0.1015987}

So here’s a simple example.  In this case I’m just going to start by trying to  use a javascript array of LatLngLiterals:

var path = [
	{lat: 51.5573205, lng: -0.1663994},
	{lat: 51.5636304, lng: -0.1613568}
];

var polyline = new google.maps.Polyline({
	path: path,
	map: map
	editable: true
});

By adding that editable: true option, we go from this to this:

Map_with_editable_polyline_-_2015-10-15_22.52.59 Map_with_editable_polyline_-_2015-10-15_22.58.59

We now have a polyline with the two end vertices we can now drag.  There’s also a placeholder vertex at the centre, and dragging that adds it as a new point to the path.

Next, what we can do is add event listeners.  Google’s documentation has a little bit of useful information about editable shapes and their event listeners.

The Polyline class has several events you can listen for, including drag, dragstart and dragend.  But it’s not got any that seem to be related to when the polyline is being edited (i.e. individual points being dragged, rather than the whole line).

Instead, we need to have event listeners for the path which we’ve just constructed the polyline with.  Again reading the documentation, the MVCArray class has event listeners we can use, insert_at, set_at and remove_at. The first two happen automatically as we drag the polyline’s points; the remove_at one we can trigger by calling the removeAt function on the path (more on that later):

google.maps.event.addListener(path, 'insert_at', function(vertex) {
	console.log('Vertex ' + vertex + ' inserted to path.');
});

google.maps.event.addListener(path, 'set_at', function(vertex) {
	console.log('Vertex ' + vertex + ' updated on path.');
});

However, nothing happens using all this code.  We get an editable polyline, but the event listeners don’t seem to trigger when I drag the individual points on it.  The documentation says “Note that if you pass a simple array, it will be converted to an MVCArray“.  So you’d assume this was the case here, and I could then expect the MVCArray events to be triggered, but it doesn’t seem to be the case.

So, despite being able to construct a polyline with a single LatLngLiteral, passing an array of them prevents any of the path listener events triggering.

Instead we need to amend how we define the path; it needs to be explicitly an MVCArray, not just a normal javascript array.  You’d think we could just do:

var path = new google.maps.MVCArray([
	{lat: 51.5573205, lng: -0.1663994},
	{lat: 51.5636304, lng: -0.1613568}
]);

However that throws a javascript error, and no line appears:

InvalidValueError: at index 0: not an instance of LatLng

So third time lucky; we have to define the path as an MVCArray of LatLng objects, not an MVCArray of LatLngLiterals, or a normal javascript array of either LatLngs or LatLngLiterals.

var path = new google.maps.MVCArray([
	new google.maps.LatLng(51.5573205, -0.1663994),
	new google.maps.LatLng(51.5636304, -0.1613568)
]);

Ok, finally, this works.  Now what… well in our event listener functions, the only argument passed in is an integer indicating which vertex was added / edited / removed.  Great.  We can also access the path itself, of course.  And from that we could find out the coordinates of all the vertices, and the path’s length, which might be useful.

So I’m going to use a little bit of jQuery to output that information.  I call this function from each of the event listeners, so we always have an up-to-date list of coordinates and length of the path:

function updateCoords(path) {
	var row;
	$('#length').text('Length: ' + google.maps.geometry.spherical.computeLength(path).toFixed(2));
	$('#vertices tr:gt(0)').remove();
	path.forEach(function(element, index) {
		row = $('<tr>');
		row.append('<td>' + (index+1) + '</td>');
		row.append('<td>' + element.lat() + '</td>');
		row.append('<td>' + element.lng() + '</td>');
		row.append('<td><a href="">X</a></td>');
		$('#vertices').append(row);
	});
}

To get the length, I’m using Google’s Geometry library.  To use this, you have to include it as a parameter when you load in the Maps API:

https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry

And what we end up with is something like this:

Map_with_editable_polyline_-_2015-10-15_23.31.23

As well as displaying the coordinates for each vertex, I’ve also added a link to remove each one, and this jQuery event listener for then removing them from the path. I didn’t want to completely remove the path, so make sure there’s always at least 2 points:

$('body').on('click', 'a', function(event) {
	event.preventDefault();
	if (path.getLength() > 2) {
		path.removeAt($(this).data('id'));
	}
	return false;
});

In a typical application you’d probably want to do something like fire off an AJAX request to store the coordinates for use later on, or something like that.  This was just a simple example to see how you needed to construct the polyline in order to use its event listeners.  You can see it in action, and the full code, here.

Update: Havelly asked how we could make a right-click on the line do the same as clicking the X in the table, to remove a section of the path.  As I mentioned in the comments, the Polyline has a ‘rightclick’ event listener, which gives you access to a PolyMouseEvent. The PolyMouseEvent has properties for edge, vertex and path, which you’d think would do the trick. However I couldn’t seem to ever get the path property; I suspect it’s only available on Polygons, not Polylines.

Instead, the PolyMouseEvent also gives you the latLng of where the event occurred, which we can use instead. I ended up drawing an invisible Polyline for each section of the Path.  In response to the rightclick event on the original Polyline, I loop over that array of hidden polylines, checking if the latLng where we just right-clicked is on that individual polyline, using the google.maps.geometry.poly::isLocationOnEdge function.  And if it is, we can them just call the remoteAt function.

This code’s slightly rough and untested, but seemed to work.  I had to tweak the tolerance a bit until I got it right; possibly you’d need to adjust it to something else depending on stroke width and so on.

To make this work, I made the map variable global, and also added one for an array of polylines:

var map, polylines;

When I create the polyline, I also add an event listener for the right-click:

polyline.addListener('rightclick', function(polyMouseEvent) {
	for (var i = 0; i < polylines.length; i++) {
		if (google.maps.geometry.poly.isLocationOnEdge(polyMouseEvent.latLng, polylines[i], 0.0001)) {
			path.removeAt(i);
		}
	}
});

That polylines array I’m looping over, I create inside the updateCoords function.  Firstly inside that function, I reset polylines to an empty array:

polylines = [];

Then inside the foreach that I’ve already got in place to loop over all the vertices in the MVCArray path, I create an invisible polyline and add it into the array of polylines.  I only do this if we’re past the first of the vertices.  And I then save these coordinates in the variable point, so I can then use that in the next iteration of this forEach loop, for an endpoint of the next invisible polyline.

if (index > 0) {
	polyline = new google.maps.Polyline({
		path: [
			point,
			element
		],
		map: map,
		visible: false,
		geodesic: true
	});
	
	polylines.push(polyline);
}

point = element;

And that’s it!

Advertisements

3 Comments »

  1. An excellent tutorial.

    Instead of having the table from which to delete a line segment it would be great if it could be achieved by right clicking on an actual vertex. I did try a few ideas but had no success. Have you any suggestions?

    Comment by Havelly — October 27, 2015 @ 3:02 am | Reply

    • Interesting… the Polyline has a ‘rightclick’ event listener, which gives you access to a PolyMouseEvent. The PolyMouseEvent has properties for edge, vertex and path, which you’d think would do the trick. However I couldn’t seem to ever get the path property; I suspect it’s only available on Polygons, not Polylines.

      The PolyMouseEvent also gives you the latLng of where the event occurred. Possibly you could use that with the google.maps.geometry.poly::isLocationOnEdge function.

      Comment by duncan — October 28, 2015 @ 10:41 pm | Reply

      • In fact yes, that seems to work. See my update at the end of this article for the details (I can post up the full code somewhere later if need be)

        Comment by duncan — October 28, 2015 @ 11:31 pm


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: