Duncan's blog

June 17, 2016

Google Maps – displaying lots of markers

Filed under: Google Maps,Javascript — duncan @ 8:00 am
Tags: , , , , ,

I was inspired by this amazing site by Jill Hubley which uses publicly-available data of all the street trees in New York City, to see if the same data was available on the London DataStore.  I couldn’t find the same thing for the whole city, although I did find just two of the 33 borough councils had published this data on the nation-wide data.gov.uk site (but not the London-specific DataStore for some reason).

However, while looking for the trees data, I did find a dataset of all the allotments in London.  Pretty exciting stuff, I think you’ll agree, and something I’m sure something everyone wants to see mapped.  And I thought it might be an interesting exercise in seeing how to handle slightly larger amounts of markers than I usually do, so…

Firstly, they provide the data in three different files.  Two of them are zipped bundles containing a bunch of files in formats that aren’t familiar to me, but I assume are standard for GIS software used across the public sector.  The third is just a plain-old CSV file, so that’s what I’ve gone for.

There are 741 allotment locations listed.  I’ve imported that into a Google Spreadsheet, and got rid of the columns I didn’t think were necessary.

So initially I just want to grab all this data, and add it to a Google map as standard markers with infoWindows attached.  Nothing new here.  I exported my slightly amended version of the spreadsheet as a new .csv file, then used this site here to convert that into JSON structure.  That did a pretty nice job of giving me an array with each allotment being an object like this:

{
    "Name": "Abbots Way",
    "Location": "Alongside railway line",
    "Borough": "Bromley",
    "Organisation": 0,
    "Facilities": 0,
    "Comments": 0,
    "Latitude": 51.393386,
    "Longitude": -0.047422
}

For the fields Organisation, Facilities and Comments this data seemed to be fairly inconsistent across the various councils, but mostly they were blank, which has ended up as zero in the JSON.

I then simply set this up as a JSON structure, and minified it using this site (reducing it from 206Kb to 103Kb).  At this point it makes sense to get the data via AJAX, rather than embed this as a giant variable into my javascript code.  I’m using jQuery’s $.ajax() method instead of their $.getJson() method, just because I’m running this locally, not using a webserver, and I needed to specify the mimetype using the beforeSend callback (thanks to this answer on StackOverflow).

$.ajax({
	dataType: "json",
	url: 'allotments-min.json',
	beforeSend: function(xhr){
		if (xhr.overrideMimeType) {
			xhr.overrideMimeType("application/json");
		}
	},
	success: function(data) {
		var allotments = data.allotments;

		for (var i = 0; i < allotments.length; i++) {
			createMarker(allotments[i]);
		}

		map.fitBounds(bounds);
	}
});

Then I simply add the markers, setup an event listener to update the infowindow with the relevant content, and extend the map’s bounds to fit them all in:

function createMarker(allotment) {
	var marker = new google.maps.Marker({
		position: {lat: allotment.Latitude, lng: allotment.Longitude},
		map: map,
		title: allotment.Name
	});

	bounds.extend(marker.getPosition());

	var content = '<strong>' + allotment.Name + '</strong><br>';
	if (allotment.Location) {
		content += 'Location: ' + allotment.Location + '<br>';
	}
	if (allotment.Borough) {
		content += 'Borough: ' + allotment.Borough + '<br>';
	}
	if (allotment.Organisation) {
		content += 'Organisation: ' + allotment.Organisation + '<br>';
	}
	if (allotment.Facilities) {
		content += 'Facilities: ' + allotment.Facilities + '<br>';
	}
	if (allotment.Comments) {
		content += 'Comments: ' + allotment.Comments + '<br>';
	}

	marker.addListener('click', function() {
		infowindow.setContent(content);
		infowindow.open(map, this);
	});
}

And this produces a map that looks like this:

Allotments_-_2016-06-13_21.40.48

Well that works, and it’s simple, but there’s really too many markers tightly grouped together, and you can’t filter them down by Borough for instance.  What can we do to improve this?  The Google Maps API documentation lists several things you can do when working with large datasets.  Let’s try a KML Layer.  I need to convert my data to a KML format for starters.  I used this handy site to do that for me, turning my 59Kb .csv file into a 466Kb .kml file.  Each allotment now turned into an XML structure like this:

<Placemark>
	<name>Abbots Way</name>
	<ExtendedData>
		<SchemaData schemaUrl="#csv_20160528085652">
			<SimpleData name="Name">Abbots Way</SimpleData>
			<SimpleData name="Location">Alongside railway line</SimpleData>
			<SimpleData name="Borough">Bromley</SimpleData>
			<SimpleData name="Organisation"></SimpleData>
			<SimpleData name="Facilities"></SimpleData>
			<SimpleData name="Comments"></SimpleData>
			<SimpleData name="Latitude">51.393386</SimpleData>
			<SimpleData name="Longitude">-0.047422</SimpleData>
		</SchemaData>
	</ExtendedData>
	<Point>
		<coordinates>-0.047422,51.393386</coordinates>
	</Point>
</Placemark>

Great! So according to Google’s docs, it’s simply a case of adding a KmlLayer like so:

var kmlLayer = new google.maps.KmlLayer({
	url: 'http://www.example.com/allotments.kml',
	map: map
});

Firstly the KML file has to be publicly accessible, so I had it uploaded to my server (I’m only running the HTML file locally on my laptop).  However it didn’t like that; I had to add KML (application/vnd.google-earth.kml+xml) to my list of mime types in IIS, otherwise I got a 404 error.

This still didn’t seem to work, and I stumbled across something which suggested any KML file over 10Kb should really be turned into a KMZ file instead.  That was simply a case of zipping up the KML file, and changing the file extension to .kmz.  Oh, and then adding KMZ (application/vnd.google-earth.kmz) as a mime type in IIS as well.

This still didn’t give me my full results; markers appear, but clicking each one just gave me the title, none of the other data.  Turns out the lovely KML format I was working with contained lots of elements Google Maps API aren’t supporting (here’s the full list of what they do).  So I had to reformat it, mainly replacing all the SimpleData elements with Data elements instead, and getting rid of a Schema declaration at the top, and ending up having each allotment in this format:

