Wyznaczanie trasy w google maps

W artykule pokażę prostą aplikację planera wycieczek. Wykorzystuję w niej google maps api, geolokalizację, directions api oraz jQuery UI. Krótki opis działania aplikacji: zaznaczamy na mapie punkty, przez które ma przebiegać trasa. Ich kolejność można dodatkowo zmieniać metodą drag and drop (wykorzystane jQuery sortable). Nazwy punktów są określane na podstawie geolokalizacji. Trasa jest wyznaczana na podstawie directions api.

Wyznaczanie trasy google maps Kod html (index.html):
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>Wyznaczanie trasy google maps</title>
	<link href="css/ui-lightness/jquery-ui-1.8.21.custom.css" rel="stylesheet" type="text/css">
	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
	<script type="text/javascript" src="js/jquery-ui-1.8.21.custom.min.js"></script>
	<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false&language=pl&libraries=drawing"></script>
	<link href="css/styl.css" rel="stylesheet" type="text/css">
</head>
<body>
	<div id="idPrintArea"><div id="mapka" style="width:800px;height:450px;"><!-- miejsce na mapke --></div></div>
	<div id="directionsPanel" style="position:absolute;top:0;right:0;"></div>
	<div>
		<ul id="sortable">
			<li class="ui-state-default" data-pos="1" data-lat="41.850033" data-lon="-87.6500523"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">chicago, il</span><a class="del">x</a></li>
			<li class="ui-state-default" data-pos="2" data-lat="41.525031" data-lon="-88.0817251"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">Joliet</span><a class="del">x</a></li>
			<li class="ui-state-default" data-pos="3" data-lat="40.6936488" data-lon="-89.5889864"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">Peoria</span><a class="del">x</a></li>
			<li class="ui-state-default" data-pos="4" data-lat="38.6270025" data-lon="-90.1994042"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">st louis, mo</span><a class="del">x</a></li>
		</ul>
	</div>
	<a id="add_point">Dodaj punkt</a>
	<script type="text/javascript" src="js/main.js"></script>  
