Archivo de la etiqueta: correr app en tel茅fono

Tutorial de Ionic – Crear una aplicaci贸n para guardar nuestros sitios geolocalizados – Parte 4 – Mostrando la direcci贸n a partir de las coordenadas y sacando foto con la c谩mara.

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 el post anterior vimos como poner un marcador al mapa, aprendimos a utilizar los botones FAB y aprendimos a utilizar ventanas modales.

Hoy vamos a continuar desarrollando nuestra app, vamos a crear un peque帽o formulario en el modal donde mediante una llamada a la api de google maps obtendremos y mostraremos la direcci贸n a partir de las coordenadas y permitiremos introducir una descripci贸n y 聽tambi茅n tomar una fotograf铆a del lugar.

Bien, vayamos por partes:

Ahora vamos聽a a帽adir al controlador del modal tres variables nuevas que vamos a necesitar:

  • Una聽variable de tipo string聽 que vamos a llamar address聽donde聽guardaremos la direcci贸n que luego mostraremos en la vista.
  • Una variable de tipo string a la que vamos a llamar description y que contendr谩 la descripci贸n del lugar que introduzcamos desde el formulario.
  • Por 煤ltimo una variable de tipo any que vamos a llamar foto, donde guardaremos una foto del lugar codificada en base 64.

Por lo tanto editamos el controlador de nuestro modal (modal-nuevo-sitio.ts) encima del constructor de la clase despu茅s de la variable coords聽definimos las siguientes variables:

...

export class ModalNuevoSitioPage {

  coords : any = { lat: 0, lng: 0 }
聽聽address: string;
  description: string = '';
  foto: any = '';

  constructor(public navCtrl: NavController, public navParams: NavParams, private viewCtrl : ViewController) {}

...

Para obtener la direcci贸n correspondiente a unas coordenadas Google maps cuenta con el elemento Geodecoder.聽

Vamos a ver brevemente como funciona Google Maps 聽Geodecoder:

Para realizar una petici贸n debemos crear un objeto dela clase Geodecoder y llamar al m茅todo geodecode pasandole las coordenadas y una funci贸n callback donde recibimos los datos en caso de tener 茅xito y el estado de la petici贸n:

var geocoder = new google.maps.Geocoder();

geocoder.geocode({'location': coords} , function (results, status) {
    if (status == google.maps.GeocoderStatus.OK) {
       // en results tenemos lo que nos devuelve la llamada;
    } else {
      // Ha habido alg煤n error
    }
});

Para obtener los resultados tenemos pasarle una funci贸n callback, sin embargo lo que nos interesa en que nos devuelva una promesa que es la mejor manera de gestionar las peticiones as铆ncronas desde el controlador.

Creando nuestras propias promesas

Ya vimos en el capitulo anterior como se tratan las funciones que nos devuelve una promesa utilizando then, pero si queremos que una funci贸n que creemos nosotros devuelva una promesa tenemos que devolver un objeto Promise donde la promesa se crea a partir de una funci贸n聽callback en el que ejecutaremos la sentencia que nos devolver谩 el resultado as铆ncrono y 聽llamaremos a las funciones pasadas como argumento resolve y reject. Si la operaci贸n se a ejecutado correctamente se llama a resolve, y si a ocurrido un error llamamos a reject.

Sabiendo esto para conseguir que geodecode nos devuelva la direcci贸n en una promesa tenemos que utilizar un peque帽o truco que consiste en crear una funci贸n聽que vamos a llamar getAddress聽y que haremos que nos devuelva una promesa con el resultado de la llamada a geodecode, para ello editamos el archivo modal-nuevo-sitio.ts a帽adimos la siguiente funci贸n en el controlador:

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);
            }
        });
    });
}

Como podemos ver la funci贸n retorna una promesa donde se le pasa como par谩metro una funci贸n con resolve y reject, luego dentro de la funci贸n se ejecuta la llamada a geodecoder.geodecode pas谩ndole las coordenadas y la funci贸n callback donde si se ha recibido como status聽google.maps.GeocoderStatus.OK significa que hemos recibido correctamente los datos y entonces ejecutamos resolve, de lo contrario ejecutamos reject.

De esta manera podemos hacer que geodecode nos devuelva una promesa.

