Mostrar un mapa offline en Ionic con Leaflet

En el siguiente enlace tienes el índice para acceder al resto de entradas de este tutorial:

 

¡¡Atención!! este tutorial se basa en ionic 3 y está desactualizado por lo que es posible que los ejemplos no funcionen en la última versión de ionic, haz click aquí para acceder a un tutorial mas actual de Ionic.

Hola a todos:

En posts anteriores vimos como mostrar un mapa de Google maps en ionic, sin embargo ¿que pasa si queremos mostrar información en un mapa sin conexión?, google maps no nos podrá mostrar el mapa.

En lugar de utlizar Google maps en esta ocasión vamos a utilizar Leaflet para mostrar un mapa.

Leaflet  es una biblioteca javascript de código abierto que nos permite crear mapas.

Lanzado por primera vez en 2011, es compatible con la mayoría de las plataformas móviles y de escritorio, y es compatible con HTML5 y CSS3 .

Es una de las bibliotecas de mapas de JavaScript más populares y es utilizada por los sitios web como FourSquare , Pinterest y Flickr .

Para poder mostrar un mapa offline es necesario tener alojados locálmente los titles que forman el mapa, si quieres mostrar un mapa de todo el mundo offline a día de hoy podemos afirmar  que es inviable ya que eso supondría muchísimos gigas de información, sin embargo si que puede ser viable mostrar un mapa offline de una ciudad.

Si por ejemplo estás desarrollando una aplicación de turismo de una ciudad concreta donde vas a mostrar en un mapa  los monumentos que merece la pena visitar en dicha ciudad, o cualquier otra información geolocalizada en un mapa, puede ser interesante el poder mostrar un mapa sin necesidad de tener conexión a Internet en ese preciso momento.

Vamos a ver como podemos solucionar esto en ionic 3, y como siempre la mejor manera de verlo es con un ejemplo.

Lo primero que vamos ha hacer es crear un nuevo proyecto en ionic que vamos a llamar mapaoffline:

ionic start mapaoffline blank

Una vez creado el en proyecto entramos en la carpeta con:

cd mapaoffline

Ahora vamos a descargar Leaflet, para ello descargamos la última versión desde  la sección de descargas de su página oficial en el siguiente enlace:

http://leafletjs.com/download.html

Se habrá descargado un archivo .zip, antes de nada debemos descomprimirlo para extraer su contenido:

 

Ahora vamos a crear una carpeta llamada leaflet en src/assets y dentro vamos a copiar la carpeta images y el archivo leaflet.css.

Una vez hecho esto vamos a cargar el archivo leaflet.css en index.html:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
  <meta charset="UTF-8">
  <title>Ionic App</title>
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">

  <link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico">
  <link rel="manifest" href="manifest.json">
  <meta name="theme-color" content="#4e8ef7">

  <!-- add to homescreen for ios -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">

  <!-- cordova.js required for cordova apps -->
  <script src="cordova.js"></script>

  <!-- un-comment this code to enable service worker
  
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('service-worker.js')
        .then(() => console.log('service worker installed'))
        .catch(err => console.error('Error', err));
    }
  -->

  <link href="build/main.css" rel="stylesheet">
  <link href="assets/leaflet/leaflet.css" rel="stylesheet">


</head>
<body>

  <!-- Ionic's root component and where the app will load -->
  <ion-app></ion-app>

  <!-- The polyfills js is generated during the build process -->
  <script src="build/polyfills.js"></script>

  <!-- The vendor js is generated during the build process
       It contains all of the dependencies in node_modules -->
  <script src="build/vendor.js"></script>


  <!-- The main bundle js is generated during the build process -->
  <script src="build/main.js"></script>

</body>
</html>

Ahora vamos a instalar leaflet desde consola con los siguiente comandos:

npm install leaflet --save
npm install @types/leaflet --save

Ahora que ya tenemos instalado leaflet necesitamos descargar los tiles del mapa que vamos a mostrar, para ello vamos a echar mano de una herramienta llamada Mobile Atlas Creator (MOBAC), para descargarla accedemos al siguiente enlace: http://mobac.sourceforge.net/ y bajamos en la página hasta encontrar Download y pulsamos en latest stable version para descargar la aplicación.