<Placemark>
	<name>Abbots Way</name>
	<ExtendedData>
		<Data name="Name"><value>Abbots Way</value></Data>
		<Data name="Location"><value>Alongside railway line</value></Data>
		<Data name="Borough"><value>Bromley</value></Data>
		<Data name="Latitude"><value>51.393386</value></Data>
		<Data name="Longitude"><value>-0.047422</value></Data>
	</ExtendedData>
	<Point><coordinates>-0.047422,51.393386</coordinates></Point>
</Placemark>

Finally that started working, giving me this kind of result…

Allotments_KML_-_2016-06-13_21.44.06

At this point there’s steps I could take to tidy up the layout of the infoWindow, but why bother?  End result: a lot of faffing around for not much different from before.  In retrospect, it seems KML is really a format more for the benefit of Google Earth than Google Maps, and I’m not sure I came up with anything useful just by changing my code to use that file format.  Other than learning what’s needed to use KML files with the Google Maps API for future reference.

What about applying some marker clustering just to reduce the huge number of markers?  That’s not too tricky, I just set everything up like in my original example, included the MarkerCluster JS file, and added this line in after I’d put all the markers into an array:

var markerCluster = new MarkerClusterer(map, markers, {imagePath: 'markerclusterer/images/m'});

The only gotcha I had was I needed to add the imagePath for the marker images to appear correctly (you may not need this, depending where your JS file is).  And this then gave me:

Allotments_-_2016-06-13_21.49.14

Zooming in a bit you start to see different icons and individual markers, e.g.

Allotments_-_2016-06-13_21.55.33

Well a bit better for reducing the amount of markers displayed at any one time, and giving you an idea of how they’re grouped across the city, but still far from ideal.  What I really want is to break it down by the various boroughs.

Next step, FusionTables.  This article by Dan Nguyen was very useful: Intro to Data Mashing and Mapping with Google Fusion Tables.  It took a bit of trial-and-error, importing data from Google Spreadsheets to Google FusionTables.  I ended up with one file in Google Spreadsheets. It contained two spreadsheets:

One with all 741  allotments and their coordinates:

The other with all 33 boroughs and the coordinates for the polygons defining their boundaries:

This data came from this publicly-available KML file; I’m not sure how accurate or up-to-date that is, and it may not tally with the data to do with the allotments (e.g. boundary changes since that KML file was made may put some allotments in the wrong councils on the map).  In this second sheet I added a new column, for a count of the allotments per borough.  This used a simple formula, using COUNTIF to tally up how often the name of each council appears in the other sheet:

=COUNTIF(Allotments!C2:C742,A2)

You’ll notice in the above screenshot that in the row for Bromley the ‘geometry’ column is blank; this was also the case for several of the other councils.  It seemed to be a problem importing from the KML file into Google Spreadsheets.  I think I ended up turning this into a FusionTable, then manually editing the values for any missing polygons.

So at this point I imported both the spreadsheets into FusionTables as separate tables.  In FusionTables you get an option to turn your table into a map.  You can then choose Publish > Get HTML and JavaScript, and get all the code you’d need to turn that into a web page.  Doing that with each of these tables, I got two separate maps;

One with all the council boundaries:

Merge_of_London_Allotments_by_Borough_-_Google_Fusion_Tables_-_2016-06-14_23.06.45

And the other with small markers for all the allotments:

London_Allotments_by_Borough_-_Google_Fusion_Tables_-_2016-06-14_23.05.24

In Fusion Tables it was easy to setup the colour schemes and add the ‘# of allotments‘ legend.  All I need to do now is combine both of these into one map.

So initially I want to just display the council boundaries, so this FusionTablesLayer does that:

var boroughsLayer = new google.maps.FusionTablesLayer({
    map: map,
    suppressInfoWindows: true,
    query: {
        select: 'geometry',
        from: '15nhaHjAOYp2CrBJRJoP5bXkytmgfuRXYvGwsIuIk'
    },
    styles: [{
        where: 'count = 0',
        polygonOptions: {
            strokeColor: '#000000',
            strokeOpacity: 0.3,
            strokeWeight: 1,
            fillColor: '#edf8e9',
            fillOpacity: 0.1
        }
    },{
        where: 'count > 0',
        polygonOptions: {
            strokeColor: '#000000',
            strokeOpacity: 0.3,
            strokeWeight: 1,
            fillOpacity: 0.5,
            fillColor: '#bae4b3'
        }
    },{
        where: 'count > 15',
        polygonOptions: {
            strokeColor: '#000000',
            strokeOpacity: 0.3,
            strokeWeight: 1,
            fillOpacity: 0.5,
            fillColor: '#74c476'
        }
    },{
        where: 'count > 30',
        polygonOptions: {
            strokeColor: '#000000',
            strokeOpacity: 0.3,
            strokeWeight: 1,
            fillOpacity: 0.5,
            fillColor: '#31a354'
        }
    },{
        where: 'count > 45',
        polygonOptions: {
            strokeColor: '#000000',
            strokeOpacity: 0.3,
            strokeWeight: 1,
            fillOpacity: 0.5,
            fillColor: '#006d2c'
        }
    }]
});

The query gets all the polygon data.  Then we want to give each polygon a different style based on the number of allotments.  If you omit the ‘where’ part, you can set a default style; however you can only set up to 5 of these styles, and because I’m wanting five different colours based on the allotment count, I need to just specify all the styles for each possible option (and so I end up repeating all the properties apart from the fillColors).

This is the HTML for the map and legend:

<div id="map"></div>
    
<div id="legend">
    <p id="legend-title"># of allotments</p>
    <div>
        <span class="legend-swatch" style="background-color: #edf8e9"></span>
        <span class="legend-range">0</span>
    </div>
    <div>
        <span class="legend-swatch" style="background-color: #bae4b3"></span>
        <span class="legend-range">1 - 15</span>
    </div>
    <div>
        <span class="legend-swatch" style="background-color: #74c476"></span>
        <span class="legend-range">16 - 30</span>
    </div>
    <div>
        <span class="legend-swatch" style="background-color: #31a354"></span>
        <span class="legend-range">31 - 45</span>
    </div>
    <div>
        <span class="legend-swatch" style="background-color: #006d2c"></span>
        <span class="legend-range">46+</span>
    </div>
