loading...
Cover image for Implementing a realtime geo-location tracker with VueJS and Ably
Ably Realtime

Implementing a realtime geo-location tracker with VueJS and Ably

ogwurujohnson profile image Johnson Ogwuru ・10 min read

In this tutorial, we will see how to use the Ably Realtime client library to build a realtime location Tracking PWA with Vue js. Ably enables realtime data sharing using Pub/Sub messaging architecture via a concept called channels.

For the purpose of this tutorial, we'll be discussing

  • How to build PWAs in Vue
  • How to use the Geolocation in APIs in PWAs
  • How to share the live location updates with other clients using Ably

The application we would be building will make use of Ably to share realtime location data in the app. As an example, we would be building a friend location tracker with the following specifications:

  • Any user who comes online on the app is able to see their location represented by a blue pin.
  • The location of any other users that are online needs to be represented by a red pin
  • All these location pins need to be plotted on a map and need to move in real-time as the user device moves. Here is a link to a demo of the application we will build.

And a screenshot of it.
landing page
application

Getting Started

Vue.js is an open-source model–view–viewmodel(MVVM) JavaScript framework for building user interfaces and single-page applications. It was created by Evan You and is maintained by him and the rest of the active core team members coming from various companies such as Netlify and Netguru.

The Smashing Magazine, defined a PWA as a progressive web application that takes advantage of the latest technologies to combine the best of web and mobile apps. We could think of it as an application built with web technologies but behaves like a mobile application.

Once a site has a PWA built and ready to go, Chrome will push it to be installed on a user’s mobile device so long as it meets the following criteria:

  1. It is running under HTTPS - Emphasis on the “S” there. Your site must be secured with an SSL certificate.

  2. It has a Web App Manifest - This is a JSON file that lets you customize various features of your app such as name, colors, design, etc.

  3. It has a Service Worker - This is a JavaScript file that allows your PWA to work offline (to the extent that it is capable, of course). It’s essentially the script that is always working tirelessly in the background.


Step1 - Setup an Ably Account

In order to run this tutorial locally, you will need an Ably API key. If you are not already signed up, you should sign up now for a free Ably account. Once you have an Ably account:

  • Log into your app dashboard
  • Under “Your apps”, click on “Manage app” for any app you wish to use for this tutorial, or create a new one with the “Create New App” button Click on the “API Keys” tab
  • Copy the secret “API Key” value from your Root key and store it so that you can use it later in this tutorial

setup ably account

Step2: Vue CLI

Make sure you have node and npm installed. If you do, we would need to install Vue CLI, which is a boilerplate that speeds up the process of getting started with building a vue application.

We start off by creating the basic setup and the file structure of our app. To speed things up, we will bootstrap the app with vue-cli.
First, we need to install the vue CLI tool globally.
yarn global add @vue/cli

Now we can instantiate the template by
vue init pwa friend-finder

We will be prompted to pick a preset — I recommend the following configuration with your personal details where necessary of course:
? Project name friend-finder
? Project short name: fewer than 12 characters to not be truncated on home screens (default: same as name) friend-finder
? Project description A simple friend finder
? Author Johnson Ogwuru <ogwurujohnson@gmail.com>
? Vue build runtime
? Install vue-router? No
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Setup unit tests with Karma + Mocha? No
? Setup e2e tests with Nightwatch? No

For the Vue build configuration, we can choose the smaller runtime option.

Next, run yarn or npm install to install all the dependencies. To start the development mode just run yarn start or npm start.

Don’t get confused by all the files in the project — For now, we won’t touch most of them. If you want to learn more about the template’s structure check out the documentation. I can only recommend it!

Next, we would need to install all the packages we would be using in this project yarn add ably vue2-google-maps or npm install ably vue2-google-maps

After installing the packages, head straight to https://support.google.com/googleapi/answer/6158862?hl=en, to create a project and then get an API key for making requests to Google Cloud server. This API key we get from Google is what we need to be able to make requests to the Google Maps API. Without it, we won't have the authentication needed to make a request to the Google Maps API.