Nota: al igual que hicimos en la pagina de inicio, para que聽Typescript no de error por no reconocer la clase google cuando la llamemos desde la funci贸n que vamos a crear, vamos a declarar la variable google justo debajo de los imports con declare聽var聽google:聽any;聽

Ahora en el m茅todo ionViewDidLoad聽que se ejecuta cuando la p谩gina se ha cargado vamos ha hacer una llamada a getAddress聽pas谩ndole las coordenadas que hemos recibido para obtener la direcci贸n y asign谩rsela a this.address:

ionViewDidLoad() {
    console.log('ionViewDidLoad ModalNuevoSitioPage');
    this.coords.lat = this.navParams.get('lat');
    this.coords.lng = this.navParams.get('lng');
   this.getAddress(this.coords).then(results=> {
        this.address = results[0]['formatted_address'];
      }, errStatus => {
          // Aqu铆 ir铆a el c贸digo para manejar el error
      });
  }

Un ejemplo de la estructura de datos que recibimos en聽results聽ser铆a:

{
聽"results": [ {
聽"types": street_address,
聽"formatted_address": "Etorbidea Abandoibarra, 2, 48001 Bilbo, Bizkaia, Espa帽a",
聽"address_components": [ {
聽"long_name": "2",
聽"short_name": "2",
聽"types": street_number
聽}, {
聽"long_name": "Etorbidea Abandoibarra",
聽"short_name": "Etorbidea Abandoibarra",
聽"types": route
聽}, {
聽"long_name": "Bilbo",
聽"short_name": "Bilbo",
聽"types": [ "locality", "political" ]
聽}, {
聽"long_name": "Bizkaia",
聽"short_name": "BI",
聽"types": [ "administrative_area_level_2", "political" ]
聽}, {
聽"long_name": "Euskadi",
聽"short_name": "PV",
聽"types": [ "administrative_area_level_1", "political" ]
聽}, {
聽"long_name": "Espa帽a",
聽"short_name": "ES",
聽"types": [ "country", "political" ]
聽}, {
聽"long_name": "48001",
聽"short_name": "48001",
聽"types": postal_code
聽} ],
 "geometry": {
聽"location": {
聽"lat": 43.26861,
聽"lng": -2.934380000000033
聽},
 "location_type": "ROOFTOP",
 "viewport": {
聽"southwest": {
聽"lat": 43.26726101970851, 
聽"lng": -2.9357289802915147
聽},
 "northeast": {
聽"lat": 43.26995898029151,
聽"lng": -2.933031019708551
聽}
 }
 }
 } ]
}

Lo que nos interesa obtener que es la direcci贸n completa se encuentra en results[0][‘formatted_address’], por lo tanto 聽le asignamos este dato a this.address.

Mostrando las coordenadas y la 聽direcci贸n.

Ahora vamos a a帽adir en la vista un componente ion-card聽 donde mostraremos las coordenadas y la direcci贸n donde nos encontramos, editamos el archivo modal-nuevo-sitio.html聽para que quede de la siguiente manera:

<!--
聽聽Generated template for the ModalNuevoSitio page.

聽聽See http://ionicframework.com/docs/v2/components/#navigation for more info on
聽聽Ionic pages and navigation.
-->
<ion-header>

  <ion-navbar>
    <ion-title>Nuevo sitio</ion-title>
    <ion-buttons start>
      <button ion-button (click)="cerrarModal()">
        <ion-icon name="md-close"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>

</ion-header>

<ion-content padding>
  <ion-card>
    <ion-card-header>
      Localizaci贸n actual
    </ion-card-header>
    <ion-card-content>
      <p><strong>lat:</strong>{{ coords.lat}}<br/>
      <strong>lng:</strong>{{coords.lng }}</p>
      <hr>
      <p>{{ address }}</p>
    </ion-card-content>
  </ion-card>
</ion-content>

Como pod茅is observar hemos creado un componente ion-card que consta a su vez de un elemento ion-card-header donde ponemos como t铆tulo “Localizaci贸n 聽actual”, y en ion-car-content pondremos el contenido que queremos mostrar, en este caso mostramos un elemento <p> donde mostramos el valor de las variables coords.lat y coords.lng que hemos definido en el controlador y que contendr谩n las coordenadas actuales.

