7 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (V)

Part V: Versió amb Google Maps

Aquest article ens mostrarà com utilitzar Google Maps per escollir el lloc en l'aplicació per saber l'hora de sortida i posta de sol.

Fins ara hem vist:

Ara utilitzarem Google Maps per poder escollir el lloc. Tot i que hi ha molta informació sobre com utilitzar Google Maps, la informació sol estar incompleta o no acabar-se d'adaptar a les nostres necessitats concretes.

En general, una de les meves primeres fonts d'informació són els articles de Josh Morony. Són acurats i força actualitzats. Tota una referència. I just podem trobar-hi el que busquem: "com fer una pàgina de selecció de lloc amb Google Maps i Ionic". Seguirem el tutorial i fem els canvis necessaris per afegir la funcionalitat a la nostra aplicació.

Afegim el mòdul de xarxa, que permetrà verificar quan tenim connexió de dades, i les definicions de tipus per Google Maps:
 $ ionic cordova plugin add cordova-plugin-network-information
 $ npm install --save @ionic-native/network
 $ npm install @types/google-maps --save

Seguint el tutorial, afegim la pàgina nova (LocationSelect) i els nous proveïdors a app.module.ts:
 ...
 import { CalendarModule } from "ion2-calendar";
 import { LocationSelect } from '../pages/location-select/location-select';
 import { Connectivity } from '../providers/connectivity-service';
 import { GoogleMapsService } from '../providers/google-maps-service';
 import { Network } from '@ionic-native/network';
 ...
  declarations: [
    ...
    HomePage,
    LocationSelect,
    TabsPage
  ],
  ...
  entryComponents: [
    ...
    HomePage,
    LocationSelect,
    TabsPage
  ],
  providers: [
    ...
    Geolocation,
    Connectivity,
    GoogleMapsService,
    Network
  ]
 ...

Ara afegim les pàgines dels proveïdors de servei, segons el tutorial. En primer lloc, providers/connectivity-service.ts:
 import { Injectable } from '@angular/core';
 import { Network } from '@ionic-native/network';
 import { Platform } from 'ionic-angular';

 @Injectable()
 export class Connectivity {
  onDevice: boolean;

  constructor(public platform: Platform, public network: Network) {
    this.onDevice = this.platform.is('cordova');
  }

  isOnline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type != 'none';
    } else {
      return navigator.onLine; 
    }
  }

  isOffline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type == 'none';
    } else {
      return !navigator.onLine;   
    }
  }

  watchOnline(): any { return this.network.onConnect(); }
  watchOffline(): any { return this.network.onDisconnect(); }
 }

I tot seguit, providers/google-maps-service.ts:
 import { Injectable } from '@angular/core';
 import { Connectivity } from './connectivity-service';
 import { Geolocation } from '@ionic-native/geolocation';

 @Injectable()
 export class GoogleMapsService {
  mapElement: any;
  pleaseConnect: any;
  map: any;
  mapInitialised: boolean = false;
  mapLoaded: any;
  mapLoadedObserver: any;
  currentMarker: any;
  apiKey: string; //  = "YOUR_API_KEY";
  lat: number = null;
  lon: number = null;

  constructor( public connectivityService: Connectivity, 
               public geolocation: Geolocation ) 
  {
  }

  init( mapElement: any, pleaseConnect: any, 
        lat: number, lon: number 
       ): Promise 
  {

    this.mapElement = mapElement;
    this.pleaseConnect = pleaseConnect;
    this.lat = lat;
    this.lon = lon;

    return this.loadGoogleMaps();
  }

  loadGoogleMaps(): Promise {
    return new Promise((resolve) => {
      if(typeof google=="undefined" || typeof google.maps=="undefined") {
        console.log("Google maps JavaScript needs to be loaded.");
        this.disableMap();

        if(this.connectivityService.isOnline()){
          window['mapInit'] = () => {
            this.initMap().then(() => {
              resolve(true);
            });

            this.enableMap();
          }

          let script = document.createElement("script");
          script.id = "googleMaps";

          if(this.apiKey){
            script.src = 'http://maps.google.com/maps/api/js?key=' + 
                       this.apiKey + '&callback=mapInit&libraries=places';
          } else {
            script.src = 'http://maps.google.com/maps/api/js' + 
                                     '?callback=mapInit&libraries=places';       
          }

          document.body.appendChild(script);  
        } 
      } else {
        if(this.connectivityService.isOnline()){
          this.initMap();
          this.enableMap();
        } else {
          this.disableMap();
        }

 resolve(true);
      }

      this.addConnectivityListeners();
    });
  }

  initMap(): Promise {
    this.mapInitialised = true;

    return new Promise((resolve) => {
      if(this.lat && this.lon) {
        this.initMapPos(this.lat, this.lon);
        resolve(true);

      } else {
        this.geolocation.getCurrentPosition().then((pos) => {
          this.initMapPos(pos.coords.latitude, pos.coords.longitude);
          resolve(true);

        }).catch((error) => {
          console.log('Error getting location'+JSON.stringify(error));
          return Promise.reject("Unable to get location");
        });
      }
    });
  }

  initMapPos(lat: number, lon: number) {
    let latLng = new google.maps.LatLng(lat, lon);
    let mapOptions = {
      center: latLng,
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    }

    this.map = new google.maps.Map(this.mapElement, mapOptions);
  }

  disableMap(): void {
    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "block";
    }
  }

  enableMap(): void {
    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "none";
    }
  }

  addConnectivityListeners(): void {
    this.connectivityService.watchOnline().subscribe(() => {
      setTimeout(() => {
        if(typeof google=="undefined" || typeof google.maps=="undefined") {
          this.loadGoogleMaps();
        } else {
          if(!this.mapInitialised) {
            this.initMap();
          }
          this.enableMap();
        }
      }, 2000);
    });

    this.connectivityService.watchOffline().subscribe(() => {
      this.disableMap();
    });
  }
 }

