Tutorial. App (Citymapper) con Ionic 2 Beta

30/04/2016

Hoy vamos hacer una app cutre inspirada en Citymapper (por si os hace ilusión y tal) usando la versión beta 25 de Ionic 2. El primer paso será instalar Ionic 2 Beta y Cordova 4.2.0, algo bastante obvio y que haremos con estos sencillos comandos:

sudo npm install -g ionic@beta sudo npm install -g cordova@4.2.0

Ahora creamos un proyecto predefinido para comprender mejor como funciona este sistema para generar apps multiplataforma (Android/IOS):

/* Creamos un proyecto predeterminado Ionic con la etiqueta --v2 */ ionic start cutePuppyPics --v2 /* Ejecutamos
    este comando dentro de la carpeta cutePuppyPics para subsanar algun bug */ npm install node-sass@3.4.2 /* Lanzamos
    la app y esperamos a que se muestre en el navegador */ ionic serve

Enhorabuena, acabas de configurar tu entorno de trabajo, pero lo mejor será que nos remanguemos las mangas para hacer nuestra App. Para no tener que perder el tiempo en engorrosas explicaciones pasare a entregaros el código en cuestión que es como mejor se aprende y así comprendamos un poco como funciona Ionic:

Nuestro objetivo es hacer una app que tire del servicio de Google Maps API para que nos busque rutas a un destino que no interese, por eso tenemos que incluir el script de Google Maps en www/index.html. Es importante tener conocimientos de ES6 y JQuery respectivamente para entenderlo mejor. En la ruta app/pages se encuentran las páginas del proyecto: home, la página encargada de las búsquedas y Map, donde tendremos la posibilidad de ver el mapa correctamente. Pero ahora lo que nos importa es configurar el fichero app/app.js, he dejado algunas anotaciones:

/* Fichero  app/app.js */

import 'es6-shim';
import {App, Platform} from 'ionic-angular';
import {StatusBar} from 'ionic-native';
/* Importaremos nuestras dos paginas */
import {HomePage} from './pages/home/home';
import {Map} from './pages/map/map';

@App({
  template: '<ion-nav [root]="rootPage"></ion-nav>',
  config: {} 
})
export class MyApp {
  static get parameters() {
    return [[Platform]];
  }

  constructor(platform) {
   /* Nuestra vista principal sera Home */
    this.rootPage = HomePage;
    platform.ready().then(() => {
      StatusBar.styleDefault();
    });
  }
}

app/pages/home/home.html: este fichero contiene un ion-navbar con su objeto *navbar y su variable city, en ion-content hemos creado un row con 4 columnas (25% de ancho) formado por 4 items que actuaran como link para la página Map gracias a la función itemTapped que explicaremos más adelante. Un div para el mapa y un ion-list que actúa como formulario con su input (modelo search para recoger lo que tecleemos) y su botón. Para finalizar, tendremos un div con su identificador panel. Veamos el contenido de app/pages/home/home.js:

/* Importamos los elementos que nos interesa:
Geolocation: plugin de cordova
Alert: para las alertas
NavController y NavParams: paso de parametros a otra pagina
*/
import {
    Page, Geolocation, Alert, NavController, NavParams
}
from 'ionic-angular';


/* Importamos la pagina Map para invocarla con itemTapped */
import {
    Map
}
from '../map/map';

@
Page({
    templateUrl: 'build/pages/home/home.html'
})

export class HomePage {
    static get parameters() {
        return [[NavController], [NavParams]];
    }

/* Variables globales en el constructor */
    constructor(nav) {
        /* Obligatorio */
        this.nav = nav;
        /* var para el mapa de Google Maps API */
        this.map = null;
        /* var para el destino */
        this.destination = "";
        /* var para el origen */
        this.origin = null;
        /* var para la busqueda en el input */
        this.search = null;
        /* enumerable con los tipos de transporte */
        this.icons = ['car', 'walk', 'bus', 'bicycle'];
        /* enumerable con los tipos de modo de transporte */
        this.modes = ['DRIVING', 'WALKING', 'TRANSIT', 'BICYCLING'];
        /* lista vacia para guardar los items */ 
        this.items = [];
        /* Muy obligatorio */
        this.render = new google.maps.DirectionsRenderer();
        
        /* Lanzamos una funcion nada mas cargar la pagina Home */
        this.initMap();
    }
    
