Firebase Real-Time Location Tracking App Using Google Maps API

Last modified on September 19th, 2022
Raja Tamil
Firebase Vue.js

By the end of this tutorial, you’ll be able to:

  • Track individual driver’s location, with their permission, using HTML5 Geolocation API after logging into the app.
  • Store authenticated driver’s location data to the Cloud Firestore as the location changes.
  • Show all the active driver’s current location in real-time on Google Maps in the Admin view.

🛑 The app will only get location updates as long as it’s on the foreground state and it STOPS giving update when it’s in the background mode.

What You’ll Make by the End of this Tutorial

Infographics

Table Of Contents

Project Setup

I have created five page based components in the starter Vue project which are:

  • Login.vue
  • Signup.vue
  • Driver.vue
  • Admin.vue

Before going further, we need to include a couple of files to index.html.

Add a Semantic UI CSS framework to build a UI.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">

Also add the Google Maps JavaScript library CDN link to the project.

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

Create A New Firebase User

Of course, we need authentication in place to track multiple drivers at once.

Firebase offers a variety of sign-in methods such as Facebook, Google, etc but I am going to stick with Email/Password for simplicity sake.

🛑 Assuming you already know how to add firebase to the vue.js project.

Enable Email/Password Authentication by going to Firebase ConsoleFirebase AuthenticationSign-in MethodEmail/Password.

Here is the standard signup form with just an email and password.

Signup.vue

<template>
  <section class="ui centered grid" style="margin-top:30px;">
    <div class="column" style="max-width:450px;">
      <form class="ui segment large form">
        <div class="ui segment">
          <div class="field">
            <div class="ui left icon input large">
              <input type="text" placeholder="Enter your email" v-model="email" />
              <i class="ui icon user"></i>
            </div>
          </div>
          <div class="field">
            <div class="ui left icon input large">
              <input type="password" placeholder="Enter your password" v-model="password" />
              <i class="ui icon key"></i>
            </div>
          </div>
          <button class="ui button fluid large green" @click="signUpButtonPressed">Sign up</button>
        </div>
      </form>
      <router-link :to="{ name: 'Signin' }" tag="a" class="ui button basic">Log in</router-link>
    </div>
  </section>
</template>

<script>
export default {
  data() {
    return {
      email: "",
      password: "",
    };
  },
  methods: {
    signUpButtonPressed() {
    },
  }
};
</script>

Import firebase at the top inside the script tag.

import firebase from 'firebase'

Then, invoke createUserWithEmailAndPassword() method on the firebase object to create a new user inside the signUpButtonPressed().

firebase
  .auth()
  .createUserWithEmailAndPassword(this.email, this.password)
  .then(user => {
    console.log("Brand new user is added")
  })
  .catch(error => {
    console.log(error.message);
  });

To show all the users data on the Admin view, add user data to the Cloud Firestore when a new user account is created.

signUpButtonPressed() {
   firebase
      .auth()
      .createUserWithEmailAndPassword(this.email, this.password)
      .then(user => {
         this.addUserToDB(user);
      })
      .catch(error => {
         this.error = error.message;
      });
},

async addUserToDB({user}) {
   try {
      const db = firebase.firestore();
      _ = await db
         .collection("users")
         .doc(user.uid)
         .set({
            email: user.email,
            active:false
         });
      
   } catch (error) {
      console.log(error.message);
   }
}

As you can see, I have created three user accounts from the signup.vue page.

Add User Roles

  • The Admin role will have access to all the drivers stored in the database and will be able to track their location at any time.
  • The Driver role will have access to read or write their own data and will be sending location updates to the database as they move.

Adding a role to each user should be done on the server side for security reasons and it can be done in two different ways using Cloud Function.

  • Using Auth Claims.
  • Adding read-only (from the client) collection that has user roles on the Cloud Firestore.

For simplicity sake, I have added three user roles inside the roles collection with the same document ID as the user’s collection.

Two of them have isDriver : true field and one has the isAdmin : true field.

Roles collection on the Cloud Firestore

Firebase Security Rules

1. Only authenticated users can read or write data to the users collection.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {	
    match /users/{uid} {
      allow read, write: if request.auth.uid == uid
    }
  }
}

2. Only authenticated users can read to the roles collection but no one can write to it from the front-end.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
   ...	
    match /roles/{uid} {
      allow read: if request.auth.uid == uid;
      allow write: if false;
    }
  }
}

