Build a geofencing web app using Nest.js and the Google Maps API
You will need Node and npm installed on your machine. A basic knowledge of Node.js and TypeScript will be helpful.
Introduction
This tutorial will walk you through the process of implementing a basic realtime location-aware application with geofencing updates. Once we are done, you will have gathered enough knowledge to try out more creative ways on how to make use of virtual boundaries.
Geofencing as a technique, is a virtual perimeter (also referred to as geofences) around a physical location. This can allow you to provide useful experiences or carry out specific actions when users are within or outside the specified vicinity.
To keep our application simple, we will have a list of users with random locations. In a real-world application, you would need to get the current location of a user. Fortunately a tool like Geolocation API is available for use on most browsers.
A quick look at what we will be building in the tutorial:
In this application, we will randomly display a few people from our users list. Once a user is selected, we will set the location of the user as the center of the map and then show the locations of other users with markers.
Prerequisites
A basic understanding of TypeScript and Node.js will help you get the best out of this tutorial. I assume that you already have Node and npm installed, if otherwise quickly check Node.js and npm for further instructions and installation steps.
Here is a quick overview of the core technologies that we will be using in this post.
-
Nest.js: a progressive framework for building efficient and scalable server-side applications; built to take the advantage of modern JavaScript but still preserves compatibility with pure JavaScript.
-
Pusher: a Node.js client to interact with the Pusher REST API
-
GoogleMaps API: GoogleMaps JavaScript API provides utilities that make it possible to add interactive and customizable maps to web apps.
-
Axios: a promise-based HTTP client that works both in the browser and Node.js environment.
-
Vue.js: Vue is a progressive JavaScript frontend framework for building web applications.
Setting up the application
The simplest way to set up a Nest.js application is to install the starter project on GitHub using Git. To do this, let’s run a command that will clone the starter repository into a new project folder named nest-geofencing
on your machine. Open up your terminal or command prompt and run the command below:
$ git clone https://github.com/nestjs/typescript-starter.git nest-geofencing
Go ahead and change directory into the newly created folder and install all the dependencies for the project.
// change directory
cd nest-geofencing
// install dependencies
npm install
Running application
Start the application with:
npm start
The command above will start the application on the default port used by Nest.js. Open your browser and navigate to http://localhost:3000. You should see a page with a welcome message.
Installing server dependencies
Run the command below to install the server dependencies required for this project.
npm install ejs body-parser pusher
-
ejs: this is a simple templating language for generating HTML markup with plain JavaScript.
-
Body-parser: a middleware used for extracting the entire body portion of an incoming request stream and exposing it on
req.body
. -
Pusher: a Node.js client to interact with the Pusher REST API
Google Maps application
To use the Maps JavaScript API, you must register your app project on the Google API Console and get a Google API key which you can add to your app. Follow this quick guide to register your Maps app and get your API credentials.
Setting up a Pusher application
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with, for a better setup experience:
You can retrieve your keys from the App Keys tab:
Configure the entry point of the application
Nest.js uses the Express library and therefore, favors the popular MVC pattern.
To set this up, open up the main.ts
file and update it with the content below:
// ./src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as path from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', __dirname + '/views');
// set ejs as the view engine
app.set('view engine', 'ejs');
await app.listen(3000);
}
bootstrap();
This is the entry point of the application and necessary for bootstrapping Nest.js apps. I have included the Express module, path and set up ejs as the view engine for the application.
Building the homepage
As configured within main.ts
file, the views
folder will hold all the templates for this application. Now let’s go ahead and create it within the src
folder. Once you are done, create a new file named index.ejs
right inside the newly created views
folder and update the content with:
// ./src/views/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<title>Geofencing Nest.js Demo</title>
</head>
<body>
<div id="app">
<div class="row">
<div class="col-md-3">
<div class="user-wrapper">
<h3> <b>Select a user</b> </h3>
<p>Get the current location of a user and others (2km away)</p>
<div v-for="user in users" style="margin: 10px;">
<button class="btn btn-default" v-on:click="getUserLocation(user.position)">{{ user.name}}</button>
</div>
</div>
<div class="load-more">
<button class="btn btn-success" v-on:click="loadMoreUsers"> Load more users </button>
</div>
</div>
<div class="col-md-9" style="background: grey">
<div id="map"></div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_MAP_KEY&libraries=geometry">
</script>
<script src="/main.js"></script>
</body>
</html>
Here, we are simply building a layout for the geofencing application.
First, we included a link to the Bootstrap CDN file to add some default styling and layout to our application. We also added a custom stylesheet for further styling. We will create this stylesheet later in this tutorial. Also included in a <script>
tag just before the page title is a CDN file for Vue.js. This is to ensure that Vue.js is loaded immediately the index.ejs
file is rendered.
Furthermore, we included a button with the caption Load more users. Once this button is clicked we will call a method named loadMoreUsers()
to fetch more random users. This method will be created later in the tutorial.
We included a CDN file each for Axios
and Pusher
. To load the Maps JavaScript API, we included a script
tag and added a URL which links to the location of a JavaScript file that loads all of the symbols and definitions required as the src
.
💡 Note: ensure you replace the
YOUR_GOOGLE_MAP_KEY
string with your actual GoogleMaps API key
Finally, we then proceeded to add a custom script file named main.js
. To set up this file, go ahead and create a public
folder within the src
folder in the application and create the main.js
file within it.
Styling
To set up this stylesheet, locate the public
folder and create a new file named style.css
within it. Next, open the file and paste the code below:
// ./src/public/style.css
html, body {
background-color: #f0f2fa;
font-family: "PT Sans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
color: #555f77;
-webkit-font-smoothing: antialiased;
}
#map {
height: 600px;
width: 100%
}
.user-wrapper {
padding: 20px;
margin: 20px;
}
.load-more {
padding: 20px;
margin: 20px;
}
Building the home route
Nest uses a controller metadata @Controller
to map routes to a specific controller. The starter project already contains a controller by default. We will make use of this in order to render the homepage for this app. Open ./src/app.controller.ts
and edit as shown below:
// ./src/app.controller.ts
import { Get, Controller, Res } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
root(@Res() res) {
res.render('index');
}
}
This controller will ensure that Nest.js maps every /
route to the index.ejs
file.
Creating a Vue instance
Earlier, we created main.js
file within the public
folder and included it on our homepage. We will create Vue instance within this file and bind it to a div
element with an id of #app
. We will also declare an initial value for users
as an empty array inside the data
options:
// ./src/public/main.js
new Vue({
el: '#app',
data: {
users: []
},
...
})
This will get Vue registered to manipulate the DOM in our application.
Create the users controller
To further organize items, we will create a new folder named users
in the src
folder and create a new file called users.controller.ts
within it. Paste following code in the newly created file:
// ./src/users/users.controller.ts
import { Get, Controller, Res, HttpStatus, Body, Post } from '@nestjs/common';
import { UsersService } from 'users/users.service';
@Controller('users')
export class UsersController {
constructor( private userService: UsersService) {}
@Get()
getUser(@Res() res) {
let users = this.userService.getAllUsers();
res.send(users);
}
@Post()
getUsersLocation(@Res() res, @Body() user) {
this.userService.postLocation(user);
res.status(HttpStatus.OK).send("User's location fetched successfully");
}
}
This controller contains two methods:
-
getUser()
: this method will fetch the list of users and send it to the view. -
getUsersLocation()
: this method receives the user object as a form parameter and returns a successful HttpStatus with a success message.
As shown above, we imported UsersService
and injected it into the controller through the constructor. As recommended by Nest, a controller should handle only HTTP requests and abstract any complex logic to a service. We’ll create this service in the next section.
Set up the users service
Within the UsersController
, we imported the UsersService
and used it to fetch all users and also post the location of the selected user. Let’s create this service. Go to the users
folder and create a new file named users.service.ts
within it and then paste the code below into the newly created file:
// ./src/users/users.service.ts
import { Component } from '@nestjs/common';
const people = require('./users');
@Component()
export class UsersService {
getAllUsers(){
return people.map( (person, index) => ({
name: person.name,
position: person.position,
}));
}
intializePusher() {
const Pusher = require('pusher');
const pusher = new Pusher({
appId: 'YOUR_APP_ID',
key: 'YOUR_API_KEY',
secret: 'YOUR_SECRET_KEY',
cluster: 'CLUSTER',
encrypted: true
});
return pusher;
}
postLocation(user) {
const Pusher = require('pusher');
const {lat, lng} = user.position
people.forEach( (person, index) => {
if (person.position.lat === user.position.lat) {
people[index] = { ...person, position: { lat, lng } };
return this.intializePusher().trigger('map-geofencing', 'location', {person: people[index], people})
}
})
}
}
Let’s understand what is happening in this file:
First, we imported the list of users as people
from a file named users.js
. This file holds the list of fake users with a specified location for our application. You can download this list here on GitHub. Once you are done, locate the users
folder and save this file within it as users.js
.
Next, we created a getAllUsers()
method that returns the name and specified positions of users by using the JavaScript map()
method.
We also proceeded to initialize Pusher with the credentials from the dashboard. Don’t forget to replace YOUR_APP_ID
, YOUR_API_KEY
, YOUR_SECRET_KEY
and CLUSTER
with the right credentials obtained from your dashboard.
The postLocation()
method accepts the user object posted from the frontend of the application as a parameter. In addition, it was also used to trigger an event named location
with the selected user and people as a payload on a map-geofencing
channel.
Register the component and the controller
At the moment, our application doesn’t recognize the newly created controller and service. Let’s change this by editing our module file app.module.ts
; putting the controller into the controller
array and service into components
array of the @Module()
decorator respectively.
// ./src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { UsersService } from 'users/users.service';
import { UsersController } from 'users/users.controller';
@Module({
imports: [],
controllers: [AppController, UsersController],
providers: [UsersService],
})
export class AppModule {}
Displaying random users
As mentioned earlier, we will be displaying users from the mock data in our application. Open ./src/public/main.js
file and update it with the code below:
// ./src/public/main.js
new Vue({
el: '#app',
data: {
users: [],
},
mounted() {
this.getUser();
},
methods: {
getUser() {
axios.get('/users').then(response => {
this.users = this.getRandomUsers(response.data, 6)
});
},
getRandomUsers(people, number) {
const selected = [];
for ( var i = 0; i < number; i++) {
const index = Math.floor(Math.random() * people.length);
if (selected.includes(index)) continue;
selected.push(index);
}
const selectedUsers = selected.map(index => {
const users = { name, position } = people[index];
return users;
});
return selectedUsers;
}
}
})
Here, we created a method named getUser()
with the purpose of fetching all users from the backend of our application. Immediately after this, we then proceeded to create a new method called getRandomUsers()
, for getting random users from the response. This method takes in two arguments which are the total number of users returned and the maximum random number of users we wish to display on the homepage of our application.
Restart the development server if it is currently running. Check your page on http://localhost:3000. You should see:
This is what the page will look like at the moment. On page mount, we displayed the list of random users and an empty map. Let’s add functionality to display map.
Getting the location of a user and initializing map
Next, we will display the location of a selected user on the GoogleMap using the coordinate specified for each of the users in ./src/users/users.js
file. Go ahead and open main.js
file and update it with:
// ./src/public/main.js
const USER_MARKER = 'http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1526555652/user_my7yzc.png';
const OFFLINE_MARKER = 'http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1526555651/offline_elrlvi.png';
const ONLINE_MARKER = 'http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1526555651/online_bpf5ch.png'
const RADIUS = 2000;
new Vue({
el: '#app',
data: {
users: [],
},
created() {
let pusher = new Pusher('YOUR_API_KEY', {
cluster: 'CLUSTER',
encrypted: true
});
const channel = pusher.subscribe('map-geofencing');
channel.bind('location', data => {
this.initializeMap(data.person.position, data.people);
});
},
mounted() {
this.getUser();
},
methods: {
getUser() {
...
},
getRandomUsers(people, number) {
...
},
getUserLocation(position) {
const user = { position }
axios.post('/users', user).then(response => {
console.log(response);
})
},
initializeMap(position, people) {
const referencePoint = {lat:position.lat, lng:position.lng};
this.map = new google.maps.Map(document.getElementById('map'), {
center: referencePoint,
zoom: 13
})
for ( var i = 0; i < people.length; i++) {
if (this.withinRegion(referencePoint, people[i], RADIUS)){
this.addMarker(people[i], ONLINE_MARKER);
} else {
this.addMarker(people[i], OFFLINE_MARKER);
}
}
this.addCircle(position);
},
addMarker(props, marker) {
this.marker = new google.maps.Marker({
position: props.position,
map: this.map,
animation: google.maps.Animation.DROP,
icon: marker
})
},
addCircle(position) {
this.circle = new google.maps.Circle({
map: this.map,
center: new google.maps.LatLng(position.lat, position.lng),
radius: 2000,
strokeColor: '#00ff00',
fillColor: "#484040bf",
});
},
withinRegion(position, user, radius) {
const to = new google.maps.LatLng(user.position.lat, user.position.lng);
const from = new google.maps.LatLng(position.lat, position.lng);
const distance = google.maps.geometry.spherical.computeDistanceBetween(from, to);
return distance <= radius;
}
}
})
We added constants for USER_MARKER
, OFFLINE_MARKER
, ONLINE_MARKER
, these markers will be used to indicate the location of a user on the map. The position of a user within a 2km radius from the center of the map will be indicated with ONLINE_MARKER
while others will be indicated with OFFLINE_MARKER
. Also included is a constant for RADIUS
, which represents the distance from the center of the map.
Next, we established a connection to Pusher Channels using the Key
and cluster
obtained from our dashboard. We then proceeded to subscribe to the map-geofencing
channel we created earlier and listened for an event location
. We then passed the payload from the map-geofencing
channel to initializeMap()
method. This is the method responsible for initializing the map. It accepts the position
of the selected user and the list of other users named people
as an argument.
Once any of the random users is selected, we used the method getUserLocation()
to make an HTTP POST request to the /users
endpoint, passing in a user
object which contains the location of the user.
In the initializeMap()
method, we created a variable referencePoint
, which represents the location of the selected user and set it as the center of the map. We then went ahead to instantiate GoogleMaps and attach it to a <div>
HTML element with an id of map
. This is where our map indicating the locations of users will be mounted. Next, we looped through the list of other users and added markers based on the condition used to check if they are within the region 2km away from the location of the selected user at the center of the map.
Finally, we also created three different methods, which are:
-
addMarker()
: this method was used to add markers to the map based on the location of a user. -
addCirlce()
: used to add a circle indicating a 2km radius from the center of the map. -
withinRegion()
: created to check if a user is within the region from the center of the map.
Load more users
Lastly, to load more random users, we will create the loadMoreUsers()
method:
// ./src/public/main.js
...
new Vue({
el: '#app',
data: {
users: [],
},
created() {
...
},
mounted() {
this.getUser();
},
methods: {
// other methods
...
// load more users
loadMoreUsers() {
this.getUser();
}
}
})
This method will call on the getUser()
method and fetch random users.
Final result
Restart the development server if it is currently running. Go ahead and navigate to [http://localhost:3000](http://localhost:3000.)
in your browser to test the application.
Conclusion
We have successfully built a basic realtime application with geofencing updates. We used GoogleMaps API for geolocation and Pusher for the realtime functionality.
With access to the right technology such as the ones used in this tutorial, there is no limit to what you can build. I hope you found this tutorial helpful. Feel free to download the source code here on GitHub.
23 May 2018
by Christian Nwamba