Por otro lado mostramos otro elemento <p> con el contenido de la variable address que de momento no contiene nada pero que contendr谩 la direcci贸n que corresponda con las coordenadas que hemos recogido.

Al pulsar en el FAB se abrir谩 el modal mostrando algo similar a esto:

Modal con las coordenadas y a direcci贸n
Modal con las coordenadas y a direcci贸n

Creando el formulario.

Vamos a seguir a帽adiendo elementos a la vista del modal. Adem谩s de las coordenadas y la direcci贸n queremos dar la posibilidad de tomar una foto del lugar y escribir anotaciones.
Para ello vamos a crear dentro del card, debajo de la direcci贸n un peque帽o formulario donde habr谩 un bot贸n para sacar una foto y un campo text-area para 聽escribir una descripci贸n del lugar.

Editamos de nuevo modal-nuevo-sitio.html聽y a帽adimos lo que est谩 marcado de amarillo:

<!--
聽聽Generated template for the ModalNuevoSitio page.

聽聽See http://ionicframework.com/docs/v2/components/#navigation for more info on
聽聽Ionic pages and navigation.
-->
<ion-header>

  <ion-navbar>
    <ion-title>Nuevo sitio</ion-title>
    <ion-buttons start>
      <button ion-button (click)="cerrarModal()">
        <ion-icon name="md-close"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>

</ion-header>

<ion-content padding>
  <ion-card>
    <ion-card-header>
     <strong>Localizaci贸n actual</strong>
    </ion-card-header>
    <ion-card-content>
      <p><strong>lat:</strong>{{ coords.lat}}<br/>
      <strong>lng:</strong>{{coords.lng }}</p>
      <hr>
      <p>{{ address }}</p>
      <hr/>
      <form (ngSubmit)="guardarSitio()">
      <ion-item>
        <img [src]="foto" *ngIf="foto" />
        <button ion-button icon-left full type="button" (tap)="sacarFoto()">
          Foto&nbsp;&nbsp;
          <ion-icon name="camera"></ion-icon>
        </button>
      </ion-item>
      <hr/>
      <ion-item>
        <ion-label>Descripci贸n</ion-label>
        <ion-textarea [(ngModel)]="description" name="description"></ion-textarea>
      </ion-item>
      <button ion-button type="submit" block>Guardar Sitio</button>
    </form>
    </ion-card-content>
    </ion-card>
</ion-content>

Como vemos al formulario le hemos a帽adido (ngSubmit), con esto le indicamos que cuando se ejecute el evento submit del formulario se ejecute la funci贸n guardarSitio definida en el controlador.

Despu茅s hemos a帽adido un elemento ion-item, donde vamos a mostrar la imagen con la fotograf铆a que tomemos:

<img [src]="foto" *ngIf="foto" />

Por un lado con [src] le indicamos que la imagen va a mostrar lo que contenga la variable foto, que hemos definido el el controlador y que al principio estar谩 vac铆a, y por otro lado con *ngIf le indicamos que solo se muestre la imagen si dicha variable foto tiene un valor, es decir聽que solo se mostrar谩 una vez hayamos tomado la fotograf铆a.

Despu茅s tenemos un bot贸n que al pulsar llama a la funci贸n sacarFoto que despu茅s definiremos en el controlador:

<button ion-button icon-left full type="button" (tap)="sacarFoto()">
          Foto&nbsp;&nbsp;
          <ion-icon name="camera"></ion-icon>
 </button>

Con icon-left聽le indicamos al bot贸n que el icono se situar谩 a la izquierda, para mostrar el icono despu茅s del texto “Foto” y un par de espacios en blanco “&nbsp;” insertamos el elemento ion-icon聽con 聽el icono camera.

Despu茅s tenemos un campo de tipo ion-textarea聽donde podremos introducir la descripci贸n del sitio:

<ion-item>
    <ion-label>Descripci贸n</ion-label>
    <ion-textarea [(ngModel)]="description" name="description"></ion-textarea>
</ion-item>

Por 煤ltimo tenemos un bot贸n de tipo submit para enviar el formulario. Recordad que cuando pulsemos en este bot贸n se disparar谩 el evento ngSubmit聽y por lo tanto se ejecutar谩 la funci贸n guardarSitio聽tal y como hemos definido en la etiqueta form.

