loading...
Ionic

Building an Ionic 4 Firebase Location Tracker with Capacitor & Google Maps

simon profile image Simon Grimm Originally published at devdactic.com ・11 min read

With Capacitor 1.0 just days ago released it’s time to get into it – and what would be a better scenario than refreshing and updating one of the most read tutorials?

In this tutorial we will go through the steps of building a Capacitor App with Ionic 4, Firebase integration and Google maps.

ionic-4-firebase-capcitor

This means we will combine different technologies but none of them on their own are hard to handle, so just follow along and you’ll have your first Capacitor app ready soon!

Setting up our Ionic App, Capacitor & Firebase

As a small prerequisite make sure you got a Firebase app, so simply create a free account and start a new project like you can also see in my previous Firebase tutorial!

To use Capacitor you don’t have to install anything specific, and you got two options for integration:

  • Start a new Ionic project and automatically integrate it
  • Add Capacitor to an existing project

We are going with the first option, but the second works totally fine as well.

So simply run the commands below to create a new project, build the project once (which is important) and then you can add the native platforms:

ionic start devdacticTracking blank --capacitor
cd devdacticTracking
npm install firebase @angular/fire

// Build at least once to have a www folder
ionic build

npx cap add android
npx cap add ios

We will use a geolocation plugin from Capacitor, and normally Cordova would add some permissions for iOS and Android automatically.

However, Capacitor is different. You can read more about it in the release post and as well in the information about native projects.

In general this means that the native iOS/Android project should now be a real part of your project – not just an autogenerated folder that you don’t touch!

Therefore, you will have to get into the native projects and specify the right permissions like described for iOS and Android.

But from my observation, the iOS changes were already applied upfront.

If you run into problems on your Mac also make sure to check your pods version:

pod --version #should be >= 1.6
# Update if needed
sudo gem install cocoapods
pod setup

So if needed update your pods which are a system like NPM for native iOS apps that manages dependencies.

Now your Capacitor app is basically ready but we need to integrate a few more things.

Preparing our App

As we want to show a map with locations we need an API key from Google. Apparently this can be done in seconds following this link and generating a new key.

Once you got the key, open your index.html and add this snippet at the end of the head block:

<script src="https://maps.googleapis.com/maps/api/js?key=YOURKEY"></script>

Now we also need to tell our app about Firebase, and as we are using Angularfire we need to initialize it with the information from Firebase.

ionic-firebase-capacitor-dialog

In the previous part you should have created a Firebase project, and now it’s time to copy the project settings that you should see after you have added a platform to your project. The information goes straight to your environments/environment.ts file and should look like this:

export const environment = {
  production: false,
  firebase: {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
  }
};

Now we just need to load this environment information in our app/app.module.ts and also import the Auth and Store module so we got access to all the functionality that we need:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { environment } from '../environments/environment';

import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

As we are using a simply anonymous login make sure to enable it inside your Firebase authentication settings as well!

ionic-capacitor-firebase-anon-auth

We don’t need anything else for Capacitor as we have set up our app with it and it was already included. The geolocation plugin is already part of the core API!

Building our Geolocation Tracker

Now it’s time to get to the meat of the article, in which we build and combine everything.

First of all our app needs to load the Google map on startup, and to prevent any Typescript errors we can simply declare the variable google at the top. Remember, we have added the maps script to our index.html so we don’t need any other package.

We will also automatically sign in a user – anonymously. This makes the tutorial a bit easier but of course you could simply add a login and authentication system as well.

There are great courses on this topic inside the Ionic Academy as well!

After the automatic login we establish a connection to our Firebase collection at locations/${this.user.uid}/track. This means, we can store geolocation points for each anon user in their own separate list.

Now go ahead and add this first bit of code to your app/home/home.page.ts:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import {
  AngularFirestore,
  AngularFirestoreCollection
} from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { Plugins } from '@capacitor/core';
const { Geolocation } = Plugins;

