Mapas offline en Android con OpenStreetMap y la librería MapsForge

Mapas offline en Android con OpenStreetMap y la librería MapsForge

En este artículo vamos a ver cómo integrar mapas offline en nuestra aplicación gracias a OpenStreetMap y la librería MapsForge.

Los mapas offline nos permiten mostrar al usuario de nuestra aplicación un mapa con toda la información necesaria pero sin requerir conexión a internet, los mapas se almacenan en el teléfono.

La librería MapsForge es una librería libre y gratuita que integra los mapas de OpenStreetMap en Android, sus características principales son:

  • Formato de archivo ligero para renderizado rápido de datos OpenStreetMap en el dispositivo
  • De fácil uso, similar a la librería de Google Maps (v1)
  • API de capas potente y flexible
  • Estilos de mapas personalizables mediante ficheros de configuración XML
  • Librería de sólo 400KB de tamaño
  • 100% libre y gratuita

 

Eso es lo que nos ofrece MapsForge, además hace nada han publicado una actualización que facilita mucho las cosas, el problema es que cambia mucho respecto a la versión anterior, y, a día de hoy la documentación está sin actualizar, por lo que hay que mirar los ejemplos que publicaron para poder entender algo, sobre todo si es la primera vez que la utilizas. Ese es uno de los principales motivos que me han llevado a escribir este artículo, ya que me ha costado un mundo implementar la librería en mi aplicación.

Dicho esto, vamos a  ello.

Los pasos necesarios, en resumen, son:

  • Generar un fichero con el mapa de nuestra zona y convertirlo al formato utilizado por MapsForge
  • Integrar la librería en nuestro proyecto
  • Copiar los datos a la tarjeta SD del dispositivo, ya sea a mano o mediante código, aquí para reducir el tamaño del artículo lo haremos a mano
  • Implementar la lógica de la aplicación

De extra veremos cómo ejecutar una tarea al hacer click en un Marker

1.- Generar fichero con el mapa de nuestra zona

Lo primero que hay que hacer es generar el fichero ".map" con el mapa de la zona que incluiremos en nuestra aplicación, para ello vamos a esta página web: OpenStreetMap Data Extracts y seleccionamos el continente y país de la zona que incluiremos en nuestra aplicación. Para este ejemplo yo he entrado en "Europe->Spain" y he descargado el fichero con nombre "spain-latest.osm.pbf".

Una vez tenemos el fichero con extensión "pbf" necesitamos otra herramienta más llamada Osmosis que podemos descargar pinchando aquí.

Una vez descargado lo descomprimimos y accedemos a la carpeta desde una terminal en Linux o desde la consola de comandos en Windows. Para generar nuestro mapa tendríamos que ejecutar el siguiente comando:

osmosis --read-pbf file=spain-latest.osm.pbf --bounding-box left=COORDENADA_OESTE bottom=COORDENADA_SUR right=COORDENADA_ESTE top=COORDENADA_NORTE --write-xml coruna.osm

–read-pbf file: Requiere la ruta a nuestro fichero "pbf" descargado antes.

–write-xml: Nombre del fichero generado (respetar la extensión ".osm")

Dónde se nos piden 4 valores que no conocemos, que son las coordenadas que limitan nuestra zona, por tanto tendremos que conseguirlas. Para ello entramos aquí, navegamos por el mapa hasta la zona que incluiremos en nuestra aplicación y pinchamos en la opción de la izquierda que dice "Seleccionar manualmente un área diferente" lo que nos permitirá seleccionar la zona mostrando las coordenadas. Así que la seleccionamos y lo dejamos como está.

SeleccionZona

Ahora tenemos que pasar esas coordenadas a Osmosis. Según podemos ver en la imagen anterior las coordenadas están situadas por punto cardinal, por tanto la COORDENADA_OESTE está en la izquierda, la COORDENADA_NORTE está arriba y así. Por lo que el comando quedaría así:

osmosis --read-pbf file=spain-latest.osm.pbf --bounding-box left=-8.4481 bottom=43.3252 right=-8.3747 top=43.3911 --write-xml coruna.osm

El nombre del fichero resultante es "coruna.osm", podemos llamarlo como queramos pero respetando la extensión ".osm" para no tener problemas a posteriori.

Ya tenemos el mapa de nuestra zona generado en formato OpenStreetMap, pero como vimos en las características principales MapsForge usa un formato de archivo diferente que reduce el tamaño considerablemente, por lo que nos falta convertir el fichero ".osm" generado al formato ".map" de MapsForge para ello necesitamos un plugin que pondremos a Osmosis.

