Real-Time Tracking using — Node.js, WebSockets, Redis and Open Layers

Senior Principal Engineer

In this article, I’m going to write a very basic Real-Time Tracking application based on Open Layers, Redis, WebSocket and Node.js.

What are we building?
The use case revolves around managing rides (preconfigured routes for cabs/buses) and providing ride subscribers real time visibility to the current location of the rides on a map.

Note on Architecture and Technology Choices
The idea is to use Open Layers to display a map and show the current location of the ride on the map. The vehicle that serves the route would send back its location back to the servers, The backend servers perform the logic and calculations before publishing the current location back to the clients for the client to update the map display. Client could be web pages which is used by operations , Android or IPhone Application used by subscribers. The scope for this article is to show on web pages and in subsequent posts do similarly for Native Mobile Apps.

Node.js and WebSocket are becoming the de facto standard for developing non-blocking, event-driven servers, due to its single-threaded nature. It’s used for traditional web sites and back-end API services, but was designed with real-time, push-based architectures in mind.

Redis A backend Publish-Subscribe mechanism is imperative to scaling WebSocket architectures. For this article, I am using Redis as a pub-sub provider. It is also possible to use other messaging infrastructures like RabbitMQ, Kafka etc.

Project Setup

Install a redis cluster, acquire the credential is and start the Redis server etc.I had installed a redis server locally, but the reader could use a Managed Redis Instance. There are many like AWS Redis, Scalegrid etc.

Install Node and NPM and create a directory to hold the source code and serve it using Node.js. Perform an NPM Init inside the directory. A Package.json file should be created and its content may look like below

{
"name": "realtimetracking",
"version": "0.0.1",
"description": "A realtime GPS tracking using redis, nodejs and websockets",
"dependencies": {
"body-parser": "^1.15.2",
"express": "^4.10.2",
"redis": "^2.6.3",
"socket.io": "^1.7.1"
},
"main": "index.js",
"author": ""
}

Please note that we are using express framework for developing our app, and Socket.io provides that WebSocket implementation that will be required later in this article.

npm install --save // This command should resolve all the dependencies

create a views directory and create a file called index.html. Create some test content in index.html, we will update as needed later. Further, create a public directory with css , js and image subdirectories to serve client-side css, javascript files and images.

Create an index.js file in the directory

var express = require('express');
var bodyParser = require('body-parser');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);var port = process.env.PORT || 3000;

// Start the Server
http.listen(port, function () {
console.log('Server Started. Listening on *:' + port);
});


// Express Middleware
app.use(express.static('public'));
app.use(bodyParser.urlencoded({
extended: true
}));

// Render Main HTML file
app.get('/', function (req, res) {
res.sendFile('views/index.html', {
root: __dirname
});
});

At this point start the server with node index.js command to start the server and hit use the browser to ensure the contents of the index.html is served when http://{host}:3000/ is accessed from a browser.

Displaying the Open Layers map with a marker.

Update index.html to include appropriate CSS and JS files

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node.js + Socket.io + Redis + OpenLayers + Real Time Tracking </title>
<link rel="stylesheet" href="https://openlayers.org/en/v5.3.0/css/ol.css" type="text/css">

<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<h1>Node.js + Socket.io + Redis + OpenLayers + Real Time Tracking </h1>

<div id="map">
<!-- Your map will be shown inside this div-->
</div>

</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://openlayers.org/en/v5.3.0/build/ol.js"></script>
<script type="text/javascript" src="js/main.js"></script>

</body>
</html>

The bolded portion in the above code indicate the various css and js files. Our Maps will be displayed inside the div tag with id as “map”.

Note that we have included a main.js file , this needs to be created in the public/js folder this file would contain the code required to display the Open Layers Map.

/Base Layer with Open Street Maps
var baseMapLayer = new ol.layer.Tile({
source: new ol.source.OSM()

});

//Construct the Map Object
var map = new ol.Map({
target: 'map',
layers: [ baseMapLayer],
view: new ol.View({
center: ol.proj.fromLonLat([80.2459,12.9860]),
zoom: 15 //Initial Zoom Level
})
});

//Set up an Style for the marker note the image used for marker
var iconStyle = new ol.style.Style({
image: new ol.style.Icon(/** @type {module:ol/style/Icon~Options} */ ({
anchor: [0.5, 16],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: 'image/icon.png'
}))
});


//Adding a marker on the map
var marker = new ol.Feature({
geometry: new ol.geom.Point(
ol.proj.fromLonLat([80.24586,12.9859])
)
});