3. Get all the user data when a signed in user has an Admin role.

We can access the roles collection inside here using the get method by passing the database path to the isAdmin field.

 match /users/{uid} {
      allow read, write: if request.auth.uid == uid || get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.isAdmin;
    }

Want to learn more about Security Rules?
6 Must-Know Firestore Security Rules

Sign In A User

Again, here is a standard sign in form.

<template>
  <section class="ui centered grid" style="margin-top:30px;">
    <div class="column" style="max-width:450px;">
      <form class="ui segment large form">
        <img class="ui centered medium image" src="@/assets/rt-logo.png" style="max-width:150px;" />

        <div class="ui segment">
          <div class="field">
            <div class="ui left icon input large">
              <input type="text" placeholder="Enter your email" v-model="email" />
              <i class="ui icon user"></i>
            </div>
          </div>
          <div class="field">
            <div class="ui left icon input large">
              <input type="password" placeholder="Enter your password" v-model="password" />
              <i class="ui icon key"></i>
            </div>
          </div>
          <button class="ui button fluid large red" @click="loginButtonPressed">Log in</button>
        </div>
        <router-link :to="{ name: 'SignUp' }" tag="a" class="ui button basic">Sign up</router-link>
      </form>
    </div>
  </section>
</template>
<script>
import firebase from "firebase";
export default {
  data() {
    return {
      email: "",
      password: ""
    };
  },
  methods: {
    loginButtonPressed() {
      firebase
        .auth()
        .signInWithEmailAndPassword(this.email, this.password)
        .then(user => {
          console.log("user is singed in ");
        })
        .catch(error => {
          console.log(error.message);
        });
    }
  }
};
</script>

At this stage, when a user is signed in, the app goes nowhere.

Let’s fix it.

Using Auth Guard, we can send users to appropriate routes based on their roles.

I have added two different meta objects to the routes:

  • requiresAuth:true for authenticated user only.
  • guest:true
let router = new Router({
  routes: [{
      path: '/driver',
      name: 'Driver',
      component: Driver,
      meta: {
        requiresAuth: true
      }
    },
    {
      path: '/signin',
      name: 'Signin',
      component: Signin,
      meta: {
        guest: true
      }
    },
    {
      path: '/signup',
      name: 'SignUp',
      component: SignUp,
      meta: {
        guest: true
      }
    },
    {
      path: '/admin',
      name: 'Admin',
      component: Admin,
      meta: {
        requiresAuth: true
      }
    }
  ]
})

router.beforeEach((to, from, next) => {
  firebase.auth().onAuthStateChanged(userAuth => {
    if (!userAuth && to.matched.some(record => record.meta.requiresAuth)) {
      next({
        name: 'Signin'
      })
    } else if (userAuth) {
      if (to.matched.some(record => record.meta.guest)) {
        next(from.fullPath)
      } else {
        firebase.firestore().collection("roles").doc(userAuth.uid).get().then(snapShot => {
          if (snapShot.data().isAdmin) {
            next({
              name: 'Admin'
            })
          } else {
            next({
              name: 'Driver'
            })
          }
        })
      }
    } else {
      next()
    }
  })
  next()
})
export default router

Using the onAuthStateChanged method, we can check to see if there is any signed in user before getting into any route.

Redirect to the sign-in route, when a user wants to go to the protected route without signing in.

Go back to the previously protected route when a signed-in user wants to go to the guest route such as sign up, sign in etc.

Otherwise, send a signed-in user to an appropriate route based on his/her role.

Want to learn more about Role-Based Authentication & Authorization
Vue.js + Firebase: Role-Based Authentication & Authorization

Driver View

The drive view has three UI Elements:

  • Top bar where the user email, role name, and the signout button will be.
  • The map which holds the Google Maps object.
  • The bottom bar that contains two buttons: start location & stop location with the latitude and longitude labels above.

Driver.vue

This view has to be mobile friendly as drivers will be using their phones to view this page when they travel.

<template>
  <div class="driver-view">
    <section class="top-bar">
      <div>User Email</div>
      <div>Driver</div>
      <button class="ui button red" @click="signOutButtonPressed">Signout</button>
    </section>

    <section ref="map" class="map"></section>

    <section class="bottom-bar">
      <div class="latLngLabel"></div>
      <button class="ui button green" @click="startLocationUpdates">
        <i class="circle dot outline icon large"></i>
        Start Location
      </button>

      <button class="ui button red" @click="stopLocationUpdates">
        <i class="circle dot outline icon large"></i>
        Stop Location
      </button>
    </section>
  </div>