</div>

The values for the fillColor property obviously match up with the background colours on the legend. This adds the legend onto the map:

map.controls[google.maps.ControlPosition.RIGHT_TOP].push(document.getElementById('legend'));

Now what I want is if you click on any of the councils, it shows you just the allotments there.  This does that:

allotmentsLayer = new google.maps.FusionTablesLayer();

boroughsLayer.addListener('click', function(FusionTablesMouseEvent) {
    allotmentsLayer.setMap(null);
    
    allotmentsLayer.setOptions({
        map: map,
        query: {
            select: 'col6',
            from: '1kBhYAiZGBsIzZ-iXQZ0VC8Lhr32IUf_WOp-cYntm',
            where: "'Borough' = '" + FusionTablesMouseEvent.row.name.value + "'"
        },
        options: {
            styleId: 2,
            templateId: 2
        }
    });
});

So firstly I’ve got a global variable for the allotmentsLayer.  Each time I click a new council, I set its map property to null, removing any markers that were previously visible.  The FusionTablesLayer‘s click event handler gives you a FusionTablesMouseEvent, which lets you know which row in the FusionTable that equates to.  From this, I can get the name of the council, and I can then use that to query the FusionTable with all the allotments.

I’m also specifying an options property on the layer here.  This isn’t documented in the Maps API, but when you get the generated HTML + Javascript from FusionTables, it includes those depending on how you style your markers.  And they seemed to be required; I wasn’t able to style my markers otherwise from what I could see.

What I’m also doing is outputting the name of the council and the number of allotments it contains.  And providing a ‘show all‘ link so you can see all the allotments at any time.

<div id="borough">
    <strong id="name"></strong> <span id="count"></span>
    <p><a href="" id="showAll">Show all allotments</a></p>
</div>

I wrap these up in a div and treat it like the legend, and add it directly onto the top-middle of the map:

map.controls[google.maps.ControlPosition.TOP_CENTER].push(document.getElementById('borough'));

And in the click event listener, I update the value of the HTML:

$('#name').text(FusionTablesMouseEvent.row.name.value + ': ');
$('#count').text(FusionTablesMouseEvent.row.count.value + ' allotments');
$('#showAll,#borough').show();

The ‘show all allotments‘ link has its own event listener, which just does the query again, but without a ‘where’ clause:

$(document).ready(function() {
    $('#showAll').on('click', showAllAllotments);        
});

function showAllAllotments(event) {
    event.preventDefault();
    $('#showAll,#borough').hide();
    
    allotmentsLayer.setMap(null);
        
    allotmentsLayer.setOptions({
        map: map,
        query: {
            select: 'col6',
            from: '1kBhYAiZGBsIzZ-iXQZ0VC8Lhr32IUf_WOp-cYntm'
        },
        options: {
            styleId: 2,
            templateId: 2
        }
    });
}

And what all this gives is this:

Allotments_-_Fusion_Table_-_2016-06-15_20.24.19

You can see it working here.  This is more or less what I was hoping to end up with.  I would have liked to fit the bounds of the map to fit each council as it was selected.  And to have a list of all the councils, perhaps as a dropdown you could choose from.  I’m sure  these things must be possible.

From a data point of view, it would be good to include things like the length of the allotment waiting lists, or to have slightly more useful data about each allotment than just what the DataStore provided.

Next steps:

  • use a different mapping system such as CartoDB or MapBox
  • map what tree data is available for London

Some useful resources:

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!

March 21, 2015

Google Maps API – locked draggable markers

Filed under: Google Maps,Javascript — duncan @ 1:53 pm
Tags: , , ,

This is a nice simple post, inspired by a recent question on StackOverflow:

How to check if marker is out of specific constant bounds?

In summary, the user had defined a bounds, and wanted users to be able to drag a marker, but not outside of those bounds.

So firstly, you can simply use the LatLngBounds contains’ function to determine if a point is within those bounds.  And you can have an event listener for when the user drags the marker.  Usually you’d just want to check for the dragend event, when they stop dragging.

In this example I’ve drawn a semi-transparent rectangle using the bounds, so it’s obvious the area where you can drag your marker in.

I also keep track of the coordinates the marker had when they start dragging it.  If they go outside of the bounds, I reset it to that position.

Initial state of the map:

bounds map1

And after the user drags the marker outside of the bounds:

bounds map2

Here’s the code:

<!DOCTYPE html>
<html>
<head>
	<title>Locked draggable markers</title>
	<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
	<meta charset="utf-8">
	<style>
		#map {
			width: 640px; height: 480px;
		}
	</style>
	<script src="https://maps.googleapis.com/maps/api/js?v=3"></script>
	<script>
		function initialize() {
			var bounds = new google.maps.LatLngBounds(
				new google.maps.LatLng(51.5089872,-0.155157), 
				new google.maps.LatLng(51.599285,0.032984)
			);
			
			var map = new google.maps.Map(document.getElementById("map"), {
			  center: bounds.getCenter(),
			  zoom: 13,
			  mapTypeId: google.maps.MapTypeId.ROADMAP
			});
			
			map.fitBounds(bounds);
			
			var rectangle = new google.maps.Rectangle({
				bounds: bounds,
				map: map,
				fillOpacity: 0.2,
				fillColor: 'blue',
				strokeOpacity: 0
			});
					 
			var marker=new google.maps.Marker({
				map: map,
				position: bounds.getCenter(),
				draggable: true
			});
				
			var markerPosition;

			google.maps.event.addListener(marker, 'dragstart', function() {
				markerPosition = this.getPosition();
			});

			google.maps.event.addListener(marker, 'dragend', function() {
				if (bounds.contains(this.getPosition()) == false) {
					alert("You've dragged the marker outside of the bounds allowed. We've reset it");
					
					this.setPosition(markerPosition);
				}
			});
		}
		
		google.maps.event.addDomListener(window, 'load', initialize);
	</script>
</head>
<body>
	<div id="map"></div>