Step3: Building Our Product
By running this script vue init pwa friend-finder on the terminal to bootstrap our application we had already instructed vue to create a PWA application for us. And now for the application, we are building.

We will start with getting our map displayed on the application and to do this we would make use of the npm package vue2-google-maps. Since we already have it installed we would start making use of it.

We would also need to install the Vue router, our application would require an onboarding page detailing how to use the application. So add router to our vue application, run yarn add vue-router on the terminal.

*3.1 - * Navigate to the main.js file, which is located in the src folder and paste the following code, replacing what we had in the file initially.


      import Vue from 'vue'
      import App from './App'
      import router from './router'

      import * as VueGoogleMaps from 'vue2-google-maps'

      Vue.use(VueGoogleMaps, {
        load: {
          key: '<your google map key>',
          libraries: 'places',
        },
      })

      Vue.config.productionTip = false

      /* eslint-disable no-new */
      new Vue({
        el: '#app',
        router,
        template: '<App/>',
        components: { App }
      })

In the above code, we get to import the google maps library and instantiate it, while providing the necessary credentials like your generated API key. Then we instantiate the Vue class, passing to it our template, router and component of choice which is App.

*3.1.1 - * Next up, you need to create the components/Application.vue file and replace the code in it with this

  <template>
    <div id="app">
      <GmapMap
        :center="{lat: 10, lng:10}"
        :zoom="15"
        map-type-id="terrain"
        style="width: 100%; height: 100%"
      >
      </GmapMap>
    </div>
  </template>

  <script>
    export default {
      name: 'app',
    }
  </script>

In the code above, we create our map using the GmapMap component and pass it the following properties zoom, center, map-type, style which contributes to how the map looks on the browser.

*3.2 - * The next thing in our bucket list would be to have our application retrieve the user's location and to do this we would be making use of the geolocation API available in HTML5. Paste the following code inside the <script> tag in app.vue file.

   methods: {
    fetchData() {
      if (!("geolocation" in navigator)) {
        this.errorStr = "Geolocation is not available.";
        return;
      }
      this.gettingLocation = true;
      navigator.geolocation.watchPosition(
        pos => {
          this.gettingLocation = false;
          this.initialPosition.lat = pos.coords.latitude;
          this.initialPosition.lng = pos.coords.longitude;
          const userData = {
            position: {
              lat: pos.coords.latitude,
              lng: pos.coords.longitude
            },
            userName: this.usersName
          };
          this.userlocation = userData;
          this.updateRoom(userData);
        },
        err => {
          this.gettingLocation = false;
          this.errorStr = err.message;
        }
      );
    },
   }

In the code above we are fetching the users location, which we wrap in an object together with the users' name (which we would provide how it would be supplied later on), then we call a method that handles publishing to ably realtime with the users' credential as an argument. The methods property in the file, is how vue s specifies methods to be used in the application. They are functions that hang off of an object-typically the Vue instance itself or a Vue component.

*3.2.1 - * Next we would create the method updateRoom, which we would use to update the presence of a user in a channel, while at the same time sending certain information about the users' current location.
Before we do that, we would want to import ably and set it up, so on the lines following the opening script tag, paste the following code

   import * as Ably from "ably";
   var ably = new Ably.Realtime({
     key: "<your ably key>",
     clientId: `${Math.random() * 1000000}`
   });

Now we have imported the ably library and configured ably to use in our application. In order for a user to be present in the channel, the user must be identified by having a clientId. A single clientId may be present multiple times on the same channel via different client connections. As far as Ably is concerned, these are different members of the presence set for the channel, however, they will be differentiated by their unique connectionId. For example, if a client with ID “Sarah” is connected to a chat channel on both a desktop and a mobile device simultaneously, “Sarah” will be present twice in the presence member set with the same client ID, yet will have two unique connection IDs. A member of the presence set is therefore unique by the combination of the clientId and connectionId strings.

So it's time for us to send location data to ably and update data too by making use of the updateRoom method. Copy the following code and paste under the fetchData method.

  updateRoom(data) {
      channel.presence.update(data, function(err) {
        if (err) {
          return console.error("Error updating presence data");
        }
        console.log("We have successfully updated our data");
      });
    }