</body>
</html>
W elemencie o id sortable są punkty, przez które ma przebiegać trasa. Link o id add_point służy do wchodzenia i wychodzenia z trybu dodawania punktów na mapie. Jeśli jesteśmy w trybie dodawania punktów kursor zmienia się na krzyżyk. Kliknięcie na mapę w trybie dodawania punktów doda punkt w klikniętym miejscu, spróbuje go zgeolokalizować i doda informacje o nim do listy ul. Można zmieniać kolejność punktów dzięki jquery sortable oraz je usuwać. Ponadto w divie o id directionsPanel będzie się wyświetlała wyznaczona przez Google trasa przechodząca przez wszystkie punkty.
Styl css (styl.css):
#sortable { list-style-type: none; margin: 0; padding: 0; width: 60%; }
#sortable li { margin: 0 3px 3px 3px; padding: 0.4em; padding-left: 1.5em; font-size: 1.4em; height: 18px;cursor:pointer; }
#sortable li span.ui-icon { position: absolute; margin-left: -1.3em; }
.ui-state-highlight { background:transparent; }
a.del {margin-left:10px;cursor:pointer;color:orange;}
a.active_p {color:orange;}
a#add_point {cursor:pointer;text-decoration:underline;}
#directionsPanel {width:680px;}
Są to podstawowe style dotyczące interfejsu. Omówię teraz pokrótce plik javascript (main.js) dzieląc go na części. Na końcu artykułu umieściłam również listing całej zawartości pliku main.js. Cała zawartość jest wykonywana po załadowaniu dokumentu. Na początku na elemencie o id sortable (czyli naszej liście ul punktów) aktywujemy jQuery sortable.
$("#sortable").sortable({
	placeholder: "ui-state-highlight"
});
Następnie inicjujemy podstawowe zmienne, które będziemy wykorzystywać.
var directionDisplay;
var directionsService = new google.maps.DirectionsService();
var map;
var markerArray = [];
var additionalmarkers = [];
var trasamarkers = [];
// Start/Finish icons
var icons = {
	start: new google.maps.MarkerImage(
		'number_1.png',
		new google.maps.Size( 32, 37 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 16, 37 )
	),
	end: new google.maps.MarkerImage(
		'number_2.png',
		new google.maps.Size( 32, 37 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 16, 37 )
	),
	wayp: new google.maps.MarkerImage(
		'circle.png',
		new google.maps.Size( 11, 11 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 6, 6 )
	)
};
Poniżej funkcja initialize(), która wykonuje tzw. czynności początkowe czyli inicjalizuje mapę oraz panel na przebieg trasy.
function initialize() {
	directionsDisplay = new google.maps.DirectionsRenderer({suppressMarkers: true});
	var chicago = new google.maps.LatLng(41.850033, -87.6500523);
	var mapTypeIds = [];
	for(var type in google.maps.MapTypeId) {
		mapTypeIds.push(google.maps.MapTypeId[type]);
	}
	var myOptions = {
	  zoom:7,
	  mapTypeId: google.maps.MapTypeId.ROADMAP,
	  center: chicago,
	  mapTypeControlOptions: {
			mapTypeIds: mapTypeIds,
			position: google.maps.ControlPosition.TOP_CENTER
		}
	}
	map = new google.maps.Map(document.getElementById("mapka"), myOptions);
	directionsDisplay.setMap(map);
	directionsDisplay.setPanel(document.getElementById("directionsPanel"));
}
initialize();
Kolejna funkcja służy do obliczeń związanych z trasą pomiędzy punktami. Zapisuje w tablicy wayps punkty pośrednie (tzn. wszystkie oprócz punktu początkowego i końcowego). Następnie wykonuje zapytanie do google w celu wyznaczenia trasy. Na mapie rysujemy trasę i punkty.
function calcRoute() {
	var wayps = [];
	//look waypoints :)
	$('#sortable li').each(function() {
		wayps.push({
			location: new google.maps.LatLng($(this).data('lat'), $(this).data('lon')),
			stopover: true
		});
	});
	if($('#sortable li').length<2) {
		return;
	}
	var request = {
		origin: new google.maps.LatLng($("#sortable li:first").data('lat'), $("#sortable li:first").data('lon')),
		destination: new google.maps.LatLng($("#sortable li:last").data('lat'), $("#sortable li:last").data('lon')),
		travelMode: google.maps.DirectionsTravelMode.WALKING,
		unitSystem: google.maps.UnitSystem.METRIC,
		waypoints: wayps
	};
	directionsService.route(request, function(response, status) {
	  if (status == google.maps.DirectionsStatus.OK) {
		clearTrasaMarkers();
		directionsDisplay.setDirections(response);
		var leg = response.routes[0].legs[0];
		makeMarker(response.routes[0].legs[0].start_location, icons.start, 'title');
		makeMarker(response.routes[0].legs[response.routes[0].legs.length-1].end_location, icons.end, 'title');
		for(var w=1;w<response.routes[0].legs.length-1;w++) 
		{
			if(w!=response.routes[0].legs.length-2)
			{
			  makeMarker(response.routes[0].legs[w].end_location, icons.wayp, 'title');
			}
		}
	  }
	});
}
Poniżej funkcja makeMarker służąca do wstawiania markerów na mapę oraz dodająca punkt do odpowiedniej tablicy.
function makeMarker(position, icon, title) {
	var mark = new google.maps.Marker({
		position: position,
		map: map,
		icon: icon,
		title: title
	});
	trasamarkers.push(mark);
}
Kolejny krok to wywołanie funkcji calcRoute. Należy zauważyć, że wywołujemy ją również gdy następuje aktualizacja listy czyli gdy zmienimy kolejność punktów.
calcRoute();
$("#sortable").sortable({
	update: function(event, ui) { 
		calcRoute();
	}
});
Na poniższym listingu mamy oprogramowane usuwanie oraz przełączanie do trybu dodawania punktów.
$('a.del').live('click', function() {
	if($(this).parent().hasClass('additional')) {
		for(var m=0; m<additionalmarkers.length; m++)
		{
			if(additionalmarkers[m].getPosition().lat()==$(this).parent().data('lat') && additionalmarkers[m].getPosition().lng()==$(this).parent().data('lon'))
			{
				additionalmarkers[m].setMap(null);
			}
		}
	}
	$(this).parent().remove();
	$("#sortable").sortable('refresh');
	calcRoute();
});

