Lo primero que debemos preguntarnos es ¿cuántos marcadores se consideran muchos? Eso dependerá de varios factores, entre ellos:

  • Rendimiento: mientras más marcadores añadas más lento se vuelve el mapa. Indicar a cual número de marcador se vuelve lento el mapa es difícil de decir. Todo va a depender de qué navegador se use
  • Usabilidad: mientras más marcadores añadas más difícil puede ser para el usuario encontrar el punto que desea

Por lo general con menos de 100 marcadores no debe haber problemas (siempre y cuando los marcadores estén distribuidos a través del mapa y se pueda visualizar corréctamente), cuando hay más de 100 se debe cuestionar:

  • ¿Es el mapa lento?
  • ¿Es difícil de visualizar?
  • ¿Es difícil de verificar la data en el mapa?

Si respondes que sí a una de esas preguntas, entonces piensa como mejorar el rendimiento o la forma de como se debe visualizar la data.

Manejando los marcadores

Una forma de trabajar los marcadores es filtrándolos, crear un menú y que tenga solo seleccionado algunos por defecto y que el usuario luego escoja aquellos que desee visualizar.

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>test</title>
	<style>
	*{ margin: 0; padding: 0; }
	html, body, #map{
		width: 100%;
		height: 100%;
	}
	#menu{
		width: 200px;
		margin: 0 auto;
		background-color: #fff;
		border: 1px solid #333;
		position: relative;
		top: -50px;
		text-align: center;
		padding: 5px;
	}
	</style>
	<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false&amp;language=es"></script>
	<script type="text/javascript" src="js/map.js"></script>
</head>
<body>
	<div id="map"></div>
	<div id="menu">
		<form action="index.html" method="get">
			<input type="checkbox" id="foo" name="fooBarBaz" /> <label for="foo">Foo</label>
			<input type="checkbox" id="bar" name="fooBarBaz" /> <label for="bar">Bar</label>
			<input type="checkbox" id="baz" name="fooBarBaz" /> <label for="baz">Baz</label>
		</form>
	</div>
</body>
</html>

map.js

/**
 * Listener para el formulario
 */
function addListener(element, type, expression, bubbling)
{
	bubbling = bubbling || false;

	if(element.addEventListener) { // Standard
		element.addEventListener(type, expression, bubbling);
		return true;
	}else if(element.attachEvent) { // IE
		element.attachEvent('on' + type, expression);
		return true;
	}else return false;
}


window.onload = function(){
	var fooArr = [], barArr = [], bazArr = [];
	var options = {
		zoom: 9
		, center: new google.maps.LatLng(18.2, -66.3)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);

	var southWest = new google.maps.LatLng(17.85, -67.35);
	var northEast = new google.maps.LatLng(18.55, -65.2);
	var lngSpan = northEast.lng() - southWest.lng();
	var latSpan = northEast.lat() - southWest.lat();

	for(var i=1; i<=1000; i++){
		var lat = southWest.lat() + latSpan * Math.random();
		var lng = southWest.lng() + lngSpan * Math.random();
		var latlng = new google.maps.LatLng(lat, lng);
		var marker = new google.maps.Marker({
			position: latlng
		});

		if((i%3) == 0){ bazArr.push(marker); }
		else if((i%2) == 0){ barArr.push(marker); }
		else{ fooArr.push(marker); }
	}

	document.getElementById('foo').checked = true;
	for(var i in fooArr){
		fooArr[i].setMap(map);
	}

	addListener(document.getElementById('foo'), 'click', function(){
		if(this.checked){
			for(var i in fooArr){
				fooArr[i].setMap(map);
			}
		}else{
			for(var i in fooArr){
				fooArr[i].setMap(null);
			}
		}
	});
	addListener(document.getElementById('bar'), 'click', function(){
		if(this.checked){
			for(var i in barArr){
				barArr[i].setMap(map);
			}
		}else{
			for(var i in barArr){
				barArr[i].setMap(null);
			}
		}
	});
	addListener(document.getElementById('baz'), 'click', function(){
		if(this.checked){
			for(var i in bazArr){
				bazArr[i].setMap(map);
			}
		}else{
			for(var i in bazArr){
				bazArr[i].setMap(null);
			}
		}
	});
};