Una vez descargada tenemos que descomprimir el archivo zip y dentro encontraremos el archivo ejecutable.

Al ejecutar Mobile Atlas Creator nos pide introducir un nombre para el atlas (en este caso le llamamos simplemente mapa) y el formato, tenemos que seleccionar Osmdroid ZIP.

 

Pulsamos Aceptar y nos mostrará un mapa, podemos mover zoom para ver mejor la zona que queremos seleccionar. Si hacemos doble click en un punto el mapa hará zoom y se centrará en ese punto, por ello para localizar la zona que queremos mostrar es mejor partir de un zoom que nos permita ver donde estamos e ir haciendo doble click hasta obtener la localización y zoom que nos interesa.

En el panel de la izquierda en la sección map source tenemos los mapas de los que podemos obtener los tiles, muchos son solo de una zona en concreto, yo para este ejemplo voy a utilizar OpenStreetMap4UMaps.eu que nos ofrece un mapa de todo el mundo.

Debajo del panel Map Source tenemos  el panel Zoom Levels, aqui debemos seleccionar los niveles de zoom que queremos que tenga el mapa, cabe mencionar que cuantos más niveles de zoom mas tiles necesitaremos y por ende mas espacio ocuparán las imágenes.

Marcamos por ejemplo 11,12,13,14, y 15.

Ahora tenemos que seleccionar la porción del mapa que queremos descargar:

En la parte de arriba del mapa tenemos una barra de zoom que podemos mover para ver mejor la parte del mapa que queremos seleccionar. Con el ratón sobre el mapa seleccionamos un área.

Yo por ejemplo he seleccionado la zona de Bilbao, de está manera podré mostrar un mapa de Bilbao sin conexión.

Ahora debajo del panel Zoom Levels tenemos el panel Atlas content, aquí vamos a pulsar en el botón Add Selection lo que nos creará un Layer  que si lo desplegamos vemos que contiene dentro los niveles de zoom que hemos seleccionado.

Bien, una vez que hemos seleccionado el área que queremos descargar tenemos que seleccionar Create Atlas y acto seguido comenzará a generar el atlas:

El atlas que se ha generado será un archico .zip con el nombre que le hemos dado al atlas en este caso mapa seguido de la fecha y hora en la que ha sido generado y lo encontrarás en la carpeta atlases dentro de la carpeta  donde tengas Mobile Atlas Creator.

Bien, ahora vamos descomprimir el archivo zip que nos ha generado, al descomprimir vemos que dentro de la carpeta que nos ha creado hay otra carpeta llamada 4uMaps, vamos a cambiarle el nombre a la carpeta por simplemente mapa y a copiar esta carpeta dentro de la carpeta assets de nuestro proyecto:

 

Una vez que tenemos Los tiles copiados en la carpeta assets de nuestro proyecto ya podemos mostrarlos en el mapa.

Vamos a editar home.html e igual que hacíamos cuando creamos un mapa con google maps vamos a crear un div con id=”map” donde se renderizará el mapa:

<ion-header>
<ion-navbar>
  <ion-title>
    Ionic Blank
  </ion-title>
</ion-navbar>
</ion-header>

<ion-content padding>
<div id="map"></div>
</ion-content>

Ahora le vamos a definir el tamaño para que ocupe toda la pantalla por lo tanto editamos home.scss y añadimos lo siguiente:

page-home {
    #map{
        width:100%;
        height: 100%;
    }
}

Ahora en home.ts vamos a ver como se crea un mapa con leaflet:

Lo primero que debemos hacer es importar leaflet:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import * as L from 'leaflet';

Ahora vamos a crear una variable de clase llamada map donde guardaremos la instancia del mapa que vamos a crear:

...

export class HomePage {

  map : any;

