[Ionic 2從入門到精通] 6.5 接入Google Maps和地理定位

weixin_34019929發表於2018-05-17

Google Maps和移動應用是絕配。Google Maps API本身就是一黑科技來的,當你和裝置組合起來的時候以為這移動性,不是靜止的,他開啟了一扇新世界的大門。現今市面上有很多使用Google Maps武裝起來的牛逼應用。
即使地圖功能不是你的應用的核心功能,他們經常作為補充功能出現(例如在地圖上顯示一個商業地址)。
本課中我們將在Location頁上實現Google Maps。我們實際上要做的事情如下:

  • 展示一個地圖
  • 允許使用者設定在地圖上當前的位置
  • 顯示上一次在地圖上標記的位置
  • 啟用導航給使用者回到預設位置

在應用中設定Google Maps SDK以及使用他是非常簡單的事情。你只需要簡單的載入Google Maps SDK指令碼,然後與其API互動。事情變得稍微有點複雜,因為我們有一個主要的問題:
如果使用者沒有聯網咋辦?
如果使用者因為沒有聯網而用不了地圖是不合理的,但是如何友好的處理這個問題呢?我們不希望報錯和調處應用(因為Google Maps SDK沒有載入)或者甚至造成地圖沒法工作,我們需要考慮以下幾點:

  • 如果使用者沒有網路連線怎麼辦?
  • 如果使用者在開始沒有聯網但是後面又有了?
  • 如果用話開始有聯網但是後面又沒有了?

為處理上以上的情景,我們的解決方案需要實現如下幾點:

  • 不直接載入Google Maps SDK,等待到有網路連線再載入
  • 在網路斷開的時候,禁用Google Maps功能
  • 網路再次連上的時候,啟用Google Maps功能

為了讓程式碼更清晰,我們將抽象出大量的功能到早先生成的Google Maps提供者中。這樣在別的應用中就能很簡單的重用這些程式碼了。
注意:我們這節課中將會使用Google Maps JavaScript SDK,但是你需要知道你也可以通過Cordova外掛使用它們的本機SDK:https://github.com/mapsplugin/cordova-plugin-googlemaps
這節課會變得很大,所以我們還是儘快開始吧。我們先從實現一個Connectivity服務開始,也就是我們早先生成的另一個提供者。

Connectivity服務

這將是一個用來檢查網路連線的快速簡單的服務。如果用的是真機的話,我們可以使用之前安裝的network information外掛(這個更準確),但是如果應用是通過普通瀏覽器執行的話,我們使用onLine屬性來檢查聯網(沒外掛那麼準確)。
> 修改 src/providers/connectivity.ts 為如下:

import { Injectable } from '@angular/core';
import { Network } from 'ionic-native';
import { Platform } from 'ionic-angular';

declare var Connection;

@Injectable()
export class Connectivity {

    onDevice: boolean;

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

    isOnline(): boolean {
        if(this.onDevice && Network.connection){
            return Network.connection !== Connection.NONE;
        } else {
            return navigator.onLine;
        }
    }

    isOffline(): boolean {
        if(this.onDevice && Network.connection){
            return Network.connection === Connection.NONE;
        } else {
            return !navigator.onLine;
        }
    }
}

這個服務直白明瞭。我們建立了一個onDevice便用,然後通過Platform來檢查應用是否執行在真機上。然後我們使用onDevice變數來檢查我們是否要檢查navigator.connection.type來確認使用哪種網路資訊外掛,或者只是檢查navigator.onLine屬性。
我們定義了兩個函式isOnlineisOffline,這樣我們在任何匯入了此服務的類裡都可以呼叫這兩個方法。技術層面上,你只需要定義其中一個方法就可以了(如果isOnline返回false的話我們顯然就已經知道是離線狀態),但是我覺得用兩個其實也蠻好的。
這就是這個服務的全部,我們還是接著高Google Maps 服務吧。

Google Maps 服務

這個服務將持有我們們地圖功能的大部分邏輯。這是一個很大的服務所以我們先來建立一點骨架然後功能一個一個的去實現。
> 修改 src/providers/google-maps.ts 為如下:

import { Injectable } from '@angular/core';
import { Connectivity } from './connectivity';
import { Geolocation } from 'ionic-native';

declare var google;