</template>

<script>
export default {
 methods: {
    signOutButtonPressed() {},
    startLocationUpdates() {},
    stopLocationUpdates() {}
 }
}
</script>

The top and bottom bar elements have a fixed height and the map element will stretch to fill the remaining vertical space on the view port.

<style>
.driver-view {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.top-bar {
  height: 100px;
  text-align: center;
  background: white;
}

.map {
  flex-grow: 1;
}

.bottom-bar {
  height: 100px;
  text-align: center;
  background: white;
}
</style>

Show Signed-in User Email

Define a property called user inside the data model.

Call the onAuthStateChange method on the firebase to check if there are any signed-in user objects and assign it to the user property.

driver.vue

<template>
    <section class="top-bar">
      <div v-if="user">{{user.email}}</div>
     ...
    </section>
...
</template>
<script>
... 
data() {
    return {
      user: null,
    };
  },
  mounted() {
    firebase.auth().onAuthStateChanged(user => {
      this.user = user;
    });
  },
...
</script>

Sign Out User

driver.view

Invoke the signOut() method on the firebase auth object. Once it’s successful, redirect to the sign-in page.

 signOutButtonPressed() {
      firebase
        .auth()
        .signOut()
        .then(user => {
          this.$router.push({
            name: "Signin"
          });
        })
        .catch(error => {
          console.log(error.message);
        });
    },

Get User Location Using watchPosition()

To show the latitude and longitude values above the buttons on the view, add lat and lng properties to the data model.

data() {
    return {
      ...
      lat:0,
      lng:0,
    };
},

Then, bind the properties to the template.

<section class="bottom-bar">
      <div class="latLngLabel">{{lat}}, {{lng}}</div>
      ...
</section>

Then, assign coordinate values to the lat and lng properties inside the watchPosition() method.

 startLocationUpdates() {
      navigator.geolocation.watchPosition(
        position => {
          this.lat = position.coords.latitude;
          this.lng = position.coords.longitude;
        },
        error => {
          console.log(error.message);
        }
      );
},

The watchPosition() method is a part of HTML5 Geolocation API that gets location updates frequently upon the user’s permission.

The first callback function inside the watchPosition() method will receive a location object specified in the position parameter.

🛑 But the location updates will stop working when the browser is in the background mode. 😕

Show Current Location On The Map

Instantiate the map and marker objects above the watchPosition() method to avoid repeated object creation inside the startLocationUpdates().

var map = new google.maps.Map(this.$refs["map"], {
   zoom: 15,
   mapTypeId: google.maps.MapTypeId.ROADMAP
});

var marker = new google.maps.Marker({
   map: map
});

Center the map based on the user’s current location and place the marker in it inside the watchPosition() success callback function.

 map.setCenter(new google.maps.LatLng(this.lat, this.lng));
 marker.setPosition(new google.maps.LatLng(this.lat, this.lng));

To do that, invoke the setCenter() method on the map object passing the LanLng object with the coordinate values. Similarly, call the setPosition() method on the marker.

Stop Location Updates

To stop the location updates, we need to call the clearWatch() method on the geolocation object passing the watchPosition() ID as an argument.

Define the watchPositionId:null property.

data() {
    return {
      ...
      watchPositionId:null
    };
},

Assign the watchPosition() method to it.

this.watchPositionId = navigator.geolocation.watchPosition(
...
)

Call the clearWatch() method inside the stopLocationUpdates() function.

stopLocationUpdates() {
  navigator.geolocation.clearWatch(this.watchPositionId);
},

Add Location Data To The Cloud Firestore

To see all the driver’s locations at once in the Admin view, we need to store them to the database as they change.

Inside the updateLocation() function, update an existing signed-in user’s document by adding latitude and logitude fields to it in the users collection.

 updateLocation(lat, lng) {
    const db = firebase.firestore();
    db.collection("users")
        .doc(this.user.uid)
        .set({ lat: lat, lng: lng, active:true }, { merge: true });
}

The active field is set to false by default and it will be true when the start location is pressed.

That way, we will only get drivers that are active in the admin view.

The merge flag will only add fields that can’t be found in the document otherwise it will overwrite it.

Then, invoke the updateLocation() inside the watchPosition() success callback function.

this.updateLocation(this.lat, this.lng)

Mock Multiple Driver’s Movement on the iOS Simulator

To test on the iOS Simulator, we need to deploy the app over https. I have an article that shows how to deploy vue.js app to Firebase Hosting in minutes for FREE!

Once it’s deployed, open up a couple of iOS Simulators and navigate to the URL that Firebase Hosting provides and log in to them.

To mock the driving movement, go to the iOS Simulator menu bar at the top choose Debug Location Freeway Drive.

Then, hit start location in both the simulators and you can see the markers updating as the locations change.

You can also see the locations being updated on the Cloud Firestore.

Nice!

Admin View

Let’s get the active driver list from the Cloud Firestore onto the left view and the map with markers indicating drivers movement onto the right.

To listen for any changes on the database, use the onSnapShot() instead of get() when getting user data from the Cloud Firestore.

<template>
  <div class="admin-view">
    <section class="left-view">
      <div class="ui segment center aligned">
        <div v-if="user">{{user.email}}</div>
        <div>Admin</div>
        <button class="ui button red" @click="signOutButtonPressed">Signout</button>
      </div>
      <div class="ui segment">
        <div class="ui divided items">
          <div
            class="item"
            v-for="driver in drivers"
            :key="driver.id"
          >
            <div class="content">
              <div class="header">{{driver.email}}</div>
              <div class="meta">
                Lat : {{driver.lat}}
                <br />
                Lng :{{driver.lng}}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
    <section class="right-view" ref="map"></section>
  </div>
</template>

<script>
import firebase from "firebase";
export default {
  data() {
    return {
      user: null,
      drivers: []
    };
  },
  async mounted() {
    firebase.auth().onAuthStateChanged(user => {
      this.user = user;
    });

    firebase
      .firestore()
      .collection("users")
      .where("active", "==", true)
      .onSnapshot(snap => {
        this.drivers = [];
        for (let i = 0; i < snap.docs.length; i++) {
          var driver = snap.docs[i].data();
          this.drivers.push(driver);
        }
      });
  },
  methods: {
    signOutButtonPressed() {
      firebase
        .auth()
        .signOut()
        .then(user => {
          this.$router.push({
            name: "Signin"
          });
        })
        .catch(error => {
          console.log(error.message);
        });
    }
  }
};
</script>


<style>
.admin-view {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: row;
}
.left-view {
  width: 250px;
  padding: 12px;
}

.right-view {
  flex-grow: 1;
}
</style>

Then, add the map onto the right inside the mounted().

Declare the map property in the data model and assign the map object to it as we will need to access the map object outside of the mounted().

this.map = new google.maps.Map(this.$refs["map"], {
      zoom: 10,
      center: new google.maps.LatLng(37.33446146, -122.04380955),
      mapTypeId: google.maps.MapTypeId.ROADMAP
});

Now, define a markers array with the markers objects that matches the number of drivers.

mounted() {
   ...
   var markers = []
   var {docs} = await firebase
      .firestore()
      .collection("users")
      .where("active", "==", true)
      .get();

   for (let i = 0; i < docs.length; i++) {
      markers.push(
         new google.maps.Marker({
            map: this.map
         })
      );
   }
}

Set each marker position inside the drivers query loop.

firebase
   .firestore()
   .collection("users")
   .where("active", "==", true)
   .onSnapshot(snap => {
      this.drivers = [];
      for (let i = 0; i < snap.docs.length; i++) {
         ...
         markers[i].setPosition(
            new google.maps.LatLng(driver.lat, driver.lng)
         );
      }
});

Similar to Marker, create an array of infoWindows that are just popovers with some text above the markers.

const infoWindows = [];

for (let i = 0; i < docs.length; i++) {
   ...
   infoWindows.push(new google.maps.InfoWindow({
      disableAutoPan: true
   }));
}

Then, show the user email onto the info window and open it by default.

firebase
   .firestore()
   .collection("users")
   .where("active", "==", true)
   .onSnapshot(snap => {
      this.drivers = [];
      for (let i = 0; i < snap.docs.length; i++) {
        ...
         infoWindows[i].setContent(
            `<div class="ui header">${driver.email} </div>`
         );
         infoWindows[i].open(this.map, markers[i]);
      }
   });

You can find the source code in here