  constructor(public navCtrl: NavController) {

  }

...

Ahora cuando se haya cargado la página en el método onViewDidLoad vamos a crear el mapa de la siguiente manera:

ionViewDidLoad() {

   this.map = L.map('map').
     setView([ 43.2603479, -2.9334110],
     12);

   L.tileLayer('assets/mapa/{z}/{x}/{y}.png', {    maxZoom: 15  }).addTo(this.map);

   L.marker([ 43.2603479,-2.9334110],{draggable: true}).addTo(this.map);

 }

Para crear un mapa con Leaflet utilizamos L.map(‘map’), ‘map’ es el id del div que hemos creado en la vista home.html y que es donde se va a renderizar el mapa.

Después con el metodo .setView le indicamos las coordenadas donde se tiene que posicionar el mapa, en este caso son las del centro de Bilbao.

Con L.tileLayer() añadimos al mapa una capa de tiles que va a contener los tiles del mapa que hemos descargado, como primer parámetro le pasamos la ruta donde se encuentran los tiles que puede ser una url si estamos utilizando un mapa online o la ruta donde se encuentrar alojados los archivos para mostrar el mapa offline como es el caso.

Si nos fijamos en la ruta vemos que es ‘assets/mapa/{z}/{x}/{y}.png’,   z, x e y hacen referencia respectivamente al nivel de Zoom, tiles en el eje X y tiles en el eje Y.

Obtendrán el valor correspondiente automáticamente en función de la posición del mapa y al tener la carpeta de los tiles ordenada por estos parámetros no te tienes que preocupar.

Despues con L.marker  creamos un marcador en en las coordenadas indicadas y le decimos que sea  arratrable (draggable) y con .addTo(this.map) lo añadimos a nuestro mapa.

El código completo de home.ts quedaría así:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import * as L from 'leaflet';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  map;

  constructor(public navCtrl: NavController) {

  }

  ionViewDidLoad() {

    this.map = L.map('map').
      setView([ 43.2603479, -2.9334110],
      12);

    L.tileLayer('assets/mapa/{z}/{x}/{y}.jpg', {    maxZoom: 15  }).addTo(this.map);

    L.marker([ 43.2603479,-2.9334110],{draggable: true}).addTo(this.map);

  }

}

Ahora si ejecutamos nuestra app aunque no tengamos conexión a internet podemos mostrar un mapa:

 

Eso es todo por hoy, como siempre espero que os sea de utilidad.

 

Si necesitas desarrollar una aplicación móvil no dudes en solicitarme un presupuesto sin compromiso:

Un saludo, y si aún no lo has hecho no olvides suscribirte a mi blog para no perderte los próximos posts  :-),

También puedes seguirme en Twitter en ‎@revigames y no olvides que me ayudas mucho si compartes este post en las redes sociales.