En aquest fitxer hem fet alguns canvis:
  • [6] Hem canviat el nom de la classe a GoogleMapsService.
  • [14] Hem eliminat l'apiKey. Se n'hauria de generar una per poder-la utilitzar en l'aplicació però, per fer proves, podem treballar sense.
  • [15-16] Declarem les variables per poder inicialitzar el mapa a partir d'una posició concreta, enlloc de la posició del nostre dispositiu.
  • [24-31] Afegim paràmetres per la posició desitjada a la funció init i els traslladem als corresponents camps de la classe. 
  • [59] Hem afegit el paràmetre que faltava a la petició: '&libraries=places'.
  • [79-109] En la funció initMap, utilitzem la posició obtinguda durant la inicialització, o si està buida, usem el servei de geolocalització. També hem separat el codi d'inicialització de Google Maps en una nova funció initMapPos per no repetir codi.

Pel que fa a la pàgina LocationSelect, el fitxer d'estils location-select/location-select.scss no s'ha modificat, en el fitxer location-select/location-select.html només s'han traduït els textos al català, i el fitxer controlador location-select/location-select.ts s'ha modificat per poder passar la localització actual al servei GoogleMapsService:
 ...
 import { NavParams } from 'ionic-angular';
 import { GoogleMapsService } from '../../providers/google-maps-service';
 ...
 export class LocationSelect {
    ...
    location: any; 
    lat: number = 42.582104;
    lon: number = 1.0008419;
  
    constructor( public navCtrl: NavController, 
   public params: NavParams,
   public maps: GoogleMapsService, 
   public platform: Platform, 
   public geolocation: Geolocation, 
   public viewCtrl: ViewController) 
    {
      this.searchDisabled = true;
      this.saveDisabled = true;

      this.lat = params.get('lat');
      this.lon = params.get('lon');
    }

    ionViewDidLoad(): void {
      this.maps.init( this.mapElement.nativeElement, 
                      this.pleaseConnect.nativeElement, 
                      this.lat, this.lon).then(() => {
        this.autocompleteService = 
               new google.maps.places.AutocompleteService();
 this.placesService = 
               new google.maps.places.PlacesService(this.maps.map);
 this.searchDisabled = false;
      }); 
    }
    ...

On:
  • [2] S'ha afegit la classe NavParams per poder passar paràmetres a la pàgina.
  • [8-9] S'han afegit membres a la classe per a guardar la posició inicial.
  • [12] S'utilitza el controlador NavParams en la classe, per a rebre els paràmetres.
  • [21-22] Es llegeixen i es guarden els paràmetres relacionats amb la posició.
  • [28] Els paràmetres de la posició s'utilitzen en la crida init del servei GoogleMapsService.

I finalment només ens queda modificar la pàgina HomePage per afegir la funcionalitat. En el controlador home.ts:
 ...
 import { LocationSelect } from '../location-select/location-select';
 ...
 export class HomePage {
  ...
  launchLocationPage() {
    let modal = this.modalCtrl.create( LocationSelect,
                                       {lat: this.lat, lon: this.lon});
    modal.onDidDismiss((location) => {
      if(location) {
        console.log("Nou lloc: ", location);
        
        this.lat = location.lat;
        this.lon = location.lng;
        this.posMsg = location.name;
        this.posError = false;
        this.getSunriseSunsetFromApi();
      }
    });

    modal.present(); 
  }
 }

Hem afegit:
  • [2] La importació de la nova pàgina per a seleccionar el lloc.
  • [6-22] La funció launchLocationPage per mostrar la finestra de selecció de lloc. Observem com passem els paràmetres de la posició actual a la nova pàgina [8], i com s'actualitzen les dades i missatges en cas d'haver escollit un lloc, en el retorn de la pàgina [13-17].

En la part de visualització home.html, simplement activem la crida a la funció anterior:
  ...
  <ion-item color="light" *ngIf="posError" (click)="launchLocationPage()">
    <h2 text-wrap>No s'ha pogut obtenir la posició actual.</h2>
    <p text-wrap>Verifica el GPS i/o els permisos.</p>
    <ion-icon name="warning" color="danger" item-left></ion-icon>
  </ion-item>
  <ion-item (click)="launchLocationPage()">
    <ion-label>Latitud</ion-label>
    <ion-note item-right>{{ lat | number: "1.1-7" }}</ion-note>
  </ion-item>
  <ion-item (click)="launchLocationPage()">
    <ion-label>Longitud</ion-label>
    <ion-note item-right>{{ lon | number: "1.1-7" }}</ion-note>
  </ion-item>
  ...

I ja ho tenim tot a punt ! Provem de seguida la nova funcionalitat:

Podem veure com el mapa s'obre en una nova finestra superposada, si té prou espai, i com permet escollir un lloc, auto-completant les opcions. En escollir-ne una, el mapa es posició en ella i, en guardar-la, s'utilitza per calcular les hores de sortida/posta del sol en la nostra aplicació:

Podem veure en la consola l'objecte retornat pel servei de Google Maps. Les seves propietats s'utilitzen per actualitzar les dades en la pàgina principal.
Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.4 (amb Google Maps).

Ara ja tenim una aplicació funcional, però encara la podem completar en alguns aspectes. Per exemple, podríem afegir-hi traducció a diversos idiomes; també es poden fer alguns canvis estètics, amb icones personalitzades i tipus de lletra diferents; i finalment, completar el projecte donant les dades de contacte i afegint una pàgina per donar crèdit als projectes i persones que l'han fet possible.

Seguiu l'evolució de l'aplicació:

Cap comentari:

Publica un comentari a l'entrada