@Injectable()
export class GoogleMaps {
    mapElement: any;
    pleaseConnect: any;
    map: any;
    mapInitialised: boolean = false;
    mapLoaded: any;
    mapLoadedObserver: any;
    currentMarker: any;
    apiKey: string;

    constructor(public connectivityService: Connectivity) {
    }
    init(mapElement: any, pleaseConnect: any): Promise<any> {
    }
    loadGoogleMaps(): Promise<any> {
    }
    initMap(): Promise<any> {
    }
    disableMap(): void {
    }
    enableMap(): void {
    }
    addConnectivityListeners(): void {
    }
    changeMarker(lat: number, lng: number): void {
    }
}

注意,我們把剛製作的Connectivity服務匯入進來了,然後作為服務注入到了構造器(譯者:原文可能有誤,說是added it as a provider in the decorator)。我們也從Ionic Native中匯入了Geolocation外掛。我們也在匯入語句後面新增了declare var google -- 這樣TypeScript編譯器就不會大逃到我們了。由於我們動態載入Google Maps SDK,編譯器不知道google是什麼,在我們不宣告(declare)這個變數的情況下,會想我們丟擲錯誤。
你應該也注意到了有些函式返回Promise。這是因為我們想追蹤地圖完成載入的時機,所有這些函式組成一條鏈(一個接一個的呼叫),所以實際上我們可以菊花鏈這些promise到最初的那個,也就是在最初的那個中我們可以設定一個處理器來處理地圖載入完成的之後的邏輯。
最後,我們建立了很多函式,我們一個一個實現並解釋。
> 修改 init 函式如下:

init(mapElement: any, pleaseConnect: any): Promise<any> {
    this.mapElement = mapElement;
    this.pleaseConnect = pleaseConnect;
    return this.loadGoogleMaps();
}

我們可以在匯入這個服務的地方隨時呼叫init函式來觸發地圖的載入流程。我們簡單的返回loadGoogleMaps函式,這樣將會執行這個方法並返回他的結果(也就是一個Promise)。
由於我們會從location.ts呼叫這個函式,我們就可以傳入早先用@ViewChild獲取的mappleaseConnect元素。我們這裡接受他們作為引數,作為成員變數這樣我們可以在類裡面任何地方訪問到他們。
現在,我們來實現loadGoogleMaps函式。
> 修改 loadGoogleMaps 函式如下:

loadGoogleMaps(): Promise<any> {
    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';
                } else {
                    script.src = 'http://maps.google.com/maps/api/js?callback=mapInit';
                }
                document.body.appendChild(script);
            }
        }
        else {
            if(this.connectivityService.isOnline()){
                this.initMap();
                this.enableMap();
            }
            else {
                this.disableMap();
            }
        }
        this.addConnectivityListeners();
    });
}

這個函式看起來蠻複雜的,但實際上很直白。首先我們通過檢查googlegoogle.maps是否可用來檢查Google Maps是否載入,因為如果這兩個變數可用的話就意味這SDK已經載入好了。
如果SDK沒有載入完成的話,我們將觸發載入流程。由於SDK還沒有載入完成,我們首先呼叫了disableMap函式,這個函式告訴使用者地圖目前不可用。然後我們通過connectivity服務來檢查使用者是否線上,如果線上的話我們通過給應用新增script元素來注入Google Maps SDK。注意,URL加入了一個&callback=mapInit。這孕育我們在應用完成Google Maps SDK的載入後觸發一個函式,在當前應用中,我們在完成載入後呼叫的是initMapenableMap函式(馬上就實現)。注意,我們這是了另一個Promise這樣一來我們可以等到initMap完成之後再返回。
如果SDK已經緊挨在完成,那麼我們檢查使用者是否線上。如果線上的話,我們初始化並啟用地圖,如果不線上的話那麼禁用地圖。
最後一行呼叫的函式addConnectivityListener函式稍後實現。這個函式會監聽上線和離線時間,這樣我們是到何時啟用和禁用地圖,當使用者開啟應用初始化的時候是離線狀態想要載入SDK的時候也一樣。
接下來是另一個函式。
> 修改 initMap 函式如下:

initMap(): Promise<any> {

    this.mapInitialised = true;

    return new Promise((resolve) => {
        Geolocation.getCurrentPosition().then((position) => {
            let latLng = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);

            let mapOptions = {
                center: latLng,
                zoom: 15,
                mapTypeId: google.maps.MapTypeId.ROADMAP
            }

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

        });
    });
}

現在Google Maps SDK載入完成了,這個函式用於使用SDK設定一個新地圖。我們想以使用者當前位置來居中顯示地圖,我們我先呼叫了Geolocation外掛的getCurrentPosition函式。一旦Promise返回解析完成,他傳入的將是一個position物件,這個物件包含了使用者當前的latitude和longitude。我們通過這些值,隨同其他一些設定(縮放級別和地圖型別)來建立一個新的地圖例項。
這個地圖將會被建立到傳入的元素內(#map)。所以,這段程式碼執行後,Google Maps將會被新增到我們的Location頁模板上。
此刻,地圖已經準備好進行互動了,所以我們解析了promise鏈中的最後的promise,他將觸發解析所有的promise,此時我們知道地圖已經準備好了。
雖然我們已經載入完地圖了,但是還需要建立一些函式。
> 修改 disableMap 和 enableMap函式如下:

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

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

我們這裡做的是展示和隱藏Google Maps區域上的覆蓋層,這樣在使用者沒有連線到網際網路的時候就不能使用地圖,以及顯示一個資訊“Please connect to the Internet...”。以上程式碼用在在使用者獲得和失去網路連線的時候展示和隱藏一個資訊元素。
我們需要給這個覆蓋層元素進行自定義樣式。
> 修改 src/pages/location/location.scss 為如下:

page-location {

    #please-connect {
        position: absolute;
        background-color: #000;
        opacity: 0.5;
        width: 100%;
        height: 100%;
        z-index: 1;
    } 

    #please-connect p {
        color: #fff;
        font-weight: bold;
        text-align: center;
        position: relative;
        font-size: 1.6em;
        top: 30%;
    }
}

現在,當使用者沒有聯網的時候,會看到這樣的螢幕:

12084159-6c324b7f9867ee78.jpg
5.6.1.jpg

在達到這點之前我們還需要做一些事情。接下來我們來到了addConnectivityListeners函式。
> 修改 src/providers/google-maps.ts 的 addConnectivityListeners 函式如下:

addConnectivityListeners(): void {
    document.addEventListener('online', () => {
        console.log("online");
        setTimeout(() => {
            if(typeof google == "undefined" || typeof google.maps == "undefined"){
                this.loadGoogleMaps();
            }else {
                if(!this.mapInitialised){
                    this.initMap();
                }
                this.enableMap();
            }
        }, 2000);
    }, false);

    document.addEventListener('offline', () => {
        console.log("offline");
        this.disableMap();
    }, false);
}

如我所述,這個函式負責處理使用者的聯網狀態在離線和上線之間的切換。我們在這裡監聽了‘online’和‘offline’事件,他們會在使用者上線和離線的時候觸發。
當使用者上線的時候,我們檢查Google Maps是否已經載入好了,如果沒有的話就載入他。否則,檢查地圖是否初始化沒有的話初始化他,有的話啟用地圖。注意,我們在這裡有用到一個setTimeout,這些程式碼在使用者上線2秒後執行一次 -- 之前即刻觸發會遇到問題,所以我給連線一點點穩定下來的時間。
當使用者離線的時候,我們禁用了地圖。最後一個需要實現的函式是changeMaker函式。
> 修改 changeMaker函式如下:

changeMarker(lat: number, lng: number): void {
    let latLng = new google.maps.LatLng(lat, lng);
    let marker = new google.maps.Marker({
        map: this.map,
        animation: google.maps.Animation.DROP,
        position: latLng
    });

    if(this.currentMarker){
        this.currentMarker.setMap(null);
    }

    this.currentMarker = marker;
}

所有其他看書都很通用可以在其他任何要用到Google Maps的應用中直接使用,但是這個函式是這個應用才能使用的。在addMarker函式之上,這個函式會移除之前的marker新增一個新的marker。我們只希望使用者設定一個露營地點。
我們只需要把需要新增marker的地方的latitude和longitude傳入進去建立一個marker就可以了。如果有已經存在的marker的話我們就先移除他,然後新增剛建立的marker。
現在我們已經完成了Google Maps服務的設定,我們只要用它他就可以了!

實現Google Maps

我們已經做了大量的工作讓地圖正常工作,但是我們還一點點的路要走。現在我們要修改Location類定義來使用Google Maps服務。我們已經匯入了這個服務,也將他新增到了providers陣列以及給他建立了引用,這樣我們就可以開始使用他了。
> 修改 src/pages/location/location.ts 的 ionViewDidLoad 函式:

ionViewDidLoad(): void {
    this.maps.init(this.mapElement.nativeElement,this.pleaseConnect.nativeElement).then(() => {
        //this.maps.changeMarker(this.latitude, this.longitude);
    });
}

首先,我們通過呼叫this.maps.init方法來觸發地圖的載入,這個函式將會返回一個promise。這個promise在地圖完成載入的時候解析,當他解析完成的時候我們呼叫changeMarker函式。
現在是不會正常工作的因為latitude和longitude現在是undefined,所以我把它註釋掉了。稍後,我們將將在儲存在儲存中的latitude和longitude。
這時候,地圖應該載入完成了,且在螢幕上可見了,但是現在顯示應該是錯誤的。首先,我們需要在.scss檔案中新增一些樣式來使他顯示正常。
> 修改 location.scss 為如下:

page-location {
    #please-connect {
        position: absolute;
        background-color: #000;
        opacity: 0.5;
        width: 100%;
        height: 100%;
        z-index: 1;
    } 

    #please-connect p {
        color: #fff;
        font-weight: bold;
        text-align: center;
        position: relative;
        font-size: 1.6em;
        top: 30%;
    } 

    .scroll {
        height: 100%;
    } 

    #map {
        width: 100%;
        height: 100%;
    }
}

接下來我們實現setLocation函式, 這個函式用於將使用者的露營設定為當前地點。
> 修改 src/pages/location/location.ts 的 setLocation 函式如下:

setLocation(): void {
    Geolocation.getCurrentPosition().then((position) => {
        this.latitude = position.coords.latitude;
        this.longitude = position.coords.longitude;

        this.maps.changeMarker(position.coords.latitude,   position.coords.longitude);
        let data = {
            latitude: this.latitude,
            longitude: this.longitude
        };

        //this.dataService.setLocation(data);
        let alert = this.alertCtrl.create({
            title: 'Location set!',
            subTitle: 'You can now find your way back to your camp site from  anywhere by clicking the button in the top right corner.',
            buttons: [{text: 'Ok'}]
        });

        alert.present();
    });
}

注意:再一次,我們註釋掉了還未實現的資料服務。
在這裡,我們用到了Geolocation來獲取使用者當前位置。一旦得到這些資訊,我們將this.latitudethis.longitude改為使用者當前位置,我們也用這個位置呼叫了changeMarker函式。我們也希望應用可以記住當前這個位置,所以我們為這個位置建立了一個物件傳給資料服務去儲存(記住,我們們還未實現此功能)。
一旦這些流程都完成了,我們出發一個警告框告訴使用者位置設定成功。現在我們還剩下唯一的一個函式需要去定義。
> 修改 src/pages/location/location.ts 的 takeMeHome 函式如下:

takeMeHome(): void {
    if(!this.latitude || !this.longitude){
        let alert = this.alertCtrl.create({
            title: 'Nowhere to go!',
            subTitle: 'You need to set your camp location first. For now, want tolaunch Maps to find your own way home?',
            buttons: ['Ok']
        });
        alert.present();
    }
    else {
        let destination = this.latitude + ',' + this.longitude;
        if(this.platform.is('ios')){
            window.open('maps://?q=' + destination, '_system');
        } else {
            let label = encodeURI('My Campsite');
            window.open('geo:0,0?q=' + destination + '(' + label + ')','_system');
        }
    }
}

這個函式的作用是給使用者展示他當前的位置和營地的位置。
首先,我們檢查了this.latitudethis.longitude是否設定好了,如果沒有的話我們會彈出警告框提醒他們需要先設定他們的地點。
如果已經設定好了的話,我們通過Google Maps和Apple Maps URL配置來啟動應用,並且向他們提供座標資訊。如果我們執行在iOS上的話,那麼我們會通過maps://配置來啟動Apple Maps,如果我們是使用的Android應用的話,我們會通過geo:配置來啟動Google Maps。現在,當使用者觸發此函式的時候,將會彈出一個地圖給使用者指示如何回到露營地點。
完成了!我們完成了我們的地圖功能了。使用者現在應該可以看到地圖了,設定好他們的地址,觸發回營方法。我們還有一個很重要的事情需要去處理,也就是使用者回到應用的時候需要獲取之前的儲存的資料。下節課中,我們馬上就處理這個事情,同時還有儲存表單資料。

相關文章