$('a#add_point').click(function() {
	clearAdditional();
	$(this).toggleClass('active_p');
	if($('a#add_point').hasClass('active_p')) {
		map.setOptions({draggableCursor: 'crosshair'});
	}
	else {
		map.setOptions({draggableCursor: 'url(http://maps.google.com/mapfiles/openhand.cur), move'});
	}
});
if($('a#add_point').hasClass('active_p')) {
	map.setOptions({draggableCursor: 'crosshair'});
}
Teraz pozostało już tylko oprogramowanie klikania na mapie i dodawania markerów. Po klinięciu na mapie odczytujemy współrzędne kliniętego punktu i przekazujemy je ajaxem do pliku geolocation.php, który zamienia współrzędne na adres. Ponadto po dodaniu markera na mapie odświeżamy listę oraz ponownie wywołujemy funkcję calcRoute w celu obliczenia parametrów trasy.
Zauważmy, że w funkcji dodającej marker jest oprogramowane zdarzenie dragend. Zapewnia to aktualizację trasy, gdy zmienimy położenie markera na mapie poprzez przeciągnięcie go w inne miejsce.
google.maps.event.addListener(map,"click",function(event)
{	
	if($('a#add_point').hasClass('active_p')) 
	{
		var latlng = event.latLng;
		var m = dodajMarker2(latlng,'abc',1);
		clearAdditional();
		additionalmarkers.push(m);
		//Geolokalizacja
		$.ajax({
			url: 'geolocation.php?lat='+latlng.lat()+'&lon='+latlng.lng(),
			type: 'GET',
			success: function(result_geo) 
			{
				if(result_geo.status=='OK')
				{
					$('#sortable').append('<li class="ui-state-default additional" data-pos="4" data-lat="'+latlng.lat()+'" data-lon="'+latlng.lng()+'"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">'+result_geo.results[0].formatted_address+'<span><a class="del">x</a></li>');
				}
				else
				{
					$('#sortable').append('<li class="ui-state-default additional" data-pos="4" data-lat="'+latlng.lat()+'" data-lon="'+latlng.lng()+'"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">nowe miejsce</span><a class="del">x</a></li>');
				}
				$("#sortable").sortable('refresh');
				calcRoute();
			}
		});
	}
});
function dodajMarker2(latlng,txt,id)
{
	var opcjeMarkera =  
	{ 
		position: latlng, 
		map: map,
		draggable: true
	} 
	var marker = new google.maps.Marker(opcjeMarkera);
	var beforelatlng = latlng;
	google.maps.event.addListener(marker,"dragend",function(e)
	{	
		var sortableobj = $("#sortable");
		$("#sortable li").each(function(index, element) 
		{
			if($(this).data('lat')==beforelatlng.lat() && $(this).data('lon')==beforelatlng.lng()) 
			{
				$(this).data('lat',e.latLng.lat());
				$(this).attr('data-lat',e.latLng.lat());
				$(this).data('lon',e.latLng.lng());
				$(this).attr('data-lon',e.latLng.lng());
				var $this = $(this);
				//Geolokalizacja
				$.ajax({
					url: 'geolocation.php?lat='+e.latLng.lat()+'&lon='+e.latLng.lng(),
					type: 'GET',
					success: function(result_geo) 
					{
						if(result_geo.status=='OK')
						{
							$this.find('span.txt').text(result_geo.results[0].formatted_address);
						}
						else
						{
							$this.find('span.txt').text('nowe miejsce');
						}
						$("#sortable").sortable('refresh');
					}
				});
				beforelatlng = new google.maps.LatLng($(this).data('lat'),$(this).data('lon'));
			}
		});
		sortableobj.sortable('refresh');
		calcRoute();
	});
	return marker;
}
function clearAdditional() 
{
	if (additionalmarkers) {
		for (i in additionalmarkers) {
		  additionalmarkers[i].setMap(null);
		}
	}
}
function clearTrasaMarkers() 
{
	if (trasamarkers) {
		for (i in trasamarkers) {
		  trasamarkers[i].setMap(null);
		}
	}
}
Zawartość pliku php (geolocation.php) jest bardzo prosta. Robimy w nim request do serwisu geolokalizacji google i zawracamy dane w postaci json.
header('Content-type: application/json');
if(isset($_GET['lat']) && isset($_GET['lon']))
{
	$lat = floatval($_GET['lat']);
	$lon = floatval($_GET['lon']);
	$dane = file_get_contents('http://maps.googleapis.com/maps/api/geocode/json?latlng='.$lat.','.$lon.'&sensor=false');
	echo $dane;
}
Pełna zawartość pliku main.js:
$(function() {
	$("#sortable").sortable({
	placeholder: "ui-state-highlight"
});
	var directionDisplay;
	var directionsService = new google.maps.DirectionsService();
	var map;
	var markerArray = [];
	var additionalmarkers = [];
	var trasamarkers = [];
	// Start/Finish icons
	var icons = {
		start: new google.maps.MarkerImage(
		'number_1.png',
		new google.maps.Size( 32, 37 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 16, 37 )
		),
		end: new google.maps.MarkerImage(
		'number_2.png',
		new google.maps.Size( 32, 37 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 16, 37 )
		)
		,
		wayp: new google.maps.MarkerImage(
		'circle.png',
		new google.maps.Size( 11, 11 ),
		// The origin point (x,y)
		new google.maps.Point( 0, 0 ),
		// The anchor point (x,y)
		new google.maps.Point( 6, 6 )
		)
	};
	function initialize() {
		directionsDisplay = new google.maps.DirectionsRenderer({suppressMarkers: true});
		var chicago = new google.maps.LatLng(41.850033, -87.6500523);
		var mapTypeIds = [];
		for(var type in google.maps.MapTypeId) {
			mapTypeIds.push(google.maps.MapTypeId[type]);
		}
		var myOptions = {
		  zoom:7,
		  mapTypeId: google.maps.MapTypeId.ROADMAP,
		  center: chicago,
		  mapTypeControlOptions: {
				mapTypeIds: mapTypeIds,
				position: google.maps.ControlPosition.TOP_CENTER
			}
		}
		map = new google.maps.Map(document.getElementById("mapka"), myOptions);
		directionsDisplay.setMap(map);
		directionsDisplay.setPanel(document.getElementById("directionsPanel"));
	}
	initialize();
	function calcRoute() {
		var wayps = [];
		//look waypoints :)
		$('#sortable li').each(function() {
			wayps.push({
				location: new google.maps.LatLng($(this).data('lat'), $(this).data('lon')),
				stopover: true
			});
		});
		if($('#sortable li').length<2) {
			return;
		}
		var request = {
			origin: new google.maps.LatLng($("#sortable li:first").data('lat'), $("#sortable li:first").data('lon')),
			destination: new google.maps.LatLng($("#sortable li:last").data('lat'), $("#sortable li:last").data('lon')),
			travelMode: google.maps.DirectionsTravelMode.WALKING,
			unitSystem: google.maps.UnitSystem.METRIC,
			waypoints: wayps
		};
		directionsService.route(request, function(response, status) {
		  if (status == google.maps.DirectionsStatus.OK) {
			clearTrasaMarkers();
			directionsDisplay.setDirections(response);
			var leg = response.routes[0].legs[0];
			makeMarker(response.routes[0].legs[0].start_location, icons.start, 'title');
			makeMarker(response.routes[0].legs[response.routes[0].legs.length-1].end_location, icons.end, 'title');
			for(var w=1;w<response.routes[0].legs.length-1;w++) 
			{
				if(w!=response.routes[0].legs.length-2)
				{
				  makeMarker(response.routes[0].legs[w].end_location, icons.wayp, 'title');
				}
			}
		  }
		});
	}
	
	function makeMarker(position, icon, title) {
			var mark = new google.maps.Marker({
			position: position,
			map: map,
			icon: icon,
			title: title
		});
		trasamarkers.push(mark);
	}
	calcRoute();
	$("#sortable").sortable({
	   update: function(event, ui) { 
			calcRoute();
	   }
	});
	$('a.del').live('click', function() {
		if($(this).parent().hasClass('additional')) {
			for(var m=0; m<additionalmarkers.length; m++)
			{
				if(additionalmarkers[m].getPosition().lat()==$(this).parent().data('lat') && additionalmarkers[m].getPosition().lng()==$(this).parent().data('lon'))
				{
					additionalmarkers[m].setMap(null);
				}
			}
		}
		$(this).parent().remove();
		$("#sortable").sortable('refresh');
		calcRoute();
	});
	
	$('a#add_point').click(function() {
		clearAdditional();
		$(this).toggleClass('active_p');
		if($('a#add_point').hasClass('active_p')) {
			map.setOptions({draggableCursor: 'crosshair'});
		}
		else {
			map.setOptions({draggableCursor: 'url(http://maps.google.com/mapfiles/openhand.cur), move'});
		}
	});
	if($('a#add_point').hasClass('active_p')) {
		map.setOptions({draggableCursor: 'crosshair'});
	}
	
	google.maps.event.addListener(map,"click",function(event)
	{	
		if($('a#add_point').hasClass('active_p')) 
		{
			var latlng = event.latLng;
			var m = dodajMarker2(latlng,'abc',1);
			clearAdditional();
			additionalmarkers.push(m);
			//Geolokalizacja
			$.ajax({
				url: 'geolocation.php?lat='+latlng.lat()+'&lon='+latlng.lng(),
				type: 'GET',
				success: function(result_geo) 
				{
					if(result_geo.status=='OK')
					{
						$('#sortable').append('<li class="ui-state-default additional" data-pos="4" data-lat="'+latlng.lat()+'" data-lon="'+latlng.lng()+'"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">'+result_geo.results[0].formatted_address+'<span><a class="del">x</a></li>');
					}
					else
					{
						$('#sortable').append('<li class="ui-state-default additional" data-pos="4" data-lat="'+latlng.lat()+'" data-lon="'+latlng.lng()+'"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><span class="txt">nowe miejsce</span><a class="del">x</a></li>');
					}
					$("#sortable").sortable('refresh');
					calcRoute();
				}
			});
		}
	});
	function dodajMarker2(latlng,txt,id)
	{
		var opcjeMarkera =  
		{ 
			position: latlng, 
			map: map,
			draggable: true
		} 
		var marker = new google.maps.Marker(opcjeMarkera);
		var beforelatlng = latlng;
		google.maps.event.addListener(marker,"dragend",function(e)
		{	
			var sortableobj = $("#sortable");
			$("#sortable li").each(function(index, element) 
			{
				if($(this).data('lat')==beforelatlng.lat() && $(this).data('lon')==beforelatlng.lng()) 
				{
					$(this).data('lat',e.latLng.lat());
					$(this).attr('data-lat',e.latLng.lat());
					$(this).data('lon',e.latLng.lng());
					$(this).attr('data-lon',e.latLng.lng());
					var $this = $(this);
					//Geolokalizacja
					$.ajax({
						url: 'geolocation.php?lat='+e.latLng.lat()+'&lon='+e.latLng.lng(),
						type: 'GET',
						success: function(result_geo) 
						{
							if(result_geo.status=='OK')
							{
								$this.find('span.txt').text(result_geo.results[0].formatted_address);
							}
							else
							{
								$this.find('span.txt').text('nowe miejsce');
							}
							$("#sortable").sortable('refresh');
						}
					});
					beforelatlng = new google.maps.LatLng($(this).data('lat'),$(this).data('lon'));
				}
			});
			sortableobj.sortable('refresh');
			calcRoute();
		});
		return marker;
	}
	function clearAdditional() 
	{
		if (additionalmarkers) {
			for (i in additionalmarkers) {
			  additionalmarkers[i].setMap(null);
			}
		}
	}
	function clearTrasaMarkers() 
	{
		if (trasamarkers) {
			for (i in trasamarkers) {
			  trasamarkers[i].setMap(null);
			}
		}
	}
});