</body>
</html>

You’ll noticed I’m only declaring coordinates for the Bounds, and then getting the Bounds’ centre, and using that to plot the centre of the map and the initial coordinates for the marker.  And then making the map fit the bounds object.

One interesting note; The Google Maps API has started accepting coordinates in a lot of its classes in this shorthand format:

 {lat: x, lng: y}

… instead of having to create a new LatLng object every time:

 new google.maps.LatLng(x, y)

However, this doesn’t seem to work (yet) with the Bound class.

 

Secondly, as well as a bounds object, it might be useful to do the same thing on a polygon that you’ve drawn on the map.  This is slightly more complicated; the google.maps.Polygon class hasn’t got a contains event.  However the Geometry library does.  You need to load it in when you specify the Maps API URL:

<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry"></script>

Here I’ve just plotted a polygon roughly corresponding to the border of Utah:

Locked_draggable_markers_-_2015-03-21_13.34.50

I construct that initially as an array of LatLng points.

var boundary = [
    new google.maps.LatLng(41.987058, -114.007506),
    new google.maps.LatLng(41.987058, -111.063170),
    new google.maps.LatLng(40.999595, -111.063170),
    new google.maps.LatLng(40.999595, -109.041686),
    new google.maps.LatLng(37.005844, -109.063658),
    new google.maps.LatLng(37.005844, -114.051451)
];

I then loop over the array, adding each coordinate to a bounds object.

var bounds = new google.maps.LatLngBounds();

for(var i = 0; i < boundary.length; i++) {
    bounds.extend(boundary[i]);
}

Then I use the array of coordinates (not the bounds), to plot the path for a polygon object.

var polygon = new google.maps.Polygon({
    paths: boundary,
    map: map,
    fillOpacity: 0.2,
    fillColor: 'blue',
    strokeOpacity: 0
});

And finally just a slight change to the event listener, to check if the polygon contains the marker’s position.

google.maps.event.addListener(marker, 'dragend', function() {
    if (google.maps.geometry.poly.containsLocation(this.getPosition(), polygon) == false) {
        alert("You've dragged the marker outside of the bounds allowed. We've reset it");
            
        this.setPosition(markerPosition);
    }
});

Nice and simple!

January 22, 2015

Animated paths with Google Maps

Filed under: Google Maps,Javascript — duncan @ 6:39 pm
Tags: , ,

I saw this question on StackOverflow a few days ago.  There was a map with animated polylines being drawn along a set of coordinates, with a minor bug in it.  The very next day I saw another question asking how to animate a route on a map.  I thought the approach I’d seen the day before was ideal, and posted a slightly re-written version of it as an answer. I thought there was enough merit in the technique used to turn it into a blog post.  Thanks to the OP Alex Man for the original version of the code.

Version 1

Animated_route_-_2015-01-22_18.35.41

function initialize() {
	var map = new google.maps.Map(document.getElementById("map"), {
	  center: {lat: pathCoords[0].lat, lng: pathCoords[0].lng},
	  zoom: 11,
	  mapTypeId: google.maps.MapTypeId.ROADMAP
	});
	
	autoRefresh(map);
}

function moveMarker(map, marker, latlng) {
	marker.setPosition(latlng);
	map.panTo(latlng);
}

function autoRefresh(map) {
	var i, route, marker;
	
	route = new google.maps.Polyline({
		path: [],
		geodesic : true,
		strokeColor: '#FF0000',
		strokeOpacity: 1.0,
		strokeWeight: 2,
		editable: false,
		map:map
	});
	
	marker=new google.maps.Marker({map:map,icon:"http://maps.google.com/mapfiles/ms/micons/blue.png"});

	for (i = 0; i < pathCoords.length; i++) {
		setTimeout(function (coords)
		{
			var latlng = new google.maps.LatLng(coords.lat, coords.lng);
			route.getPath().push(latlng);
			moveMarker(map, marker, latlng);
		}, 200 * i, pathCoords[i]);
	}
}

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

var pathCoords = [
	{
	"lat": 8.893260000000001,
	"lng": 76.61427
	},
	{
	"lat": 8.894430000000002,
	"lng": 76.61418
	},
	// etc for hundreds more coordinates

View full code

The interesting bit here is the ‘autoRefresh‘ function.  That creates an empty polyline, then loops over the global pathCoords array.  For each set of coordinates, it creates a call to this anonymous function, at 200 millisecond intervals:

function (coords) {
	var latlng = new google.maps.LatLng(coords.lat, coords.lng);
	route.getPath().push(latlng);
	moveMarker(map, marker, latlng);
}

So every 200 milliseconds it calls that function.  When using setTimeout I pass in a third parameter, which is a single set of coordinates.  I’m not sure this is an ideal approach, for instance if something in that function took longer than 200ms to execute, things could get a bit disjointed looking.  But it seems to work for now.

For each set of coordinates, it adds it to the current polyline’s path.  You might have thought I’d have to use setPath, but just pushing it onto the array that getPath returns works.

Then I update the position of the marker to the current point, and pan the map.

One thing worth noting: Google Maps have started accepting coordinates in this format in a lot of places:

	{lat: 1.234, lng: 2.345}

instead of having to always create new LatLng objects each time:

	new google.maps.LatLng(1.234, 2.345)

In the example above, the coords object was fine when passed straight to the moveMarker function, i.e. for setting the marker and panning the map.  However it didn’t work when pushing it into the array of the polyline’s path.  So I still had to create a LatLng object for that.  Possibly if I’d saved the polyline path from getPath into a variable, appended the coords parameter, then done setPath with the full array, it would have been fine.

See the map working here

Version 2

Animated_route_-_2015-01-22_18.36.08

Instead of having to hardcode hundreds of coordinates in our javascript, we can use the DirectionsService, simply specifying a start and end point.  The data it returns includes a list of all the coordinates we’d need.  Then instead of rendering the directions, we can simply use those coordinates to draw our own animated path.

function initialize() {
    var map = new google.maps.Map(document.getElementById("map"), {
      center: {lat: 51.5087531, lng: -0.1281153},
      zoom: 7,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    });
    
    getDirections(map);
}

function moveMarker(map, marker, latlng) {
    marker.setPosition(latlng);
    map.panTo(latlng);
}

function autoRefresh(map, pathCoords) {
    var i, route, marker;
    
    route = new google.maps.Polyline({
        path: [],
        geodesic : true,
        strokeColor: '#FF0000',
        strokeOpacity: 1.0,
        strokeWeight: 2,
        editable: false,
        map:map
    });
    
    marker=new google.maps.Marker({map:map, icon:"http://maps.google.com/mapfiles/ms/micons/blue.png"});

    for (i = 0; i < pathCoords.length; i++) {                
        setTimeout(function(coords) {
            route.getPath().push(coords);
            moveMarker(map, marker, coords);
        }, 200 * i, pathCoords[i]);
    }
}

function getDirections(map) {
    var directionsService = new google.maps.DirectionsService();

    var request = {
        origin: new google.maps.LatLng(51.5087531, -0.1281153),
        destination: new google.maps.LatLng(48.8583694, 2.2944796),
        travelMode: google.maps.TravelMode.DRIVING
    };
    directionsService.route(request, function(result, status) {
        if (status == google.maps.DirectionsStatus.OK) {
            autoRefresh(map, result.routes[0].overview_path);
        }
    });
}

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

See the second map working here

Again, I wasn’t able to just use a coords struct, I had to create LatLng objects for passing into the DirectionsService request. The directions service happens asynchronously, so I have to wait for when it returns back an ‘OK’ status before creating the path.

Apart from replacing the array of coordinates with the value returned by the DirectionsService’s overview_path property, the code for drawing the path is basically the same as before.

Further ideas:

  • Instead of just creating a polyline from all the coordinates in the overview_path, we could loop over all the legs of the route.  This would give us more flexibility to amend the polyline, e.g. you could examine the duration property of each DirectionsStep and change the colour of the polyline.
  • We could use the bounds property of the DirectionsRoute to update the map’s bounds so the entire route is always visible.  Doing this you wouldn’t need to constantly pan the map centre.

August 9, 2014

Google Maps API – draggable polygons

Filed under: Google Maps,Javascript — duncan @ 8:00 am
Tags: , , , ,

This post was in response to a question on StackOverflow, asking how to make a draggable and editable polygon, but restrict it to always be a pentagon.

Firstly, it’s quite simple to allow a user to drag the points on a polygon, just add ‘editable: true’ to the PolygonOptions.  However it’s not possible to restrict the number of points it has; by default Google places draggable spots on the mid-points of each edge; if the user drags these then it adds an additional point.

<script>
    function initialize() {
        var map = new google.maps.Map(document.getElementById("map_canvas"), {
            zoom: 15,
            center: {lat: 51.476706, lng: 0},
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        
        // create an array of coordinates for a pentagonal polygon
        var arrCoords = [
            new google.maps.LatLng(51.474821, -0.001935),
            new google.maps.LatLng(51.474647, 0.003966),
            new google.maps.LatLng(51.477708, 0.004073),
            new google.maps.LatLng(51.479753, 0.000468),
            new google.maps.LatLng(51.477654, -0.002192)
        ];
        
        var polygon = new google.maps.Polygon({
            editable: true,
            paths: arrCoords,
            strokeColor: "#FF0000",
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: "#FF0000",
            fillOpacity: 0.35,
            map: map
        });
    }
    
    google.maps.event.addDomListener(window, 'load', initialize);
</script>

This then gives us a pentagon like:

polygon1

If we start to drag any of those points in the centre of an edge, we can ultimately end up with something like this, very far from being a pentagon:

polygon2

So, how to get around this?  My idea is that instead of making the polygon editable, we simply add our own markers to each of the 5 vertices.  These are draggable, and in response to the user dragging them, we update the polygon path.

<script>
    function initialize() {
        var map = new google.maps.Map(document.getElementById("map_canvas"), {
            zoom: 15,
            center: {lat: 51.476706, lng: 0},
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        
        // create an array of coordinates for a pentagonal polygon
        var arrCoords = [
            new google.maps.LatLng(51.474821, -0.001935),
            new google.maps.LatLng(51.474647, 0.003966),
            new google.maps.LatLng(51.477708, 0.004073),
            new google.maps.LatLng(51.479753, 0.000468),
            new google.maps.LatLng(51.477654, -0.002192)
        ];
        
        var polygon = new google.maps.Polygon({
            paths: arrCoords,
            strokeColor: "#FF0000",
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: "#FF0000",
            fillOpacity: 0.35,
            map: map
        });
        
        // add a marker at each coordinate
        for (var i = 0; i < arrCoords.length; i++) {
             var marker = new google.maps.Marker({
                position: arrCoords[i],
                map: map,
                draggable: true
            });
                        
            bindMarker(marker, arrCoords, i, polygon);
        }
    }
    
    function bindMarker(marker, arrCoords, i, polygon) {
        google.maps.event.addListener(marker, 'dragend', function(e) {
             arrCoords[i] = e.latLng;
             polygon.setPath(arrCoords);
        });
    }
    
    google.maps.event.addDomListener(window, 'load', initialize);
</script>

When a marker gets dragged, we update the coordinates of the corresponding point in our path array.  We then update the path on the polygon.  Simple!

polygon3

After dragging the markers, we still have only 5 edges:

polygon4

So that’s all very well and good, but those markers can be a bit obtrusive looking on our page.  Well we can specify custom icons instead.  In this case we can even just use the Symbol class to come up with something much nicer looking:

            var marker = new google.maps.Marker({
                position: arrCoords[i],
                map: map,
                draggable: true,
                icon: {
                    path: google.maps.SymbolPath.CIRCLE,
                    scale: 5,
                    strokeColor: "#FF0000",
                    strokeOpacity: 0.8,
                    strokeWeight: 2,
                    fillColor: "#FFFFFF",
                    fillOpacity: 1
                }
            });

polygon5

Also it might be that we want to be able to drag the entire polygon to another location on the map (and was a requirement of the original question on StackOverflow).  In that case we have to update the markers at the same time.  It gets slightly more complicated here; basically I create an array of markers (always useful anyway).  Each time we drag the polygon, we update this array.  We also remove any markers previously created.

<script>
    function initialize() {
        var map = new google.maps.Map(document.getElementById("map_canvas"), {
            zoom: 15,
            center: {lat: 51.476706, lng: 0},
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        
        // create an array of coordinates for a pentagonal polygon
        var arrCoords = [
            new google.maps.LatLng(51.474821, -0.001935),
            new google.maps.LatLng(51.474647, 0.003966),
            new google.maps.LatLng(51.477708, 0.004073),
            new google.maps.LatLng(51.479753, 0.000468),
            new google.maps.LatLng(51.477654, -0.002192)
        ];
        
        var polygon = new google.maps.Polygon({
            paths: arrCoords,
            strokeColor: "#FF0000",
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: "#FF0000",
            fillOpacity: 0.35,
            map: map,
            draggable: true
        });
        
        var markers = addMarkers(arrCoords, map, polygon, []);
        
        google.maps.event.addListener(polygon, 'dragend', function(e) {
             markers = addMarkers(this.getPath().getArray(), map, this, markers);
        });
    }
    
    function addMarkers(arrCoords, map, polygon, oldMarkers) {
        var markers = [];
        
        // clear any existing markers
        for (var i = 0; i < oldMarkers.length; i++) {
            oldMarkers[i].setMap(null);
        }
        
        // add a marker at each coordinate
        for (var i = 0; i < arrCoords.length; i++) {
             var marker = new google.maps.Marker({
                position: arrCoords[i],
                map: map,
                draggable: true,
                icon: {
                    path: google.maps.SymbolPath.CIRCLE,
                    scale: 5,
                    strokeColor: "#FF0000",
                    strokeOpacity: 0.8,
                    strokeWeight: 2,
                    fillColor: "#FFFFFF",
                    fillOpacity: 1
                }
            });
        
            bindMarker(marker, arrCoords, i, polygon);
            
            markers.push(marker);
        }
        
        return markers;
    }
    
    function bindMarker(marker, arrCoords, i, polygon) {
        google.maps.event.addListener(marker, 'dragend', function(e) {
             arrCoords[i] = e.latLng;
             polygon.setPath(arrCoords);
        });
    }
    
    google.maps.event.addDomListener(window, 'load', initialize);
</script>

October 19, 2013

Google maps – map size

Filed under: Google Maps,Javascript — duncan @ 10:47 am
Tags: , ,

I posted an answer in response to this question on StackOverflow, ‘How can I know the actual scale that my Google maps is currently in?‘.  Which perhaps didn’t exactly answer Joaquín M‘s problem, but I thought was useful enough anyway to warrant its own blog post, in my infrequent Google Map API series.  In his question he stated “The information I need is the actual width in kilometers of my map“, which my answer does do.

What we want to do is have an event listener for whenever the size of the map changes, using the bounds_changed event.  This way we get the updated size if the user resizes the window or adjusts the zoom.  And in fact this also happens when the map first appears.  If we try and use map.getBounds() within our initialize function, we run the risk of calling it too soon before the map has fully rendered, so we need that event listener.

We can find out the map’s size from its bounds, which represent a rectangular box of what’s currently visible.  But from the bounds object, we can only find out the coordinates for its north-east and south-west corners.  So we’ve still got a bit of work to do to turn this into a distance.

We’ll need to use the geometry library to turn degrees into metres.  So we specify libraries=geometry in the Google Maps API URL.

Let’s assume we have a map with its north-east coordinates as (lat1, lng1) and its south-west coordinates as (lat2, lng2), e.g.:

map1-001

Sidenote: confusingly whenever I look at coordinates I always think of Cartesian coordinates, e.g. (x, y), where the first value x represents the horizontal value, and the second value y represents the vertical value.  However when dealing with LatLng objects in Google Maps it’s always latitude first (degrees north/south from the equator), then longitude second (degrees east/west from the Greenwich meridian).  So that’s how I’ll be referring to them here.

I’m assuming we want to calculate both the width and height of the bounds, not just its area or the diagonal distance between the two corners.  So what we really need to do is work out the vertical distance from lat1 to lat2, and then the horizontal distance from lng1 to lng2.  To do this we have to construct LatLng objects representing the points (lat1, lng1) and (lat2, lng1) [fig 1] for the vertical distance.  And again (lat1, lng1) and (lat1, lng2) [fig 2] for the horizontal.  Or equally we could have done (lat1, lng2) – (lat2, lng2), and then (lat2, lng1) – (lat2, lng2).

map1(1)

Fig 1

Fig 2

Fig 2

I could have simply put all the below into the initialize function.  However certain parts of it made sense to put into their own functions, as they could then easily be re-used, e.g. for any time we might want the distance between any two points on the map, or for calculating the distance of any rectangular area on the map.

<!DOCTYPE html>
<html>
<head>
<title></title>

<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 90% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { height: 100% }
</style>

<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false&libraries=geometry"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>

<script type="text/javascript">
    function initialize() {
        var homeLatlng = new google.maps.LatLng(51.476706,0); // London

        var myOptions = {
            zoom: 15,
            center: homeLatlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };

        var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

        google.maps.event.addListener(map, 'bounds_changed', function() {            
            var bounds = map.getBounds();

            var sizes = getBoundsSize(bounds);

            $('#size').text(sizes.horizontalkm + ' km horizontal, ' + sizes.verticalkm + ' km vertical');
        });
    }

    function getBoundsSize(bounds) {
        var sizes, NE, SW, lat1, lat2, lng2, lng2, horizontalLatLng1, horizontalLatLng2, verticalLatLng1, verticalLatLng2, horizontal, vertical;

        // get the coordinates for the NE and SW corners
        NE = bounds.getNorthEast();
        SW = bounds.getSouthWest();

        // from that, figure out the latitudes and the longitudes
        lat1 =  NE.lat();
        lat2 =  SW.lat();

        lng1 =  NE.lng();
        lng2 =  SW.lng();

        // construct new LatLngs using the coordinates for the horizontal distance between lng1 and lng2
        horizontalLatLng1 = new google.maps.LatLng(lat1,lng1);
        horizontalLatLng2 = new google.maps.LatLng(lat1,lng2);

        // construct new LatLngs using the coordinates for the vertical distance between lat1 and lat2
        verticalLatLng1 = new google.maps.LatLng(lat1,lng1);
        verticalLatLng2 = new google.maps.LatLng(lat2,lng1);

        // work out the distance horizontally
        horizontal = getDistance(horizontalLatLng1, horizontalLatLng2);

        // work out the distance vertically
        vertical = getDistance(verticalLatLng1, verticalLatLng2);

        // round to kilometres to 1dp
        sizes = {
            horizontalkm: convertMetresToKm(horizontal),
            verticalkm:   convertMetresToKm(vertical)
        };

        return sizes;
    }

    function getDistance(point1, point2) {
        // computeDistanceBetween is fine if we only have 2 points, but if we have more we need to use computeLength
        return google.maps.geometry.spherical.computeDistanceBetween(point1, point2);
    }

    function convertMetresToKm(metres) {
        return Math.round(metres / 1000 *10)/10;    // distance in km rounded to 1dp
    }

    google.maps.event.addDomListener(window, 'load', initialize);
</script>
</head>
<body>
    <div id="map_canvas"></div>
    <div id="size"></div>
</body>
</html>

And finally you end up with a map looking something like this, and as you zoom or resize the window, the sizes update.
gmap

September 12, 2013

Draggable polyline markers

Filed under: Google Maps,Javascript — duncan @ 6:39 pm
Tags: , , ,

In response to a comment on a previous post I did with draggable polylines, I wrote this little example.  I’ve drawn a polyline along several coordinates, with a marker at each point.  And if you drag the markers to new locations, it updates the polyline.

<!DOCTYPE html>
<html>
<head>
<title></title>

<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map { width:500px; height:400px }
</style>
<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
<script>
    var arrDestinations;

    function initialize() {
        var i, latlng, nextlatlng, marker, map = new google.maps.Map(document.getElementById("map"), {
            zoom: 15,
            center: new google.maps.LatLng(56.873406,-5.432421),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });

        arrDestinations = [
            {lat: 56.872831, lng: -5.442528},
            {lat: 56.874837, lng: -5.435876},
            {lat: 56.876056, lng: -5.433408},
            {lat: 56.876267, lng: -5.431155},
            {lat: 56.875306, lng: -5.428301},
            {lat: 56.874332, lng: -5.4277},
            {lat: 56.873394, lng: -5.427786}
        ];

        for (i = 0; i < arrDestinations.length; i++) {
            latlng = new google.maps.LatLng(arrDestinations[i].lat, arrDestinations[i].lng);

            if (i < arrDestinations.length-1) {
                nextlatlng = new google.maps.LatLng(arrDestinations[i+1].lat, arrDestinations[i+1].lng);

                // draw a line from this marker to the next one 
                arrDestinations[i].polyline = new google.maps.Polyline({
                    path: [latlng, nextlatlng],
                    strokeColor: "#FF0000",
                    strokeOpacity: 0.5,
                    strokeWeight: 2,
                    map: map
                });
            }

            marker = new google.maps.Marker({
                position: latlng,
                map: map, 
                draggable: true
            });

            bindMarkerToPolylines(marker, i);
        }
    }

    function bindMarkerToPolylines(marker, index) {
        google.maps.event.addListener(marker, 'dragend', function() {
            var nextlatlng, prevlatlng, newMarkerLatLng = marker.getPosition();

            // for all markers apart from the last one, we have the polyline from this marker to the next one to update
            if (index < arrDestinations.length-1) {
                nextlatlng = new google.maps.LatLng(arrDestinations[index+1].lat, arrDestinations[index+1].lng);
                arrDestinations[index].polyline.setPath([newMarkerLatLng, nextlatlng]);
            }

            // for all markers apart from the first one, we have the polyline to this marker from the previous one
            if (index > 0) {
                prevlatlng = new google.maps.LatLng(arrDestinations[index-1].lat, arrDestinations[index-1].lng);
                arrDestinations[index-1].polyline.setPath([prevlatlng, newMarkerLatLng]);
            }

            // update our lat/lng values
            arrDestinations[index].lat = newMarkerLatLng.lat();
            arrDestinations[index].lng = newMarkerLatLng.lng();
        });
    }

    google.maps.event.addDomListener(window, 'load', initialize);
</script>

</head>
<body>
    <div id="map"></div>
</body>
</html>

Which gives me a lovely little map like this (centred on the Glenfinnan viaduct in case you’re wondering):

draggablePolyline

And I can then drag the markers to new locations, and end up with it looking more like so:

draggablePolyline2

Although the number of markers remains the same.  An exercise for another day would be how to delete any marker.

March 1, 2013

Google Maps API – draggable polylines

Filed under: Google Maps,Javascript — duncan @ 6:26 pm
Tags: , , , ,

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 three global arrays for labels, polylines and markers. It’s much simpler in this case just to have one global array which contains destinations, labels, polylines and markers.

So 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 around the map, and the polylines connecting it to the other markers will update to stay elastically-attached to the one draggable marker.  And the distances will update automatically.  Again we’re using the Label code courtesy of Marc Ridey (which I’m not going to repeat here, see my earlier blogpost).  And the rest of the code remains quite similar to previously, with some key differences.

	
var arrDestinations;

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

	arrDestinations = [
		{title: 'Place A',	lat: 34.602694,	lng: -106.066132},
		{title: 'Place B',	lat: 33.917153,	lng: -106.869736},
		{title: 'Place C',	lat: 34.254946,	lng: -105.599442}
	];

	var stuHome = {title: 'Where I am', lat: 34.148181, lng: -106.17897};

	var homeLatlng = new google.maps.LatLng(stuHome.lat, stuHome.lng);
	var myOptions = {
		zoom: 9,
		center: homeLatlng,
		mapTypeId: google.maps.MapTypeId.SATELLITE
	};
	map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

	var homeMarker = new google.maps.Marker({
		position: homeLatlng, 
		map: map, 
		title: stuHome.title,
		draggable: true
	});

	$('#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
		});


		// calculate the distance between home and this marker
		stuDistances = calculateDistances(homeLatlng, latLng);
		$('#tableNeighbours').append(
			  '<tr id="row' + i + '">' 
			+ '<td>' + arrDestinations[i].title + '</td>'
			+ '<td class="km">' + stuDistances.km + ' km</td>'
			+ '<td class="miles">' + 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');
		arrDestinations[i].label.setMap(map);
	}

	// lets make the home marker draggable, but none of the others, just for illustrating how we can make the polylines move dynamically
	google.maps.event.addListener(homeMarker, 'dragend', function() {
		var i, stuDistance, inBetween, marker, markerLatLng;
		var homeLatLng = homeMarker.getPosition();

		// do this for each of the markers 
		for (i = 0; i < arrDestinations.length; i++) {
			markerLatLng = arrDestinations[i].marker.getPosition();

			arrDestinations[i].polyline.setPath([homeLatLng, markerLatLng]);
			stuDistance = calculateDistances(homeLatLng, markerLatLng);
			arrDestinations[i].label.set('text', stuDistance.miles + ' miles');

			// update the position of the marker which the label is bound to
			inBetween = google.maps.geometry.spherical.interpolate(homeLatLng, markerLatLng, 0.5);
			marker = new google.maps.Marker({  
				position: inBetween,  
				map: map,
				visible: false
			});
			arrDestinations[i].label.set('position', inBetween);

			// update values in table
			$('#row' + i + ' td.km').text(stuDistance.km + ' kilometres');
			$('#row' + i + ' td.miles').text(stuDistance.miles + ' miles');
		}
	});
}

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;
}

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

This time when creating the ‘homeMarker’ I make it draggable using this option: draggable: true.

I also added a class to each of the kilometres and miles cells, so they’re easy to identify later when I want to dynamically update them.

And the markers are appended into an array so we can use them later when dragging the centre marker.

And this time we make the labels appear automatically by calling label.setMap(map); as we create each marker, and I never remove them.

We then have a new event listener for the ‘dragend‘ event on the homeMarker.  I’ve tried using ‘drag‘ instead, but it seemed slightly processor-intensive to use (although it did automatically redraw lines and distances as I dragged  – now I’m just redrawing them when the user releases the marker).

And this is what I see. Dragging the red marker redraws the polylines and updates the distances on the labels:
new mexico

…and the table underneath displays the distances in miles and km, and automatically updates along with the labels:

Destination Where I am
Place A 51.6 km 32.1 miles
Place B 68.7 km 42.7 miles
Place C 54.7 km 34 miles

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.

February 14, 2013

Google Maps API – polylines and distances

Filed under: Google Maps,Javascript — duncan @ 6:56 pm
Tags: , , , ,

So after a basic introduction to polylines, let’s try doing a bit more with them. Here’s a simple example based on something I was doing for work. Supposing you have a bunch of different locations, and you want to show the distance between them? In this case I had one central destination, and I wanted to show the distance from there to several nearby towns. I did this initially using polylines, as in this example, although in reality it would be more useful to show driving directions (more on that in a future post).

Here’s what I came up with:

ambleside

And I displayed this table beneath the map:

Destination Distance from Ambleside
Keswick 22.2 km 13.8 miles
Coniston 9.9 km 6.1 miles
Lake District 11.3 km 7 miles
Cumbria 19.7 km 12.2 miles

And here’s the Javascript I used to do it. Originally I was getting my coordinates from the database using ColdFusion, and then writing them into the Javascript. I was also doing some infowindows attached to the markers, and labels on the polylines indicating the distances. But for this post I’ve just kept it simple for now.

<!DOCTYPE html>
<html>
<head>
<title>lines between the nearest destinations showing distance</title>

<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { width:600px; height:500px }
</style>
<!-- need to load the geometry library for calculating distances -->
<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?libraries=geometry&sensor=false"></script>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script>

<script type="text/javascript">
    function initialize() {
        var map, i, latLng, marker, polyline, stuDistances;
    
        var 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);
            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)
            polyline = new google.maps.Polyline({
                path: [homeLatlng, latLng],
                strokeColor: "#FF0000",
                strokeOpacity: 0.5,
                strokeWeight: 4,
                geodesic: true,
                map: map
            });
            
            // calculate the distance between home and this marker
            stuDistances = calculateDistances(homeLatlng, latLng);
            $('#tableNeighbours').append(
                  '<tr>' 
                + '<td>' + arrDestinations[i].title + '</td>'
                + '<td>' + stuDistances.km + ' km</td>'
                + '<td>' + stuDistances.miles + ' miles</td>'
                + '</tr>'
            );
        }
    }
    
    function calculateDistances(start,end) {
        var stuDistances = {};
        
        stuDistances.metres = google.maps.geometry.spherical.computeDistanceBetween(start, end);    // distance in metres
        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;
    }
    
    google.maps.event.addDomListener(window, 'load', initialize);
</script>

</head>
<body>
    <div id="map_canvas"></div>
    
    <table id="tableNeighbours" border="1"></table>
</body>
</html>

So I’m dynamically adding the distance information from each location to the table as each marker is added. You’ll notice that in the calculateDistances function, instead of simply dividing the metres distance by 1000 to get it in kilometres, I multiply by 10, then round, then divide by 10. This is in order to round the distances to 1 decimal place.

e.g. the distance between my Ambleside and Keswick markers is 22205.96176105692 metres, according to Google. So to convert that to kilometres if I just divide by 1000 it gives:
22.20596176105692
The Round() function rounds to the nearest integer. So instead let’s only divide by 100 (or divide by 1000 then multiply by 10, as I’m doing, same thing). Which gives:
222.0596176105692
Then we call Math.round(), which gives:
222
And finally divide that by 10:
22.2
There are other ways to achieve this in javascript of course!

Notice as well that we’ve added the optional `libraries=geometry` parameter to the URL for the Google Maps JS file. This is so we can use the Geometry Library to calculate the distance between locations, using the computeDistanceBetween function. Also read this useful article which explains some more about using the Geometry Library.

Next Page »

Blog at WordPress.com.