Utilizando la c谩mara del tel茅fono m贸vil

Ahora vamos a ver como sacar una foto y para que se muestre en la etiqueta img que hemos creado:

Para poder utilizar la c谩mara del m贸vil tenemos que instalar el plugin cordova-plugin-camera con el siguiente comando:

ionic cordova plugin add cordova-plugin-camera --save
npm install --save @ionic-native/camera

Ahora el controlador del modal (modal-nuevo-sitio.ts) tenemos聽聽que importar Camera desde ionic-native聽e inyectarlo en el constructor:

import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';

declare var google:any;
/**
聽* Generated class for the ModalNuevoSitio page.
聽*
聽* See http://ionicframework.com/docs/components/#navigation for more info
聽* on Ionic pages and navigation.
*/

@IonicPage()
@Component({
  selector: 'page-modal-nuevo-sitio',
  templateUrl: 'modal-nuevo-sitio.html',
})
export class ModalNuevoSitioPage {

  coords : any = { lat: 0, lng: 0 }
  address: string;
  description: string = '';
  foto: any = '';

  constructor(public navCtrl: NavController, public navParams: NavParams, private viewCtrl : ViewController, private camera: Camera) {
  }
...

Tambi茅n debemos importar Camera en聽app.module.ts聽y declararlo como provider:

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { Geolocation } from '@ionic-native/geolocation';
import { Camera } from '@ionic-native/camera';

import { MyApp } from './app.component';


@NgModule({
  declarations: [
    MyApp,
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},Geolocation,Camera
  ]
})
export class AppModule {}

Ahora vamos a crear聽el m茅todo sacarFoto en聽en el controlador (modal-nuevo-sitio.ts)聽que se ejecutar谩 al pulsar sobre el bot贸n Foto.

En el archivo modal-nuevo-sitio.ts聽a帽adimos el siguiente c贸digo dentro del controlador:

sacarFoto(){

    let cameraOptions : CameraOptions = {
聽聽聽聽聽聽聽聽quality: 50,
聽聽聽聽聽聽聽聽encodingType: this.camera.EncodingType.JPEG,
聽聽聽聽聽聽聽聽targetWidth: 800,
聽聽聽聽聽聽聽聽targetHeight: 600,
聽聽聽聽聽聽聽聽destinationType: this.camera.DestinationType.DATA_URL,
聽聽聽聽聽聽聽聽sourceType: this.camera.PictureSourceType.CAMERA,
聽聽聽聽聽聽聽聽correctOrientation: true
    }


    this.camera.getPicture(cameraOptions).then((imageData) => {
      // imageData is a base64 encoded string
        this.foto = "data:image/jpeg;base64," + imageData;
    }, (err) => {
        console.log(err);
    });
  }