marker.setStyle(iconStyle);

var vectorSource = new ol.source.Vector({
features: [marker]
});


var markerVectorLayer = new ol.layer.Vector({
source: vectorSource,

});

// add style to Vector layer style map
map.addLayer(markerVectorLayer);

With the above update, the web page should display a map with a pointer at a location I have chosen. (Hard Coded, but this can be dynamic as well).

Making the Marker Move from the client-side.

function updateCoordinate(item) { 
// Structure of the input Item
// {"Coordinate":{"Longitude":80.2244,"Latitude":12.97784}} var featureToUpdate = marker;

var coord = ol.proj.fromLonLat([item.Coordinate.Longitude, item.Coordinate.Latitude]);

featureToUpdate.getGeometry().setCoordinates(coord);
}

Include the above function at the bottom of main.js which updates the co-ordinates of the marker. The below piece of code helps to quickly test the marker movement on the client-side.

var longlats =
[[80.24586,12.98598],
[80.24537,12.98597],
[80.24522,12.98596],
[80.24522,12.98614],
[80.24523,12.98626]]var count = 1;
var item = {};
item.id = marker.getId;
item.Coordinate = {};
setInterval(function() {
item.Coordinate.Longitude = longlats[count][0];
item.Coordinate.Latitude = longlats[count][1];
count++;
updateCoordinate(item);
}, 5000);

Publishing the Last Known Location

In the real world, the location of the subject would be published by a device that is the subject. In our case, we simulate the same by developing a page that takes a co-ordinate from the longlats lists and send it through web sockets to our back end.

//Serve a Publisher HTML
app.get('/publish', function (req, res) {
res.sendFile('views/publisher.html', {
root: __dirname
});
});

Publisher HTML — create any html file and ensure to include the below scripts

//
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.slim.js"></script>
<script type="text/javascript" src="js/publish.js" type="text/javascript"></script>

publish.js would contain similar script code like our test script described above.

var longlats =
[[80.24586,12.98598],
[80.24537,12.98597],
[80.24522,12.98596],
[80.24522,12.98614],
[80.24523,12.98626]];const socket = io({ transports: ['websocket'] });var count = 1;

setInterval(function() {
console.log(count);
if (count < 10000){
var item = {};
item.Coordinate = {};
item.Coordinate.Longitude = longlats[count][0];
item.Coordinate.Latitude = longlats[count][1];
count++;
socket.emit('lastKnownLocation', item);
}
}, 5000);

Set up the server code to receive this location. The highlighted pieces of code demonstrate how to receive the data from the client on the server. Note the specific Line redisPublisher.publish, the explanation is in the next section.

io.on('connection', function (socket) {
console.log('socket created');

let previousId;
const safeJoin = currentId => {
socket.leave(previousId);
socket.join(currentId);
previousId = currentId;
};

socket.on('disconnect', function() {
console.log('Got disconnect!');
});

socket.on('lastKnownLocation', function (data) {
var location = JSON.stringify(data);
redisPublisher.publish('locationUpdate', location);
});


});

Publishing the last known location to subscribers for the clients to update the map

On index.js set up connections to redis like below. I use 2 connections one for subscribing from redis and one for publishing to redis. I believe a single connection may suffice as well. Also, note that when the server receives a location update the data is published to the redis channel ‘locationUpdate’. Further, when a message is received from redis, the same is sent back to all websocket clients using the bolded code below.

var redis = require('redis');
var redisSubscriber = redis.createClient();
var redisPublisher = redis.createClient();redisSubscriber.on('subscribe', function (channel, count) {
console.log('client subscribed to ' + channel + ', ' + count + ' total subscriptions');
});

redisSubscriber.on('message', function (channel, message) {
console.log('client channel ' + channel + ': ' + message);
io.emit('locationUpdate', message);
});

Also notice that earlier when we served the maps content we just served back the index.html contents. This piece could be updated to start subscrbing for ‘locationUpdate’ channel like below.

app.get('/', function (req, res) {
redisSubscriber.subscribe('locationUpdate');
res.sendFile('views/index.html', {
root: __dirname
});
});

Now , load the Main Page and the Publisher page, and see the marker move as there are the new location publishes.

Conclusion

That concludes our basic real-time tracking app. There are many cases that needs to be handled to make it production ready like, what should be done when the system receives some weird location, what should be done when the marker moves out of the visible div area, how to initialise the location of the centre of the map based on route information, how to handle security, how to set up web sockets across fire walls and load balancers. but this should provide the basic set up required for developers to get started on such an app.

Related Blog posts