Librerías

Google maps tiene librerías adicionales para trabajar con el mapa. Todas las librerías adicionales las podemos ver en Libraries y las mencionadas en este capítulo son MarkerClusterer y MarkerManager.

Nota: Podemos usar las librerías desde la dirección web, pero lo aconsajable es que descarguen el archivo (o copien y peguen el código en un archivo js), para evitar que si hacen una actualización al código, no se afecte lo que han trabajado. También es aconsejable trabajar con la compilación. Para propósitos de enseñanza lo trabajaremos indicando la dirección web oficial directamente.

MarkerClusterer

MarkerCulterer agrupa los marcadores y muestra la cantidad de marcadores que hay en cierto segmento. En la documentación en el directorio llamado examples, muestra varios ejemplos de como utilizar esta librería. Para usarla se puede bajar el archivo markerclusterer.js, markerclusterer_compiled.js o colocar en el src de script directamente la dirección de la librería.

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>test</title>
	<style>
	*{ margin: 0; padding: 0; }
	html, body, #map{
		width: 100%;
		height: 100%;
	}
	</style>
	<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false&amp;language=es"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer_compiled.js"></script>
	<script type="text/javascript" src="js/map.js"></script>
</head>
<body>
	<div id="map"></div>
</body>
</html>

map.js

window.onload = function(){
	var markers = [];
	var options = {
		zoom: 9
		, center: new google.maps.LatLng(18.2, -66.3)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);

	var southWest = new google.maps.LatLng(17.85, -67.35);
	var northEast = new google.maps.LatLng(18.55, -65.2);
	var lngSpan = northEast.lng() - southWest.lng();
	var latSpan = northEast.lat() - southWest.lat();

	for(var i=1; i<=5000; i++){
		var lat = southWest.lat() + latSpan * Math.random();
		var lng = southWest.lng() + lngSpan * Math.random();
		var latlng = new google.maps.LatLng(lat, lng);
		var marker = new google.maps.Marker({
			position: latlng
		});

		markers.push(marker);
	}
	var markerclusterer = new MarkerClusterer(map, markers);
};

Estos son los posibles colores que se pueden mostrar en el mapa que viene por defecto:

  • Azul 2-9
  • Amarillo 10-99
  • Rojo 100-999
  • Violeta 1,000-9,999
  • Violeta oscuro 10,000+

El comportamiento que trae por defecto la clase MarkerClusterer se puede modificar en el tercer parametro. Las posibles opciones son:

  • gridSize: Número entero. Valor por defecto 60
  • maxZoom: Número entero entre el 1 al 23. Indica hasta donde debe hacer el agrupamiento
  • zoomOnClick: Valor booleano para hacer “zoom” al pulsar en el marcador. true o false. Valor por defecto true
  • averageCenter: Centrar aproximádamente el marcador. true o false. Valor por defecto true
  • minimumClusterSize: Número mínimo para agrupar los marcadores. Por defecto 2

styles: Es un array que contiene uno o varios objetos de MarkerStyleOptions.
Posibles valores son:

  • url: La dirección de la imagen a mostrar
  • height: El alto de la imagen
  • width: El ancho de la imagen
  • anchor: Array con la posición x y y de los números en la imagen. Por defecto centrado
  • textColor: Color de los números. Por defecto black
  • textSize: Tamaño de los números. Por defecto 11
  • backgroundPosition: Posición del fondo

Es importante el orden en que se indican los objetos en el tercer parametro de la clase MarkerClusterer, las imágenes se van a mostrar conforme al orden de los valores numéricos indicados en los colores por defecto.