In the above code, we update the information of the user, in their registered ably channel, and this makes it possible for everyone subscribed to the channel to receive the new update in real-time without page refreshes, leveraging the power of web sockets

*3.3 - * Next we need a way to listen to changes in the channel so that when a user's presence is updated, all users in the channel get notified. And to do these we would have to add some extra block of codes in the created() method of vue js. The created() method in vue is a method that allows you to add code, once the vue instance is created. So now we would be saying once the vue instance is created keep checking if an update exists and subscribe to the channel so that any information update on the channel the user can get it. So above the methods() block at this piece of code. But before that, we need to get some information from the user, like their names and the name of the channel they would love to join. Type the following code:

mounted() {
   const name = prompt('To get started, input your name in the field below and locate your friends around based on your location, please turn on your location setting \n What is your name?')
   this.usersName = name
   const channel = prompt('Enter the name of the channel you are interested in')
   this.channelName = channel
 }, 

Ask for users name
Ask for users choice of channel

In the above code, we add the prompt code in the mounted() property, every code inside this property run immediately the component mounts. So we pick this information and store them in the assigned state variables.


async created() {
    await this.fetchData();
    var channel = ably.channels.get(this.channelName);
    channel.attach(err => {
      if (err) {
        return console.error("Error attaching to the channel");
      }
      console.log("We are now attached to the channel");
      channel.presence.enter(this.userlocation, function(err) {
        if (err) {
          return console.error("Error entering presence");
        }
        console.log("We are now successfully present");
      });
    });

    let self = this;
    channel.presence.subscribe("update", function(presenceMsg) {
      console.log(presenceMsg)
      console.log(
        "Received a " + presenceMsg.action + " from " + presenceMsg.clientId
      );
      channel.presence.get(function(err, members) {
        console.log(members)
        self.markers = members.map(mem => {
          if (JSON.stringify(self.userlocation) == JSON.stringify(mem.data)) {
            return {
              ...mem.data,
              icon: "http://maps.google.com/mapfiles/ms/icons/blue-dot.png"
            };
          } else {
            return {
              ...mem.data,
              icon: "http://maps.google.com/mapfiles/ms/icons/red-dot.png"
            };
          }
        });
        self.onlineUsers = members;
        console.log(
          "There are now " + members.length + " clients present on this channel"
        );
      });
    });
  },

In the above code, after subscribing to the channel, we expect ably to update us in real-time of changes in the channel, which include users presence activities on the channel, which gets stored in a variable, so what we do is for every 3 seconds, pick whatever is in the document and add to a slice of state to keep our application up to date, in response to the real-time data supplied by ably.

We needed each user on the map to have different colors, in that we wanted the owner of the device having a different marker color from the rest of the other markers on the map, which was why we added this logic to the code above return {...mem.data, icon: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png'}

So what happens is that, in this line var channel = ably.channels.get(this.channelName); we are either creating a new channel or joining a channel if the channel exists. So if the channel you submitted when the component mount exists, all you do is join it, but if it doesn’t then ably creates as a new one and you are able to ask others to join too. Now because we need to pick certain information from users when they join the channel, we would have the application send their location data when it’s registering their presence in the channel by using the line of code starting with channel.presence.enter..., and also while the user is in the channel, they want to be kept updated as regards events in the channel, so to listen for events, we would use one of Ably’s API, the channel.presence.subscribe.... Here you specify the event you are listening for, in our case it is update, so when it is fired, we want to get the information of everyone in the channel, with their location data, Ably’s channel.presence.get API gets us this information.

*3.4 - * Next stop we need a set of state slices, some of which we have already used in the codes above and I'm certain you already started asking yourself where the came from, well here they are. Add the following code on top of the methods() block.

   data() {
    return {
      usersName: null,
      gettingLocation: true,
      initialPosition: {
        lat: 10,
        lng: 10
      },
      zoom: 11,
      markers: null,
      userlocation: []
    };
  },

Complete application

In conclusion: The information contained here might be quite overwhelming, so for that, I have provided the repo for the project for further understanding. You can find the demo here

GitHub Repo

Discussion

pic
Editor guide