El plugin en cuestión se llama "Map Writer" y lo podéis descargar desde la página de MapsForge concretamente desde su sección de Descargas dónde dice "Writer Plugin" dentro de "Release 0.4.0 Downloads" (última versión).
 El fichero descargado lo tenemos que copiar en la carpeta "lib/default" de nuestro Osmosis (la ruta en la que lo hayamos descomprimido). Después tendremos que crear un fichero dentro de la carpeta "config" de Osmosis con nombre "osmosis-plugins.conf" que contenga la línea:

org.mapsforge.map.writer.osmosis.MapFileWriterPluginLoader

Con esto ya estamos preparados para convertir el mapa, así que ejecutamos:

osmosis --read-xml coruna.osm --mapfile-writer file=coruna.map

–read-xml: Solicita el nombre del fichero a convertir (generado antes)

–mapfile-writer file: Nombre del fichero destino (respetar extensión ".map")

Tras esto, ya tendremos preparado el fichero que contiene los mapas de la zona que mostraremos en el mapa de nuestra aplicación.

2.- Integrar la librería en nuestro proyecto (Eclipse)

Tras crear nuestro proyecto procederemos primero de todo a integrar la librería en él para poder utilizarla en nuestra aplicación. Para ello creamos un carpeta llamada "libs" en el proyecto si es que aun no existe y en él copiamos/importamos los siguientes ficheros que descargamos de aquí (versión 0.4.3):

  • mapsforge-core-0.4.3.jar
  • mapsforge-map-0.4.3.jar
  • mapsforge-map-android-0.4.3.jar
  • mapsforge-map-reader-0.4.3.jar
  • svg-android-0.4.3.jar

Después los seleccionamos todos y pulsando con el botón derecho abrimos el sub menú "Build Path" y presionamos "Add to Build Path". Ahora ya podemos empezar a desarrollar nuestra app, pero antes de ello vamos a copiar el mapa a su lugar correspondiente para tenerlo disponible en la aplicación.

Librerias Mapsforge

3.- Copiar los datos a la SD del dispositivo

Como dijimos al principio, copiaremos el mapa a mano al dispositivo, para reducir el tamaño del artículo pero lo ideal sería que la aplicación se encargase de copiarlo si no existe para no tener problemas si el usuario lo elimina o algo así.

Así que en la SD del dispositivo que vayas a utilizar la aplicación hay que crear una carpeta llamada "maps" y dentro copiamos el fichero "coruna.map" generado en el primer punto.

Vayamos con el código de nuestra aplicación

4.- Implementar la lógica de la aplicación

Lo primero que necesitamos es un contenedor para nuestro mapa, así que en nuestro "xml" escribimos lo siguiente:

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.offlinemapsforgesample.MainActivity" >
 
    <org.mapsforge.map.android.view.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
 
</RelativeLayout>

 

Algo tan sencillo como añadir un "MapView" que es la vista que mostrará el mapa.

La actividad resultante es la siguiente:

 

package com.example.offlinemapsforgesample;
 
import java.io.File;
 
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
 
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
 
public class MainActivity extends ActionBarActivity {
 
	private MapView mapView;
	private TileCache tileCache;
	private TileRendererLayer tileRendererLayer;
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		AndroidGraphicFactory.createInstance(getApplication());
		setContentView(R.layout.activity_main);
 
		mapView = (MapView)findViewById(R.id.mapView);
		mapView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
 
		mapView.setClickable(true);
 
		// create a tile cache of suitable size
		tileCache = AndroidUtil.createTileCache(this, "mapcache",
				mapView.getModel().displayModel.getTileSize(), 1f, 
				mapView.getModel().frameBufferModel.getOverdrawFactor());
 
		mapView.getModel().mapViewPosition.setZoomLevel((byte) 15);
		mapView.getMapZoomControls().setZoomLevelMin((byte)14);
		mapView.getMapZoomControls().setZoomLevelMax((byte)20);
 
		String filepath = Environment.getExternalStorageDirectory().getPath() + "/maps/coruna.map";
		// tile renderer layer using internal render theme
		tileRendererLayer = new TileRendererLayer(tileCache,
				mapView.getModel().mapViewPosition, false, AndroidGraphicFactory.INSTANCE);
		tileRendererLayer.setMapFile(new File(filepath));
		tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
 
		// only once a layer is associated with a mapView the rendering starts
		mapView.getLayerManager().getLayers().add(tileRendererLayer);
 
		//mapView.setClickable(true);
		mapView.setBuiltInZoomControls(true);
		mapView.getMapScaleBar().setVisible(false);
 