También los números de imágenes a colocar son importante, ya que si se coloca uno, esa es la imagen a mostrar en cada uno de los niveles indicados en los colores, si son dos la primera representa el primer valor numérico y la segunda del segundo al quinto, si se colocan tres, la primera representa el primer valor numérico, la segunda el segundo y la tercera del tercero al quinto y así sucesivamente.

map.js

window.onload = function(){
	var markers = [];
	var options = {
		zoom: 9
		, center: new google.maps.LatLng(18.2, -66.3)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);

	var southWest = new google.maps.LatLng(17.85, -67.35);
	var northEast = new google.maps.LatLng(18.55, -65.2);
	var lngSpan = northEast.lng() - southWest.lng();
	var latSpan = northEast.lat() - southWest.lat();

	for(var i=1; i<=1000; i++){
		var lat = southWest.lat() + latSpan * Math.random();
		var lng = southWest.lng() + lngSpan * Math.random();
		var latlng = new google.maps.LatLng(lat, lng);
		var marker = new google.maps.Marker({
			position: latlng
		});

		markers.push(marker);
	}
	var markerclusterer = new MarkerClusterer(map, markers, {
		gridSize: 60
		, maxZoom: 11
		, zoomOnClick: false
		, minimumClusterSize: 4
		, averageCenter: true
		, styles: [{
			url: "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/people35.png"
			, height: 35
			, width: 35
			, textColor: 'white'
			, textSize: 12
			, anchor: [1,1]
		},{
			url: "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/people45.png"
			, height: 45
			, width: 45
			, textColor: 'white'
			, textSize: 14
			, anchor: [1,30]
		},{
			url: "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/people55.png"
			, height: 55
			, width: 55
			, textColor: 'white'
			, textSize: 16
			, anchor: [40,40]
		}]
	});
};

MarkerManager

MarkerManager es bien similar a MarkerClusterer en que puede agrupar varios marcadores, pero su trabajo principal es mostrar solo aquellos marcadores en el “viewport” corriente. Al mover el mapa se van mostrando los puntos de acuerdo al “viewport” que se encuentra el usuario. También se puede indicar desde cuál nivel de “zoom” deseamos mostrar los marcadores. Cuando el usuario acerque o aleje el mapa, MarkerManager mostrará aquel o aquellos marcadores indicados en ese “zoom”.

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>test</title>
	<style>
	*{ margin: 0; padding: 0; }
	html, body, #map{
		width: 100%;
		height: 100%;
	}
	</style>
	<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false&amp;language=es"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/tags/markermanager/1.0/src/markermanager_packed.js"></script>
	<script type="text/javascript" src="js/map.js"></script>
</head>
<body>
	<div id="map"></div>
</body>
</html>

map.js

window.onload = function(){
	var markers = [];
	var options = {
		zoom: 9
		, center: new google.maps.LatLng(18.2, -66.3)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);
	var markerManager = new MarkerManager(map);

	var southWest = new google.maps.LatLng(17.85, -67.35);
	var northEast = new google.maps.LatLng(18.55, -65.2);
	var lngSpan = northEast.lng() - southWest.lng();
	var latSpan = northEast.lat() - southWest.lat();

	for(var i=1; i<=1000; i++){
		var lat = southWest.lat() + latSpan * Math.random();
		var lng = southWest.lng() + lngSpan * Math.random();
		var latlng = new google.maps.LatLng(lat, lng);
		var marker = new google.maps.Marker({
			position: latlng
		});

		markers.push(marker);
	}
	google.maps.event.addListener(markerManager, 'loaded', function() {
		markerManager.addMarkers(markers, 8, 10);
		markerManager.refresh();
	});
};

En la clase MarkerManager también se puede modificar algunos de sus comportamientos, en el segundo parametro. Las posibles opciones son:

  • maxZoom: Número entero entre el 1 al 23. Indica hasta donde debe trabajar MarkerManager
  • borderPadding: MarkerManager muestra solo aquellos que están en el “viewport”, pero tiene una zona adicional al “viewport” para mostrar marcadores adicionales. La razón es que cuando se mueva el mapa a cortas distancias ya tenga recargado también algunos marcadores. Por defecto esta zona de buffer es de 100
  • trackMarkers: true o false. Si cambiamos la posición de un marcador después que lo hayas añadido al MarkerManager se van a reflejar dos simultaneamente. Al colocar esta opción como valor true le indicamos al MarkerManager que recargue de nuevo los marcadores y así se ve reflejado una sola vez el marcador que se haya modificado. Esto representa que el código corra más lento, así que si no se modificara la posición de los marcadores se aconseja dejarlo como viene por defecto con valor false

map.js

window.onload = function(){
	var markers = [];
	var options = {
		zoom: 9
		, center: new google.maps.LatLng(18.2, -66.3)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);
	var markerManager = new MarkerManager(map, {
		maxZoom: 10
		, borderPadding: 60
		, trackMarkers: true
	});

	var southWest = new google.maps.LatLng(17.85, -67.35);
	var northEast = new google.maps.LatLng(18.55, -65.2);
	var lngSpan = northEast.lng() - southWest.lng();
	var latSpan = northEast.lat() - southWest.lat();

	for(var i=1; i<=1000; i++){
		var lat = southWest.lat() + latSpan * Math.random();
		var lng = southWest.lng() + lngSpan * Math.random();
		var latlng = new google.maps.LatLng(lat, lng);
		var marker = new google.maps.Marker({
			position: latlng
		});

		markers.push(marker);
	}
	google.maps.event.addListener(markerManager, 'loaded', function() {
		markerManager.addMarkers(markers, 8, 10);
		markerManager.refresh();
	});
};

Trabajando con los niveles

Se trabaja similar al MarkerClusterer pero se puede indicar en cada uno de los niveles de “zoom”, qué tipo de imagen queremos mostrar.

map.js

window.onload = function(){
	var options = {
		zoom: 13
		, center: new google.maps.LatLng(18.372201, -66.139797)
		, mapTypeId: google.maps.MapTypeId.ROADMAP
	};
 
	var map = new google.maps.Map(document.getElementById('map'), options);
	var mnr = new MarkerManager(map);

 	google.maps.event.addListenerOnce(map, 'bounds_changed', function(){
		var markers = [];

		var bounds = map.getBounds();

		var southWest = bounds.getSouthWest();
		var northEast = bounds.getNorthEast();
		var lngSpan = northEast.lng() - southWest.lng();
		var latSpan = northEast.lat() - southWest.lat();

		for (var i = 0; i < 1000; i++) {
			var lat = southWest.lat() + latSpan * Math.random();
			var lng = southWest.lng() + lngSpan * Math.random();
			var latlng = new google.maps.LatLng(lat, lng);
			var marker = new google.maps.Marker({
				position: latlng
			});

			markers.push(marker);
		}

		var bayamon = new google.maps.Marker({
			position: new google.maps.LatLng(18.383563, -66.162713)
			, icon: 'http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/people35.png'
		});
		google.maps.event.addListener(bayamon, 'click', function() {
			map.setZoom(14);
			map.setCenter(this.getPosition());
		});

		var guaynabo = new google.maps.Marker({
			position: new google.maps.LatLng(18.3580678, -66.112674)
			, icon: 'http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/people35.png'
		});
		google.maps.event.addListener(guaynabo, 'click', function() {
			map.setZoom(14);
			map.setCenter(this.getPosition());
		});
		var towns = [bayamon, guaynabo];

		google.maps.event.addListener(mnr, 'loaded', function() {
			this.addMarkers(towns, 10, 13);
			this.addMarkers(markers, 14);
			this.refresh();
		});
	});
};

Si alejamos el mapa continuará con la misma imagen hasta el “zoom” 10, pero si lo acercamos veremos todos los marcadores. Es una buena forma de agrupar y mostrar de acuerdo al nivel de “zoom” que deseamos, esto nos da un mejor control.

El próximo capítulo estará dedicado a Geocoder.