Enhancing Shared Mobility Apps with RabbitMQ

The Rise and Rise of Shared Mobility

A Simple Frontend

yarn add express socket.io leaflet amqplib alertify.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mobility App</title>
<link rel="stylesheet" href="leaflet.css" />
<link rel="stylesheet" href="css/alertify.css" />
<style>
html, body, #map {
height:100%;
width: 100%;
z-index: 0;
}
body{
margin: 0;
padding: 0;
font-size: 14px;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="leaflet.js"></script>
<script src="socket.io.js"></script>
<script src="js/alertify.js"></script>
<script src="app.js"></script>
</body>
</html>
// Start with the Map Themes
function getMapTheme(theme) {
let mapTheme;
// Default Theme
mapTheme = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png';
if ('light_all' === theme) {
mapTheme = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png';
}
if ('dark_all' === theme) {
mapTheme = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png';
}
return mapTheme;
}
// Add Map Attribution
let mapAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
let lighttheme = L.tileLayer(getMapTheme('light_all'), { attribution: mapAttribution });
let darktheme = L.tileLayer(getMapTheme('dark_all'), { attribution: mapAttribution });
// Add Themes to selectable Map Layers
let baseLayers = {
"Light Theme": lighttheme,
"Dark Theme": darktheme
};
// Setup a Riders' Marker Group
let ridersMarkers = L.layerGroup();
let overlayMaps = {
"Riders": ridersMarkers
};
// Initialize and show the map
let map = L.map('map', {
attributionControl: true,
zoom: 16,
layers: [lighttheme]
}).fitWorld();
// Add selectable controls to it
L.control.layers(baseLayers, overlayMaps).addTo(map);
// Create custom marker icons for riders other than myself
let customIcon = L.Icon.extend({
options: {
shadowUrl: "/img/marker-shadow.png",
iconSize: [25, 39],
iconAnchor: [12, 36],
shadowSize: [41, 41],
shadowAnchor: [12, 38],
popupAnchor: [0, -30]
}
});
let yellowIcon = new customIcon({ iconUrl: "/img/marker-yellow.png" });
// Function to add these custom markers
function setMarker(data) {
for (i = 0; i < data.coords.length; i++) {
// let marker = L.marker([data.coords[i].lat, data.coords[i].lng], { icon: yellowIcon }).addTo(map);
let marker = L.marker([data.coords[i].lat, data.coords[i].lng], { icon: yellowIcon });
marker.bindPopup("A ride is here!");
// add marker
ridersMarkers.addLayer(marker);
alertify.success("A nearby rider ID " + data.id + " has just connected!");
}
}
// Socket IO Client Initialization
let connects = {};
let socket = io('http://127.0.0.1:80');
socket.on('receive', function(data) {
alertify.log("New Socket Event Received");
if (!(data.id in connects)) {
setMarker(data);
}
connects[data.id] = data;
});
// placeholders for the L.marker and L.circle representing user's current position and accuracy
let current_position, current_accuracy;
function onLocationFound(e) {
// if position defined, then remove the existing position marker and accuracy circle from the map
if (current_position) {
map.removeLayer(current_position);
map.removeLayer(current_accuracy);
}
let radius = e.accuracy / 2; current_position = L.marker(e.latlng)
.addTo(map)
.bindPopup('Your current position and a ' + radius + ' meters radius').openPopup();
current_accuracy = L.circle(e.latlng, radius).addTo(map); let data = {
id: userId,
coords: [{
lat: e.latitude,
lng: e.longitude,
acr: e.accuracy
}]
}
socket.emit("send", data);
}
let errors = {
1: "Geolocation Permission Denied",
2: "Network Error",
3: "Connection Timeout"
};
function onLocationError(error) {
// confirm dialog
alertify.confirm("We could not get your current location, reason : " + errors[error.code] + "! Would you like to reload the page and try again?", function () {
// user clicked "ok"
location.reload();
}, function() {
// user clicked "cancel"
alertify.log("Please Note You Might Be Getting Stale Data!");
});
console.log(
'code: ' + error.code + '\n' +
'message: ' + error.message + '\n',
'Geo-Location Error'
);
}
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
map.on('moveend', function(e) {
let bounds = map.getBounds();
let sw = bounds.getSouthWest();
let ne = bounds.getNorthEast();
let sw_latitude = sw.lat;
let sw_longitude = sw.lng;
let ne_latitude = ne.lat;
let ne_longitude = ne.lng;
let zoom = map.getZoom(); if( zoom < 16 ){
// remove all the current layers
ridersMarkers.clearLayers();
// Show Toast to zoom in
alertify.log("Please zoom in to view nearby riders");
} else {
// To Do - Query DB for nearby rides
}
});
map.stopLocate();// Start Mobility App function
let startApp = function (){
// check whether browser supports geolocation api
if (navigator.geolocation) {
map.locate({
watch: true,
setView: true,
maxZoom: 16,
timeout: 60000,
maximumAge: 60000,
enableHighAccuracy: false
});
} else {
alertify.alert("Sorry, your browser does not support geolocation!");
}
}
// Simulate Rider Authorization by asking for hypothetical ID
let userId;
let defaultUserId = "9999";
alertify
.defaultValue("9999")
.prompt("Please Enter A Hypothetical User ID : ",
function (val, ev) {
ev.preventDefault();
alertify.success("You've clicked OK and typed: " + val);
userId = val;
startApp();
}, function(ev) {
ev.preventDefault();
alertify.error("You've clicked Cancel, default ID 9999 used");
userId = defaultUserId;
startApp();
}
);
let express = require('express')
let amqplib = require('amqplib')
let app = express()
let server = require('http').createServer(app)
let io = require('socket.io')(server)
app.use(express.static('public'))
app.use(express.static('node_modules/leaflet/dist'))
app.use(express.static('node_modules/alertify.js/dist'))
app.use(express.static('node_modules/socket.io-client/dist'))
let open = amqplib.connect('amqp://localhost')// Create the RabbitMQ Exchange, Queues and Bindings
open.then(function(conn) {
return conn.createChannel()
}).then(function(ch) {
return ch.assertExchange('geolocation-exchange', 'fanout').then(function(geolocationExchange) {
ch.assertQueue('analytics-queue').then(function(analyticsQueue) {
ch.bindQueue(analyticsQueue.queue, geolocationExchange.exchange)
})
ch.assertQueue('map-queue').then(function(mapQueue) {
ch.bindQueue(mapQueue.queue, geolocationExchange.exchange)
})
ch.assertQueue('redis-queue').then(function(redisQueue) {
ch.bindQueue(redisQueue.queue, geolocationExchange.exchange)
})
})
}).catch(console.warn)
io.sockets.on('connection', function (socket) {
socket.on('send', function (data) {
socket.broadcast.emit('send', data);
// Send to data to RabbitMQ
open.then(function(conn) {
return conn.createChannel()
}).then(function(ch) {
return ch.assertExchange('geolocation-exchange', 'fanout').then(function(geolocationExchange) {
ch.publish(geolocationExchange.exchange, '', Buffer.from(JSON.stringify(data)))
})
}).catch(console.warn)
})
socket.on('receive', function (data) {
socket.broadcast.emit('receive', data);
})
})
server.listen(80, '127.0.0.1', function() {
console.log('Mobility App running on Localhost')
})
let amqplib = require('amqplib')
let io = require('socket.io-client')
let socket = io.connect('http://127.0.0.1:80', { reconnect: true });
let queue = 'map-queue'
let open = amqplib.connect('amqp://localhost')
// Update Client Map Consumer
open.then(function(conn) {
return conn.createChannel();
}).then(function(ch) {
return ch.assertQueue(queue).then(function() {
return ch.consume(queue, function(msg) {
if (msg !== null) {
console.log(msg.content.toString());
// Tell socket.io server to update clients with new geolocation details
let data = msg.content.toString()
socket.emit('receive', JSON.parse(data));
ch.ack(msg);
}
});
});
}).catch(console.warn);
let amqplib = require('amqplib')
let queue = 'analytics-queue'
let open = amqplib.connect('amqp://localhost')
// Log & Analytics Consumer
open.then(function(conn) {
return conn.createChannel();
}).then(function(ch) {
return ch.assertQueue(queue).then(function(ok) {
return ch.consume(queue, function(msg) {
if (msg !== null) {
console.log(msg.content.toString());
// To Do - Persist new Geolocation coordinates in Analytics store like Elasticsearch
ch.ack(msg);
}
});
});
}).catch(console.warn);
let amqplib = require('amqplib')
let queue = 'redis-queue'
let open = amqplib.connect('amqp://localhost')
// Key-Value Store Consumer
open.then(function(conn) {
return conn.createChannel();
}).then(function(ch) {
return ch.assertQueue(queue).then(function(ok) {
return ch.consume(queue, function(msg) {
if (msg !== null) {
console.log(msg.content.toString());
// To Do - Persist new Geolocation Key-Value store like Redis
ch.ack(msg);
}
});
});
}).catch(console.warn);

Running the Demo

node server.jsnode consumers\map.jsnode consumers\analytics.jsnode consumers\redis.js
var data = {
id: 5678,
coords: [{
lat: -1.2355076,
lng: 36.8850185,
acr: 30
}]
}
socket.emit("send", data);

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store