		mapView.getModel().mapViewPosition.setCenter(new LatLong(43.385833, -8.406389));
 
		MyMarker marker = new MyMarker(this, new LatLong(43.385833, -8.406389), AndroidGraphicFactory.convertToBitmap(getResources().getDrawable(R.drawable.ic_launcher)), 0, 0);
		mapView.getLayerManager().getLayers().add(marker);	
	}
 
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}
 
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle action bar item clicks here. The action bar will
		// automatically handle clicks on the Home/Up button, so long
		// as you specify a parent activity in AndroidManifest.xml.
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}
		return super.onOptionsItemSelected(item);
	}
}

En ella podemos contemplar varias cosas importantes:

  • AndroidGraphicFactory.createInstance(getApplication());

Se encarga de recopilar información de nuestro dispositivo necesaria para mostrar el mapa, tiene que estar antes del "setContentView" y tiene que estar siempre presente, en caso contrario la aplicación se cierra con un error.

Después se inicializa la vista del mapa para poder usarla, como es habitual en Android y se le asigna el tipo de layer.

Le decimos que el mapa es "pulsable" para poder movernos por él y hacer zoom, sino sería un mapa estático.

  • mapView.setClickable(true);

Después de esto se crea un "Tile caché" que se encarga de cachear los "tiles" (porciones de mapa) que se van mostrando para no tener que andar cargándolas siempre desde cero y se indica el zoom por defecto y los niveles mínimo y máximo de oom. Luego se crea un "Tile renderer layer" encargado de dibujar los "tiles" al que se le indica el "Tile Caché" que utilizará y la ruta al mapa además del tipo de "renderer".

  • mapView.getLayerManager().getLayers().add(tileRendererLayer);

Esto añade la "capa" creada al mapa y por último se le indica que muestre los controles de zoom predefinidos y que oculte la barra de escala que mostraría la distancia de una proporción de mapa de forma visual, para al final centrar el mapa en una posición concreta.

Para que esto funcione tenemos que añadir a nuestro fichero Manifest el permiso:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

AndroidManifest.xml

Ya que en la tarjeta SD se almacena el caché del mapa, y es necesario poder escribir en ella.

Con esto ya tenemos nuestra aplicación funcionando y el mapa perfectamente usable.

Por último veremos como implementar el click en un "Marker" para que se puedan abrir nuevas ventanas o mostrar información relativa a un punto concreto tras pulsar en él.

EXTRA.- Ejecutar tarea al hacer click en un Marker

Algo que no sabía como hacer con esta librería y que me parece vital es cómo ejecutar algo al hacer click en un Marker para mostrar la información de ese punto o abrir una ventana nueva, por lo que vamos a ver cómo hacerlo.

Para implementar el click, tenemos que heredar el objeto "Marker" de MapsForge y realizar nuestra tarea dentro del método "onTap", así que para ello creamos una nueva clase dentro del proyecto (llamada "MyMarker" en el ejemplo) que contenga lo siguiente:

 

package com.example.offlinemapsforgesample;
 
import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.layer.overlay.Marker;
 
import android.content.Context;
import android.widget.Toast;
 
public class MyMarker extends Marker{
	private Context ctx;
 
	public MyMarker(Context ctx, LatLong latLong, Bitmap bitmap, int horizontalOffset,
			int verticalOffset) {
		super(latLong, bitmap, horizontalOffset, verticalOffset);
		this.ctx = ctx;
	}
 
	@Override
	public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
		if (this.contains(layerXY, tapXY)) {
			Toast.makeText(ctx, "Marker con latitud: " + tapLatLong.latitude + " y longitud: " + tapLatLong.longitude + " pulsado", Toast.LENGTH_SHORT).show();
		return true;
	}
		return super.onTap(tapLatLong, layerXY, tapXY);
	}
 
}

 

Y en la actividad principal añadimos estas dos líneas:

 

MyMarker marker = new MyMarker(this, new LatLong(43.385833, -8.406389), AndroidGraphicFactory.convertToBitmap(getResources().getDrawable(R.drawable.ic_launcher)), 0, 0);
mapView.getLayerManager().getLayers().add(marker);

 

Que se encargan de crear el Marker personalizado y añadirlo al mapa.

Y eso es todo por hoy, espero que con esta mini guía os sea más fácil integrar las funcionalidades de MapsForge en vuestra aplicación para poder mostrar mapas sin necesidad de conexión a internet.

Recursos:

Este es el resultado final:

Resultado final

Relacionado: