Build a geofencing web app using Angular
You will need Node and npm installed on your machine.
A geo-fence is a virtual perimeter for a real-world geographic area. Geofencing is the use of GPS or RFID technology to create a virtual geographic boundary, enabling software to trigger a response when a mobile device enters or leaves a particular area.
To follow this tutorial a basic understanding of Angular and Node.js is required. Please ensure that you have Node and npm installed before you begin.
If you have no prior knowledge of Angular, kindly follow the tutorial here. Come back and finish the tutorial when you’re done.
We’ll be using these tools to build our application:
We’ll be creating an application for a fictional ranch called “The Ranch”. Our application will allow “The Ranch” employees track the location of each active guest in realtime. The application will alert employees when an active guest is exiting the boundaries of the ranch.
Here’s a screenshot of the final product:
Initializing the application and installing dependencies
To get started, we will use the CLI (command line interface) provided by the Angular team to initialize our project.
First, install the CLI by running npm install -g @angular/cli
. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed.
To create a new Angular project using the CLI, open a terminal and run:
ng new pusher-geofencing --style=scss
--``routing
This command is used to initialize a new Angular project with routing setup; the project will make use of SCSS for styling.
Next run the following command in the root folder of the project to install dependencies.
// install depencies required to build the server
npm install express body-parser dotenv pusher
// front-end dependencies
npm install pusher-js
Start the Angular development server by running ng serve
in a terminal in the root folder of your project.
Building our server
We’ll build our server using Express. Express is a fast, unopinionated, minimalist web framework for Node.js.
Create a file called server.js
in the root of the project and update it with the code snippet below
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const app = express();
const port = process.env.PORT || 4000;
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: 'eu',
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
The calls to our endpoint will be coming in from a different origin. Therefore, we need to make sure we include the CORS headers (Access-Control-Allow-Origin
). If you are unfamiliar with the concept of CORS headers, you can find more information here.
This is a standard Node application configuration, nothing specific to our app.
Create a Pusher account and a new Pusher Channels app if you haven’t done so yet and get your appId
, key
and secret
.
Create a file in the root folder of the project and name it .env
. Copy the following snippet into the .env
file and ensure to replace the placeholder values with your Pusher credentials.
// .env
// Replace the placeholder values with your actual pusher credentials
PUSHER_APP_ID=PUSHER_APP_ID
PUSHER_KEY=PUSHER_KEY
PUSHER_SECRET=PUSHER_SECRET
We’ll make use of the dotenv
library to load the variables contained in the .env
file into the Node environment. The dotenv
library should be initialized as early as possible in the application.
Send votes
Let’s create a post route ping
, the frontend of the application will send make a request to this route containing the current location of the user. This will be done whenever there’s a location change.
// server.js
require('dotenv').config();
...
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
...
});
app.post('/ping', (req, res) => {
const { lat, lng } = req.body;
const data = {
lat,
lng,
};
pusher.trigger('location', 'ping', data);
res.json(data);
});
...
- Using object destructuring, we got the
lat
andlng
from the body of the request. - The
data
object contains the coordinates sent in. This object will be sent as the data for the triggered Pusher event. The same object will be sent as a response to the user. - The trigger is achieved using the
trigger
method which takes the trigger identifier(location
), an event name (ping
), and a payload.
Home view
Run ng generate component home
to create the home component. This component will be the view users see when they visit. It will request permission to get the user’s current location.
Open the home.component.html
file and replace it with the content below.
// home.component.html
<app-header [username]="username"></app-header>
<div class="content">
<h2>Welcome to "The Ranch"</h2>
<img src="/assets/placeholder.svg" alt="">
<h6>Enable location to get updates</h6>
</div>
Note: all assets used in the article are available in the GitHub repo
The view itself is static. There won’t be a lot happening in this particular view except the request to get the user’s current location. We referenced a header
component in the markup. The component was created because the same header will be reused in the admin page. We’ll create the component shortly.
Styling
Copy the following styles into the home.component.scss
file.
// home.component.scss
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 0;
img {
height: 100px;
}
h6 {
margin:15px 0;
opacity: 0.6;
}
}
Home component
Here, we’ll define methods to get the user’s location and sending the location to the server.
// home.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
constructor(private http: HttpClient) {}
username = 'J. User'
pingServer(location) {
this.http
.post('http://localhost:4000/ping', location)
.subscribe((res) => {});
}
ngOnInit() {
if ('geolocation' in navigator) {
navigator.geolocation.watchPosition((position) => {
this.pingServer({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
});
}
}
}
pingServer
: this method makes use of the native HttpClient service to make requests to our server. It takes alocation
parameter and sends it as the body of the request.- In the
OnInit
lifecycle, we check if the current browser supports the geolocation API; we watch for location changes and send the
To make use of the HttpClient service, we’ll need to import the HttpClientModule
into the app.module.ts
file. Update your app module file as follows:
// app.module.ts
...
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
...
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
...
})
export class AppModule { }
...
Let’s create the header component by running ng generate component header
in a terminal in the root folder of the project.
Replace the contents of the header.component.html
with the following:
<!-- header.component.html -->
<header>
<div class="brand">
<h5>The Ranch</h5>
</div>
<div class="nav">
<ul>
<li>
<img src="/assets/boy.svg" alt="avatar">
<span>{{username}}</span>
</li>
</ul>
</div>
</header>
Note: all assets used in this article are available in the repo
Update the home.component.scss
file with the following styles:
// header.component.scss
header {
display: flex;
background: mediumseagreen;
margin: 0;
padding: 5px 40px;
color: whitesmoke;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
.brand {
flex: 1;
display: flex;
align-items: center;
h5 {
font-family: 'Lobster', cursive;
font-size: 20px;
margin: 0;
letter-spacing: 1px;
}
}
ul {
list-style: none;
padding-left: 0;
display: flex;
li {
display: flex;
align-items: center;
img {
height: 40px;
border-radius: 50%;
}
span {
margin-left: 8px;
font-size: 15px;
font-weight: 500;
}
}
}
}
The header.component.ts
file should be updated to look like the snippet below:
// header.component.ts
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
constructor() {}
@Input() username = '';
ngOnInit() {}
}
We’ll be using external fonts in our application. Include a link to the stylesheet in the index.html
file.
// index.html
<head>
...
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Dosis:400,500,700|Lobster" rel="stylesheet">
...
</head>
...
Update the styles.scss
file to use the external fonts throughout the project.
// styles.scss
body{
margin: 0;
font-family: 'Dosis', sans-serif;
}
Introducing Pusher
To make the pusher library available in our project, add the library as a third party script to be loaded by Angular CLI. All CLI config is stored in .angular-cli.json
file. Modify the scripts
property to include the link to pusher.min.js
.
// .angular-cli.json
...
"scripts": [
"../node_modules/pusher-js/dist/web/pusher.min.js",
]
...
After updating this file, you’ll need to restart the angular server so the CLI compiles the new script file added.
Create a Pusher service using the Angular CLI by running the following command:
ng generate service pusher
This command simply tells the CLI to generate a service named pusher
. Now open the pusher.service.ts
file and update it with the code below.
// pusher.service.ts
import { Injectable } from '@angular/core';
declare const Pusher: any;
@Injectable()
export class PusherService {
constructor() {
const pusher = new Pusher('PUSHER_KEY', {
cluster: 'eu',
});
this.channel = pusher.subscribe('location');
}
channel;
public init() {
return this.channel;
}
}
- First, we initialize Pusher in the constructor.
- The
init
method returns the Pusher property we created.
Note: ensure you replace the
PUSHER_KEY
string with your actual Pusher key.
To make the service available application wide, import it into the app.module.ts
file.
// app.module.ts
...
import { HttpClientModule } from '@angular/common/http';
import {PusherService} from './pusher.service';
@NgModule({
....
providers: [PusherService],
....
})
...
Admin page
To monitor and track users using our application, we’ll need an admin page accessible to privileged employees. The page will use Google Maps to visualize the location of the user. Using Pusher, changes in the user’s location will be seen in realtime.
We’ll be using angular-google-maps, which has a set of reusable Angular components for Google Maps. Install the package by running npm install @agm/core
.
To use the components in our project, we’ll need to include the angular-google-maps’ module in the app.module.ts
file.
//app.module.ts
...
import {PusherService} from './pusher.service';
import { AgmCoreModule } from '@agm/core';
@NgModule({
...
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
AgmCoreModule.forRoot({
// please get your own API key here: https://developers.google.com/maps/documentation/javascript/get-api-key?hl=en
apiKey: 'GOOGLE_API_KEY',
libraries: ['geometry']
}),
...
})
export class AppModule { }
Note: ensure to replace the placeholder value with your google API key
Now we’ll create the admin component using the CLI by running the following command:
ng generate component admin
Open the admin.component.html
file and update it with the contents below:
// admin.component.html
<app-header [username]="username"></app-header>
<div class="main">
<h3>Admin</h3>
<agm-map [latitude]="center.lat" [longitude]="center.lng" [zoom]="zoom">
<agm-marker [latitude]="center.lat" [longitude]="center.lng"></agm-marker>
</agm-map>
<h4>Location Alerts</h4>
<div class="alert" [hidden]="!showAlert">
<p>This user has left the ranch</p>
</div>
<div class="location alert" [hidden]="!showLocationUpdate">
<p>{{message}}</p>
</div>
</div>
Style up the component by adding the following styles to the admin.component.scss
file:
// admin.component.scss
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: auto;
h3 {
font-size: 15px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 15px;
}
.alert {
background: #f14343;
color: white;
padding: 15px;
border-radius: 5px;
p{
margin: 0;
}
}
.location{
background: green;
margin-top: 20px;
}
}
agm-map {
height: 400px;
width: 600px;
}
Add the following content to the admin``.component.ts
file:
// admin.component.ts
import { Component, OnInit } from '@angular/core';
import { MapsAPILoader } from '@agm/core';
import { PusherService } from '../pusher.service';
declare const google;
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss'],
})
export class AdminComponent implements OnInit {
constructor(private loader: MapsAPILoader, private pusher: PusherService) {}
theRanchPolygon;
username = 'J. Admin';
message = '';
showAlert = false;
showLocationUpdate = false;
zoom = 15;
// Center of the ranch, where the initial marker will be placed
center = {
lat: 6.435838,
lng: 3.451384,
};
// This array of latLngs represents the polygon around our ranch
polygon = [
{ lat: 6.436914, lng: 3.451432 },
{ lat: 6.436019, lng: 3.450917 },
{ lat: 6.436584, lng: 3.450917 },
{ lat: 6.435006, lng: 3.450928 },
{ lat: 6.434953, lng: 3.451808 },
{ lat: 6.435251, lng: 3.451765 },
{ lat: 6.435262, lng: 3.451969 },
{ lat: 6.435518, lng: 3.451958 },
];
ngOnInit() {
// Wait for the google maps script to be loaded before using the "google" keyword
this.loader.load().then(() => {
this.theRanchPolygon = new google.maps.Polygon({ paths: this.polygon });
});
const channel = this.pusher.init();
channel.bind('ping', (position) => {
this.center = {
...position,
};
// Create a LatLng using the position returned from the pusher event
const latLng = new google.maps.LatLng(position);
this.showLocationUpdate = true;
this.message = "The user's location has changed";
// Check if the location is outside the polygon
if (!google.maps.geometry.poly.containsLocation(latLng, this.theRanchPolygon)) {
// Show alert if user has left the polygon
this.showAlert = true;
}else{
this.message = 'The user is currently in the ranch';
}
});
}
}
polygon
: this is an array of latLngs that represent the polygon around our ranchMapsApiLoader
: this is a service that provides a method to check if the Google maps script has been loaded.
In the OnInit
lifecycle we do a few things:
- We wait for the Google maps script to load; in the promise returned, we create a polygon using the array of LatLng objects.
- We initialized Pusher and listened for the
ping
event. In the bind callback, we set thecenter
property to the position sent through the event. - Create a LatLng using the position returned from the event.
- Finally, we checked if the position is outside the polygon and then we display an alert if it is.
Now that both pages have been created, let’s set up routes for each page. Open the app-routing.module.ts
file and add routes to the routes
array.
// app-routing.module.ts
...
import { HomeComponent } from './home/home.component';
import { AdminComponent } from './admin/admin.component';
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
},
{
path: '',
component: HomeComponent,
},
];
...
Now update the app.component.html
file to contain just the route-outlet
// app.component.html
<router-outlet></router-outlet>
At this point, your application should have realtime updates when there’s a location change. The admin user’s should we alerted if the user’s current location is outside “The Ranch” premises.
Navigate to http://localhost:4000 to view the home page and http://localhost:4000/admin to view the admin page.
The home page:
The different states of the admin page:
To test the realtime functionality of the application, open two browsers side-by-side and engage the application. Location updates should be in realtime.
Conclusion
Using Pusher, we’ve built out an application using the pub/sub pattern to receive realtime updates. Using geofences, we’ll be able to tell when an active guest is leaving “The Ranch”. You can check out the repo containing the demo on GitHub.
15 May 2018
by Christian Nwamba