Creating an Advanced Google Maps Component in Ionic 2



·

We all know how useful maps can be in a mobile application, and Google Maps is a great option to do just that. Of course, you can use the native Google Maps SDK through the use of a Cordova plugin, but I’m a fan of using the Google Maps JavaScript SDK.

I’ve already covered how to do a basic Google Maps implementation in Ionic 2, but when using the JavaScript SDK it’s important to consider:

What if the user does not have an Internet connection?

It’s not unreasonable to make the maps unavailable if the user does not have an Internet connection, but how do we handle that gracefully? We don’t want an error occurring and breaking the application (because the Google Maps SDK hasn’t been loaded) or otherwise causing the maps not to work, so we need to consider the following:

  • What if the user does not have an Internet connection?
  • What if the user doesn’t have an Internet connection initially but does later?
  • What if the user does have an Internet connection initially but doesn’t later?

To handle all of these scenarios, the solution we want to implement will:

  • Wait until a connection is available before loading the Google Maps SDK, rather than straight away
  • If the connection becomes unavailable, disable the Google Maps functionality
  • If the connection becomes available again, enable the Google Maps functionality again

I rely on this functionality a lot and have already implemented it in Ionic 1 and Sencha Touch, so now I’m going to cover how to set up the same functionality in Ionic 2.

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts and the differences to Ionic 1. You must also already have Ionic 2 installed on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts.

1. Generating a New Project

Let’s start off by simply generating a new Ionic 2 project by running the following command:

ionic start ionic2-advanced-maps blank --v2

We will eventually be making use of a provider for this application called ConnectivityService. This will handle detecting whether or not a user has an Internet connection available. We will go through the implementation later, for now we will just generate it.

Run the following command to generate the provider:

ionic g provider ConnectivityService

In order to use this provider throughout the application we will need to add it to the app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ConnectivityService } from '../providers/connectivity-service';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [ConnectivityService]
})
export class AppModule {}

2. Create the Google Maps Page

First, we are going to work on the home page which we will use to hold our Google Map. We will get a basic implementation of the class set up first, and then build on it.

Modify home.ts to reflect the following:

import { Component, ElementRef, ViewChild } from '@angular/core';
import { NavController } from 'ionic-angular';
import { ConnectivityService } from '../../providers/connectivity-service';
import { Geolocation } from 'ionic-native';

declare var google;

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

  @ViewChild('map') mapElement: ElementRef;

  map: any;
  mapInitialised: boolean = false;
  apiKey: any;

  constructor(public navCtrl: NavController, public connectivityService: ConnectivityService) {
    this.loadGoogleMaps();
  }
}

We’ve set up a few member variables here which we will make use of later, the only one you need to specifically worry about is the apiKey. This can be omitted, but if you have an API key for Google Maps (which you will need if your app will have a high load) then you can add it here.

You will notice that at the end of the constructor we are making a call to loadGoogleMaps. This is what will trigger all of our logic but before we move onto that, we also need to create our connectivity service that is also referenced in the constructor.

We are also importing Geolocation from the Ionic Native library that we will make use of soon, and we are using ViewChild and ElementRef to set up a reference to the map element that we will add to our template later. This way, we can simply add #map to the element in the HTML, and we will be able to set up a reference to it called mapElement by doing this:

@ViewChild('map') mapElement: ElementRef;

We’ve also added declare var google; so that TypeScript won’t throw up any errors when we start using the google object (you could also install the typings for the object if you prefer).

3. Creating a Connectivity Service in Ionic 2

We’re going to make this service work both through the browser and when it is running on a device. We can much more accurately detect if the user has an Internet connection on a device if we use the Cordova Network Information plugin, so let’s add that by running the following command:

ionic plugin add cordova-plugin-network-information

We’ll also be making use of the Geolocation API, so feel free to add that plugin as well:

ionic plugin add cordova-plugin-geolocation

If you take a look at src/providers/connectivity-service.ts now it should look like this:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class ConnectivityService {

  constructor(public http: Http) {
    console.log('Hello ConnectivityService Provider');
  }

}

This is what is automatically generated by the Ionic CLI, which is a nice starting point, but we are going to have to build on that.

Modify src/providers/connectivity-service.ts to reflect the following:

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

declare var Connection;

@Injectable()
export class ConnectivityService {

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

We are injecting the Platform service which is provided by Ionic to detect whether we are running on iOS or Android (or neither). We want to use the network information plugin to check if the user is online if we are running on a device, so we need to run some different code if they are running through a normal browser.

We’ve simply added two functions to this service which we will be able to call later, one to check if the user isOnline and one to check if the user isOffline. These both really do the same thing, and you could get away with having just the one function if you wanted. If we are running on a device we check the online status by checking navigator.connection.type, and if we are running through a browser we instead check navigator.onLine.

Now all we need to do is import this connectivity service into any class that we want to use it in (like we have already done in home.ts).

4. Load Google Maps only when Online

Ok now that we’ve got our connectivity service sorted we can get back to the logic of our Google Maps page. There’s actually quite a few steps we need to take care of, so I’m going to do a bit of a code dump here and then walk through it.

Modify src/pages/home/home.ts to reflect the following:

import { Component, ElementRef, ViewChild } from '@angular/core';
import { NavController } from 'ionic-angular';
import { ConnectivityService } from '../../providers/connectivity-service';
import { Geolocation } from 'ionic-native';

declare var google;

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

  @ViewChild('map') mapElement: ElementRef;

  map: any;
  mapInitialised: boolean = false;
  apiKey: any;

  constructor(public nav: NavController, public connectivityService: ConnectivityService) {
    this.loadGoogleMaps();
  }

  loadGoogleMaps(){

    this.addConnectivityListeners();

  if(typeof google == "undefined" || typeof google.maps == "undefined"){

    console.log("Google maps JavaScript needs to be loaded.");
    this.disableMap();

    if(this.connectivityService.isOnline()){
      console.log("online, loading map");

      //Load the SDK
      window['mapInit'] = () => {
        this.initMap();
        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()){
      console.log("showing map");
      this.initMap();
      this.enableMap();
    }
    else {
      console.log("disabling map");
      this.disableMap();
    }

  }

  }

  initMap(){

    this.mapInitialised = true;

    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.nativeElement, mapOptions);

    });

  }

  disableMap(){
    console.log("disable map");
  }

  enableMap(){
    console.log("enable map");
  }

  addConnectivityListeners(){

    let onOnline = () => {

      setTimeout(() => {
        if(typeof google == "undefined" || typeof google.maps == "undefined"){

          this.loadGoogleMaps();

        } else {

          if(!this.mapInitialised){
            this.initMap();
          }

          this.enableMap();
        }
      }, 2000);

    };

    let onOffline = () => {
      this.disableMap();
    };

    document.addEventListener('online', onOnline, false);
    document.addEventListener('offline', onOffline, false);

  }

}

As I mentioned before, the first thing we do is call the loadGoogleMaps function that we have now created. This first checks if the google object is available, which would mean the SDK has already been loaded. If the SDK has not been loaded then we check if the user is online and then we load it by injecting the script into the document. Notice that we also supply a callback function that will trigger when the SDK has been loaded, this way we know when it is safe to start doing stuff with the map. At this point, if there is no Internet connection then we don’t do anything.

The next thing we do (assuming an Internet connection is available) is run the initMap function. This simply handles creating a new map by using the loaded Google Maps SDK and sets the coordinates to the users current location using the Geolocation API.

The only other important bit of code here is the addConnectivityListeners function. This will constantly listen for when the user comes back online, and when they do it will trigger the whole loading process we just discussed above (if it has not been completed already).

This code will also call the enableMap and disableMap functions every time the Internet connection is lost or gained. Right now these don’t do anything apart from log a message to the console, but you can modify these to take whatever action you need.

5. Add the Map to the Template

If you’ve tried running your code during this process you will have found that no map actually displays on the screen (and you’ll probably get some errors too). This is because we still need to add the map to our template and also add a bit of styling.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>Map</ion-title>
  </ion-navbar>
</ion-header>

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

Modify map.scss to reflect the following:

.ios, .md {

  home-page {

    .scroll-content {
      height: 100%
    }

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

  }

}

Summary

If you compare the code of this tutorial to my other Google Maps tutorial for Ionic 2 you’ll notice that it is a lot more complex. You can’t assume that the use will always have an Internet connection though, so it means a solution like this is necessary in a production environment. If you were to go with the simpler approach and your user didn’t have an Internet connection when they first opened the app then it just wouldn’t work at all until they completely restart the app.

The good news is though that this process never really changes, now that you’ve created one map component you should be able to quite easily drop it into any of your applications and just build on top of it.

If you’d like to see how to do some more things with Google Maps in Ionic 2, my other tutorial covers how to add markers and info windows to the map.

What to watch next...