10 comentarios en “Mostrar un mapa offline en Ionic con Leaflet

  1. buen día eduardo.

    tengo una cuestión.. necesito convertir coordenadas al formato de dirección, las coordenadas la tengo en una tabla de firebase, seguí tu tutorial de mis sitios pero hay genera las coordenadas con el GPS y ahora que tengo las coordenadas ya no logro transformarlo al formato de dirección.

    me podrías decir como hacerle o donde puedo leer algo acerca de eso, ya leí la documentación de google pero no logre implementarlo a ionic.

    espero tengas un espacio en tu tiempo para contestarme, excelentes tutoriales, los sigo desde el juego de adivina el numero.

    saludos y bendiciones

    1. Hola gamster12, no entiendo muy bién cual es el problema, es decir, da igual si las coordenadas las obtienes del gps o de firebase, una vez que obtienes las coordenadas puedes usar la función getAdress que vimos en este post:
      https://reviblog.net/2017/03/23/tutorial-de-ionic-2-crear-una-aplicacion-para-guardar-nuestros-sitios-geolocalizados-parte-4-mostrando-la-direccion-a-partir-de-las-coordenadas/

      getAddress(coords):any {
      var geocoder = new google.maps.Geocoder();

      return new Promise(function(resolve, reject) {
      geocoder.geocode({‘location’: coords} , function (results, status) { // llamado asincronamente
      if (status == google.maps.GeocoderStatus.OK) {
      resolve(results);
      } else {
      reject(status);
      }
      });
      });
      }

      Luego para obtener la dirección solo tienes que llamar a la función getAddress pásandole como parámetro un objeto con las coordenadas que has obtenido por ejemplo:
      // suponiendo que tienes coords declarado
      this.coords.lat = // Aquí la latitud que has obtenido de firebase //
      this.coords.lng = // Aquí la llongitud que has obtenido de firebase //
      this.getAddress(this.coords).then(results=> {
      this.address = results[0][‘formatted_address’];
      }, errStatus => {
      // Aquí iría el código para manejar el error
      });
      }

      Un saludo

  2. hola eduardo,

    así mismo lo tengo, con una diferencia.. yo quiero mostrar la dirección de n lugares, tengo mi función getAddress y luego llamo la función de esta forma

    this.afDB.list(‘local/’).subscribe( local => {
    this.locales=local;
    local.forEach((loc) => {
    this.coords.lat=loc.latitud
    this.coords.lng=loc.longitud

    this.idLocal=loc.key;

    this.getAddress(this.coords).then(results=> {
    this.address = results[0][‘formatted_address’];
    }, errStatus => {
    // Aquí iría el código para manejar el error
    });
    console.log(this.coords,this.address);
    });

    pero el valor de address me dice unidefined, no entiendo lo que pasa, que estoy haciendo mal

    Saludos

    1. Hola Gamster12, Ya lo siento pero por ese trozo de código yo tampoco se por que te falla, comprueba que this.coords.lat y this.coords.lng realmente tiene las coordenadas correctamente, si encuentras el error puedes dejarlo en los comentarios para ayudar a otras personas que les pase lo mismo.

      Muchas gracias por comentar

      Un saludo

  3. Hola Eduardo, no eh podido resolverlo, te muestro el código de mi función getAddres.

    getAddress(coords):any {

    return new Promise(function(resolve, reject) {
    geocoder.geocode({‘location’: coords} , function (results, status) { // llamado asincronamente
    if (status == google.maps.GeocoderStatus.OK) {
    resolve(results);
    } else {
    reject(status);
    }
    });
    });
    }

    Lo que intento realizar en el código siguiente jalar información de mi base de datos de firebase de la tabla “local” y llamo la función con el parámetro this.coords.

    en la tabla de local tengo varios valores, por esa razón uso forEach, y mi intención es que me genere la dirección de varios lugares en una misma pagina.

    this.afDB.list(‘local/’).subscribe( local => {
    this.locales=local;
    local.forEach((loc) => {
    this.coords.lat=loc.latitud
    this.coords.lng=loc.longitud

    this.idLocal=loc.key;

    this.getAddress(this.coords).then(results=> {
    this.address = results[0][‘formatted_address’];
    }, errStatus => {
    // Aquí iría el código para manejar el error
    });
    console.log(this.coords,this.address);
    });

    this.coords.lat y this.coords.lng están correctos, imprimo en la imagen en lista las coordenadas.

    discúlpame, no tengo mucha habilidad para explicarme, espero puedas ayudarme.

    eso es todo el código que estoy utilizando.

    1. Hola Gamster12,

      sinceramente no se lo que te puede estar fallando, a priori el código que muestras parece correcto, siento no poder ayudarte.
      Si das con la solución puedes comentarlo aquí para que sirva de ayuda a otros.

      Gracias por comentar.

      Un saludo

  4. Buenas tardes

    ¿Quisiera saber si las coordenadas que se obtienen con leaflet son iguales a las obtenidas en Google Maps, y adicional como podría obtener la altitud de un punto?

  5. Hola! Muy bueno tu blog. Hace un tiempo estaba siguiendo los tutoriales de ionic y están muy bien explicados! aunque por motivos de trabajo no he tenido tanto tiempo para seguir por ahora. Ahora paso a dejar una pregunta por si me puedes ayudar por favor. Lo que ocurre es que quería hacer una app con videos integrados. Ya había hecho hace 1 año una hibrida y comprimido bien los videos pero igual seguía siendo bien pesada. Mi pregunta es la siguiente. Cuál es la función que se necesita para poder descargar archivos cuando esté la aplicación ya instalada. Me explico, que en apk esté solo la parte informativa y si después quiere el usuario ver un video que lo descargue al celular sólo cuando lo necesite en vez de que traiga todo incluido.

    Gracias de antemano

    Saludos!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.