    /*FUNCIONES */

}

initMap: función encargada de geolocalizar nuestra posición y dibujar el mapa en home:

initMap() {
    let options = {
        timeout: 10000,
        enableHighAccuracy: true
    };
    navigator.geolocation.getCurrentPosition(
        (position) => {
            /* Guardamos nuestro origen en la variable origin */
           
            this.origin = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
            let mapOptions = {
                center: this.origin,
                zoom: 15,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                disableDefaultUI: true,
                draggable: false
            }
            /* Dibujamos el mapa en id="map" de home.html */
            this.map = new google.maps.Map(document.getElementById("map"), mapOptions);
            
            /* Lanzamos la funcion */
            this.searchCity();
        }, (error) => {
           /* Si fallamos con la geolocalizacion pasamos a mostrar un mensaje de error */
            let not_geo = Alert.create({
                title: 'Wrong!',
                subTitle: 'You have internet?',
                buttons: ['OK']
            });
            /* Agregamos el error a nav */
            this.nav.present(not_geo);
        }, options
    );
}    

searchCity: función encargada de solicitar un json a Google Maps API y extraer nuestra ciudad en función de la variable origin:

searchCity() {
    /* ok, peticion del servicio */
    let url = "http://maps.googleapis.com/maps/api/geocode/json?latlng=" + this.origin.lat() + ", " + this.origin.lng();
    
    /* Usamos jquery  */      
    $.getJSON(url, function (json) {
        $("ion-navbar ion-buttons").append(json.results[0].address_components[3].long_name);
    })
    
    /* Esto no debia estar aqui... simplemente creo 4 items.
       Podeis comprobar el FOR home.html y sacar una conclusion
       de todo esto.     
    */
    for (let i = 0; i < 4; i++) {
        this.items.push({
            mode: this.modes[i],
            icon: this.icons[i]
            });
        }
}

Destination: función que salva la búsqueda realizada en el input. La variable destination guardara nuestro destino en formato completo.

Destination() {
    /* La variable para guardar */    
    let dat_aux = null;
       
       /* 
       Ajax no asincrono (lo se...). Hago una solicitud en funcion de la busqueda (this.search) y 
       la ciudad guardada en $("ion-navbar ion-buttons").html() 
       */
        $.ajax({
        url: "http://maps.googleapis.com/maps/api/geocode/json?address=" + this.search + ", " + $("ion-navbar ion-buttons").html(),
        async: false,
        dataType: 'json'
    }).done(function (res) {
        dat_aux = res;
    });
       
        /* Guardamos el resultado en destination */
        this.destination = "";
        if (dat_aux.results[0].geometry.location_type != "APPROXIMATE")
        this.destination = dat_aux.results[0].formatted_address;
        
        /* Calculamos la ruta idonea */
        this.calculateRoute();
}

calculateRoute: función encargada de hacer una petición a Google Maps Api con rutas alternativas exclusivamente de autobuses. Dibuja las rutas pertinentes y muestra las indicaciones en función de cada una de ellas:

calculateRoute() {
    if (this.destination != "" || this.destination != "APPROXIMATE") {
        $(".show_alternative").css("display", "block");
        let directionsDisplay = null;
        
        /* Esto me ayuda a refrescar */
        directionsDisplay = this.render;
        let directionsService = new google.maps.DirectionsService();

        document.getElementById("panel").innerHTML = "";
        directionsDisplay.setMap(this.map);
        directionsDisplay.setPanel(panel);
            directionsService.route({
            /* Introduzco mi origen */
            origin: this.origin,
            /* Introduzco mi destino */
            destination: this.destination,
            travelMode: google.maps.TravelMode.TRANSIT,
            /* Ver rutas alternativas */
            provideRouteAlternatives: true,
            /* Solo recorridos por BUS */
            transitOptions: {
                modes: [google.maps.TransitMode.BUS]
            }
            }, function (response, status) {
            if (status === google.maps.DirectionsStatus.OK) {
                /* Pintamos el mapa y el panel */
                directionsDisplay.setDirections(response);
                directionsDisplay.setMap(map);
            } else {
                /* Borramos todo si el resultado es infructuoso */
                directionsDisplay.setMap(null);
                directionsDisplay.setPanel(null);
            }
        });
    }
}