Para sacar una foto utilizamos el m茅todo getPicture聽pas谩ndole un array de opciones. En las opciones definimos las caracter铆sticas que va a tener la imagen:

  • encodingType:聽Selecciona la codificaci贸n del archivo de imagen devuelto, puede ser JPEG o PNG.
  • targetWidth:聽Anchura de la foto.
  • targetHeight:聽Altura de la foto.
  • destinationType: Define el formato del valor devuelto, puede ser :
    • DATA_URL devuelve la imagen como una cadena codificada en base64.
    • FILE_URI: Crea un archivo con la imagen y devuelve la ruta al archivo.
    • NATIVE_URI: devuelve la ruta nativa al archivo (assets-library:// en iOS o content:// en Android).
  • sourceType: Indica el origen de la foto, puede ser:
    • CAMERA (por defecto).
    • PHOTOLIBRARY
    • SAVEDPHOTOALBUM
  • correctOrientation: Gira la imagen para corregir la orientaci贸n del dispositivo durante la captura.

Puedes visitar el siguiente enlace para conocer聽todas las opciones posibles y saber m谩s sobre el plugin Camera:

https://ionicframework.com/docs/native/camera/

En this.foto聽guardamos la imagen聽codificada en formato base64 que recibimos de la c谩mara, para poder mostrarla como parte de la url en el par谩metro src de la imagen tenemos que a帽adirle聽“data:image/jpeg;base64,”聽por delante.

Por si alguno se ha perdido muestro el contenido completo de como tiene que quedar en estos momentos el archivo聽modal-nuevo-sitio.ts:

import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams,  ViewController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';

declare var google:any;

/*
聽聽Generated class for the ModalNuevoSitio page.

聽聽See http://ionicframework.com/docs/v2/components/#navigation for more info on
聽聽Ionic pages and navigation.
*/
@IonicPage()
@Component({
  selector: 'page-modal-nuevo-sitio',
  templateUrl: 'modal-nuevo-sitio.html'
})
export class ModalNuevoSitioPage {

  coords : any = { lat: 0, lng: 0 }
  address: string;
  description: string = '';
  foto: any = '';

  constructor(public navCtrl: NavController, public navParams: NavParams, private viewCtrl : ViewController, private camera: Camera ) {}

  ionViewDidLoad() {
    console.log('ionViewDidLoad ModalNuevoSitioPage');
    this.coords.lat = this.navParams.get('lat');
    this.coords.lng = this.navParams.get('lng');
    this.getAddress(this.coords).then(results=> {
        this.address = results[0]['formatted_address'];
      }, errStatus => {
          // Aqu铆 ir铆a el c贸digo para manejar el error
      });
  }

  cerrarModal(){
    this.viewCtrl.dismiss();
  }

  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);
            }
        });
    });
  }

  sacarFoto(){

    let cameraOptions : CameraOptions = {
        quality: 50,
        encodingType: this.camera.EncodingType.JPEG,
        targetWidth: 800,
        targetHeight: 600,
        destinationType: this.camera.DestinationType.DATA_URL,
        sourceType: this.camera.PictureSourceType.CAMERA,
        correctOrientation: true
    }


    this.camera.getPicture(cameraOptions).then((imageData) => {
      // imageData is a base64 encoded string
        this.foto = "data:image/jpeg;base64," + imageData;
    }, (err) => {
        console.log(err);
    });
  }

}

A帽adiendo plataformas

Bien, hasta ahora hemos estado probando nuestra aplicaci贸n en el navegador, sin embargo no podemos probar el plugin camera desde el navegador. Ha llegado la hora de probar nuestra aplicaci贸n en un dispositivo m贸vil.

Lo primero que tenemos que hacer es a帽adir la plataforma en la que queremos probar nuestra aplicaci贸n.

Para a帽adir una plataforma utilizamos el siguiente comando desde consola, recordad que debemos esta siempre dentro de la ios
ionic platform add wp

Por lo tanto si queremos probar nuestra app en android escribiremos:

ionic cordova platform add android

Esto nos crear谩 una carpeta llamada platforms si no estaba creada, y a帽adir谩 una carpeta android con todo el c贸digo necesario para poder generar un archivo apk instalable.

En Mac y Linux tal vez os pida que escrib谩is聽sudo por delante.

Ejecutando nuestra app en el dispositivo m贸vil

Una vez tenemos a帽adida la plataforma si enchufamos nuestro m贸vil con un cable usb a nuestro pc podemos ejecutar la app directamente en el dispositivo con el siguiente comando:

ionic cordova run android

Si no dispon茅is de un dispositivo tambi茅n pod茅is emular la aplicaci贸n utilizando ionic emulate:

ionic cordova emulate android

En ios tambi茅n puedes entrar en la carpeta 聽platforms/ios y abrir el archivo con extensi贸n .xcodeproj desde聽Xcode y ejecutarlo o emularlo聽desde Xcode.

El el m贸vil la app con el modal abierto se ver铆a algo as铆:

Nuestra App funcionando en un dispositivo m贸vil.
Nuestra App funcionando en un dispositivo m贸vil.

Como pod茅is observar mi gato se ha prestado voluntariamente para posar en la foto ;-P

Dependiendo del sistema operativo que utilic茅is en vuestro pc y del dispositivo puede que teng谩is alg煤n 聽problema para que os reconozca el m贸vil, yo no tengo tiempo de investigar cada caso pero googleando seguro que dais con la soluci贸n, 聽os animo a que dej茅is en los comentarios si ten茅is alg煤n problema y que qui茅n encuentre la soluci贸n lo ponga para ayudarnos unos a otros.

Por hoy lo dejamos aqu铆, en el siguiente capitulo veremos como guardar nuestros sitios en una base de datos local en nuestro dispositivo.

 

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