declare var google;

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  // Firebase Data
  locations: Observable<any>;
  locationsCollection: AngularFirestoreCollection<any>;

  // Map related
  @ViewChild('map') mapElement: ElementRef;
  map: any;
  markers = [];

  // Misc
  isTracking = false;
  watch: string;
  user = null;

  constructor(private afAuth: AngularFireAuth, private afs: AngularFirestore) {
    this.anonLogin();
  }

  ionViewWillEnter() {
    this.loadMap();
  }

  // Perform an anonymous login and load data
  anonLogin() {
    this.afAuth.auth.signInAnonymously().then(res => {
      this.user = res.user;

      this.locationsCollection = this.afs.collection(
        `locations/${this.user.uid}/track`,
        ref => ref.orderBy('timestamp')
      );

      // Make sure we also get the Firebase item ID!
      this.locations = this.locationsCollection.snapshotChanges().pipe(
        map(actions =>
          actions.map(a => {
            const data = a.payload.doc.data();
            const id = a.payload.doc.id;
            return { id, ...data };
          })
        )
      );

      // Update Map marker on every change
      this.locations.subscribe(locations => {
        this.updateMap(locations);
      });
    });
  }

  // Initialize a blank map
  loadMap() {
    let latLng = new google.maps.LatLng(51.9036442, 7.6673267);

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

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

There will be errors as some functions are still missing but we’ll get there now.

You can see that we also apply the standard map() on the Firebase collection data which is needed to also retrieve the ID from the objects! And the ID again is needed to later reference and remove a single entry.

We’ll also subscribe to any changes to the array and update our map once we got new data in order to display all marker (and clean old markers).

Now we can also dive into Capacitor and basically use it like you have previously used Cordova and Ionic native. We watch for any location changes and whenever we get a new position, we will add the geolocation with a timestamp to Firebase using our locationsCollection.

I’d say more about it but that’s already all the magic going on here – it’s that easy!

Now append the following functions inside your app/home/home.page.ts:

// Use Capacitor to track our geolocation
startTracking() {
  this.isTracking = true;
  this.watch = Geolocation.watchPosition({}, (position, err) => {
    if (position) {
      this.addNewLocation(
        position.coords.latitude,
        position.coords.longitude,
        position.timestamp
      );
    }
  });
}

// Unsubscribe from the geolocation watch using the initial ID
stopTracking() {
  Geolocation.clearWatch({ id: this.watch }).then(() => {
    this.isTracking = false;
  });
}

// Save a new location to Firebase and center the map
addNewLocation(lat, lng, timestamp) {
  this.locationsCollection.add({
    lat,
    lng,
    timestamp
  });

  let position = new google.maps.LatLng(lat, lng);
  this.map.setCenter(position);
  this.map.setZoom(5);
}

// Delete a location from Firebase
deleteLocation(pos) {
  this.locationsCollection.doc(pos.id).delete();
}

// Redraw all markers on the map
updateMap(locations) {
  // Remove all current marker
  this.markers.map(marker => marker.setMap(null));
  this.markers = [];

  for (let loc of locations) {
    let latLng = new google.maps.LatLng(loc.lat, loc.lng);

    let marker = new google.maps.Marker({
      map: this.map,
      animation: google.maps.Animation.DROP,
      position: latLng
    });
    this.markers.push(marker);
  }
}

When we start the tracking we also keep track of the ID so we can later clear our watch with that ID again.

And regarding our map – we don’t really got the code completion here but all of this is documented by Google pretty well. So whenever we want to update our markers, we set the map to null on all existing markers and then create new ones. Feel free to improve this logic of course by keeping the old ones and only changing what needs to be changed.

If you now also want to draw a route between these points, check out my tutorial on creating a Geolocation tracker!

We now just want to get our map, a few buttons to start/stop the tracking and a list to see our Firebase data update in realtime, so in general just some basic elements.

Note that our locations array is still an Observable so we need to use the async pipe to automatically get the latest data here.

Also, we are using a sliding item so we can pull in that little remove icon from one side and get rid of positions that we don’t want to have in our list!

Now change your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Tracking
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <div #map id="map" [hidden]="!user"></div>

  <div *ngIf="user">

    <ion-item>
      <ion-label>User ID: {{ user.uid }}</ion-label>
    </ion-item>

    <ion-button expand="block" (click)="startTracking()" *ngIf="!isTracking">
      <ion-icon name="locate" slot="start"></ion-icon>
      Start Tracking
    </ion-button>

    <ion-button expand="block" (click)="stopTracking()" *ngIf="isTracking">
      <ion-icon name="hand" slot="start"></ion-icon>
      Stop Tracking
    </ion-button>

    <ion-list>
      <ion-item-sliding *ngFor="let pos of locations | async">
        <ion-item>
          <ion-label text-wrap>
            Lat: {{ pos.lat }}
            Lng: {{ pos.lng }}
            <p>
              {{ pos.timestamp | date:'short' }}
            </p>
          </ion-label>
        </ion-item>

        <ion-item-options side="start">
          <ion-item-option color="danger" (click)="deleteLocation(pos)">
            <ion-icon name="trash" slot="icon-only"></ion-icon>
          </ion-item-option>
        </ion-item-options>

      </ion-item-sliding>
    </ion-list>

  </div>

</ion-content>

Right now you wouldn’t see the map as it needs some CSS to be seen. You can start with the following in your app/home/home.page.scss but of course play around with it so it fits your needs!

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

Now you can run the app on your browser, you should be able to get your location (yes, Capacitor plugins work directly in your browser!) and you can also fake your location in Chrome to see new points added without leaving the house:

Inside the debugging area click on the 3 dots -> More tools -> Sensors.

capacitor-fake-location-chrome-1

From there you can control your Geolocation and fake it to other places!

Build your Ionic + Capacitor App

If you now want to build the native project, for example iOS, you simply run:

npx cap sync
npx cap open ios

Capacitor works different as said in the beginning – the native projects persists and we only sync our web app files with the native project. This also means, your build is way faster than with Cordova before!

Conclusion

Although he initial task might have sounded intimidating, it’s actually super easy to combine all of these services and technologies. Even within a website and not a real app!

There’s a lot more to say about Capacitor and I’ll talk about it soon. If you want to see anything specific with Capacitor just leave me a comment below.

You can also watch a video version of this article below.

https://www.youtube.com/watch?v=Sq0NbvQihrk

Posted on by:

Ionic

The open source UI toolkit for developing high-quality cross-platform apps for native iOS, Android, and the web — all from a single codebase.

Discussion

markdown guide