itemTapped: función predefinida que nos permite trasladar datos de una página a otra en pocos pasos:

itemTapped(event, item) {
   /* Si no hay destino mostramos alerta */
    if (this.destination == "") {
        let alert = Alert.create({
            title: 'Wrong!',
            subTitle: 'Are you going somewhere?!',
            buttons: ['OK']
        });
        this.nav.present(alert);
    } else {
        /* 
        Agregamos 4 variables (item - pulsado -, origin, destination y search - busqueda -) 
        Con un simple push se mostrara la pagina Map a nivel visual
        */
        this.nav.push(Map, {
            item: item,
            origin: this.origin,
            destination: this.destination,
            search: this.search
        })
    }
}

app/pages/map/map.js: Esas 4 variables antes mencionadas son transferidas gracias a navParams, tan solo tengo que aplicar get junto al nombre que le di en home para recuperar sus valores y con ello bastaría para poder emplearlas en Map:

constructor(nav, navParams) {
    this.nav = nav;
    this.selectedItem = navParams.get('item');
    this.origin = navParams.get('origin');
    this.destination = navParams.get('destination');
    this.search = navParams.get('search');
    /* Lanzamos otra vez initMap definida en map.js */
    this.initMap();
}

calculateRoute: función del mismo nombre que la pudisteis ver en home.js pero con algunos matices. Aquí suprimo los marcadores y ruta original que emplea Google Maps Api por un polyline de color verde y mostramos información adicional gracias a jquery.

calculateRoute() {
    let renderer = new google.maps.DirectionsRenderer({
        suppressMarkers: true
        , suppressPolylines: true
    });
    let directions = new google.maps.DirectionsService();
    renderer.setMap(this.map);
    let request = {
        origin: this.origin
        , destination: this.destination
        , travelMode: google.maps.TravelMode[this.selectedItem.mode]
    };

    let polyline = new google.maps.Polyline({
        path: []
        , strokeColor: '#37ab2e'
        , strokeWeight: 5
    });
    
    /* Mostramos la busqueda realizada con jquery */
    $("ion-row.info ion-col.search").html(this.search);

    let distance = null;
    directions.route(request, function (response, status) {

    if (status == google.maps.DirectionsStatus.OK) {
        renderer.setDirections(response);
        let legs = response.routes[0].legs;
        distance = legs[0].distance.text;
        /* Mostramos la distancia que se recorrera con jquery */
        $("ion-row.info ion-col.distance").html(distance);

        for (let i = 0; i < legs.length; i++) {
            let steps = legs[i].steps;
                for (let j = 0; j < steps.length; j++) {
                        let step = steps[j].path;

                        $("ion-list.steps").append("<span>" + steps[j].instructions.toString() + "</span>");

                        for (let k = 0; k < step.length; k++) {
                            polyline.getPath().push(step[k]);
                        }
                    }
                }
            }
        });
        polyline.setMap(this.map);
    }

Ejemplo practico de como nos queda a nivel visual

Supongo que tendré que darle un repaso a todo esto. Entre las faltas de ortografía y los fallos del código acabare por sacarle tiempo y dejar bonito el post. De todas formas como ejemplo puede valer perfectamente para comprender como función Ionic Framework aunque no he comentado nada al respecto de SASS aunque podéis echarle un vistazo a app.score.scss, app.variables.scss y los *.scss de las paginas creadas. Tampoco he mencionado nada al respecto de cómo compilar todo esto para Android/IOS así que lo único que puedo hacer es remitiros a la página web del proyecto para dejaros un poco con la curiosidad:

Ionic Framework

Actualizacion: Este tutorial se realizo con el único objetivo de trastear un poco con Ionic usando Google Maps pero sin API Key para evitar líos. Lo lógico es crear servicios al margen de los controladores e injectarlos pero dio la casualidad que no había mucha información al respecto. Os dejo este tutorial link de Ashteya Biharisingh que explica un poco mejor el tema. Dejo copia de código por si las moscas: