How to Build your Own Uber-for-X Application — Part 2

Ashwin Hariharan
Jan 29, 2017 · 20 min read
  • Using maps to show location details of the citizen and the cop
  • Visualizing crime data

Project Set-up and folder organization

  • routes.js — you’ll use this file to write end-points and handlers
  • db-operations — where you’ll write database operations
  • views will contain your HTML pages
  • public will contain sub-folders for storing JavaScripts, stylesheets and images
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset="utf-8"/>
<title>Citizen <%= userId %> </title>
</head>
<body data-userId="<%= userId %>">
<h1>Hello Citizen <%= userId %></h1>
<h4 id="notification">
<!-- Some info will be displayed here-->
</h4>
<div id="map">
<!-- We will load a map here later-->
</div>
<!--Load JavaScripts -->
</body>
</html>
app.set('views', 'views'); 
app.use(express.static('./public'));
app.set('view engine','html');
app.engine('html',consolidate.underscore);
  • It’ll be helpful if you keep the developer console open in your browser to check for error messages in case something doesn’t seem to be working. Keep a watch on the terminal output too.
  • The words event and signal will be used interchangeably in this tutorial — both mean the same thing.

Serving Citizen and Cop Pages

Let’s render the citizen page on going to http://localhost:8000/citizen.html, and the cop page on going to http://localhost:8000/cop.html. To do this, open app.js and add these lines inside the callback function of mongoClient.connect:

app.get('/citizen.html', function(req, res){
res.render('citizen.html',{
userId: req.query.userId
});
});
app.get('/cop.html', function(req, res){
res.render('cop.html', {
userId: req.query.userId
});
});

Why do you need web sockets, and how do they work?

Event or signal based communication has always been an intuitive way to pass messages ever since historic times. The earliest techniques were quite rudimentary — like using fire signals for various purposes, mostly to warn of danger to people.

But why not simply use HTTP requests?

I read a very nice article on the difference between HTTP requests and web-sockets. It’s a short one, so you can read it to understand the concept of web-sockets better.

Integrating Socket.IO

Let’s start by integrating Socket.io with the express server and also load socket.io’s client-side library in the html pages. You’ll also use jQuery — it isn’t needed for socket.io to work, but your app will need it for making AJAX requests and tons of other stuff. So go ahead, write this in both the pages:

<!-- Load socket.io client library -->
<script src="/socket.io/socket.io.js"></script>
<!-- Load JQuery from a CDN -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<!-- load libraries before your JS code
Write rest of your JS code here -->
<script type="text/javascript">
var socket = io();
//Fetch userId from the data-atribute of the body tag
var userId = document.body.getAttribute("data-userId");
/*Fire a 'join' event and send your userId to the server, to join a room - room-name will be the userId itself!
*/
socket.emit('join', {userId: userId});
//Declare variables, this will be used later
var requestDetails = {};
var copDetails = {};
var map, marker;
</script>

Citizen-cop communication:

The entire process can be broadly divided into two sets of features:

  1. Accepting the request and notifying the citizen

Requesting for help and notifying nearby cops:

// GET request to '/cops/info?userId=02'
app.get('/cops/info', function(req, res){
var userId = req.query.userId //extract userId from query params
dbOperations.fetchCopDetails(db, userId, function(results){
res.json({
copDetails: results //return results to client
});
});
});
function fetchCopDetails(db, userId, callback) {
db.collection("policeData").findOne({
userId: userId
}, function(err, results) {
if (err) {
console.log(err);
} else {
callback({
copId: results.userId,
displayName: results.displayName,
phone: results.phone,
location: results.location
});
}
});
}
exports.fetchCopDetails = fetchCopDetails;
//First send a GET request using JQuery AJAX and get the cop's details and save it
$.ajax({
url: "/cops/info?userId="+userId,
type: "GET",
dataType: "json",
success: function(data){ //Once response is successful
copDetails = data.copDetails; //Save the cop details
copDetails.location = {
address: copDetails.location.address,
longitude: copDetails.location.coordinates[0],
latitude: copDetails.location.coordinates[1]
};
document.getElementById("copDetails").innerHTML = JSON.stringify(data.copDetails);
},
error: function(httpRequest, status, error){
console.log(error);
}
});
//Listen for a "request-for-help" event
socket.on("request-for-help", function(eventData){
//Once request is received, do this:
//Save request details
requestDetails = eventData; //Contains info of citizen
//display the data received from the event
document.getElementById("notification").innerHTML = "Someone's being attacked by a wildling! \n" + JSON.stringify(requestDetails);
});

Inside citizen.html

The next step is to create a button for the citizen that can be clicked in case of emergency. Once clicked, it will fire a request-for-help signal and the signal can carry back information of the citizen back to the server:

<button onclick="requestForHelp()">
Request for help
</button>
//Citizen's info
requestDetails = {
citizenId: userId,
location: {
address: "Indiranagar, Bengaluru, Karnataka 560038, India",
latitude: 12.9718915,
longitude: 77.64115449999997
}
}
//When button is clicked, fire request-for-help and send citizen's userId and location
function requestForHelp(){
socket.emit("request-for-help", requestDetails);
}
//Saves details like citizen’s location, time
function saveRequest(db, issueId, requestTime, location, citizenId, status, callback){
db.collection('requestsData').insert({
"_id": issueId,
"requestTime": requestTime,
"location": location,
"citizenId": citizenId,
"status": status
}, function(err, results){
if(err) {
console.log(err);
}else{
callback(results);
}
});
}
exports.saveRequest = saveRequest;

Accepting the request and notifying the citizen

Inside cop.html

The cop should be able to click a button to inform the citizen that the request has been accepted. When clicked, this button will fire a request-accepted event and also send back the cop’s info to the server:

<button onclick="helpCitizen()">
Help Citizen
</button>
function helpCitizen(){
//Fire a "request-accepted" event/signal and send relevant info back to server
socket.emit("request-accepted", {
requestDetails: requestDetails,
copDetails: copDetails
});
}

Inside citizen.html

The citizen page will start listening to any request-accepted events from the server. Once it receives the signal, you can display the cop info inside an empty div:

//Listen for a "request-accepted" event
socket.on("request-accepted", function(eventData){
copDetails = data; //Save cop details
//Display Cop details
document.getElementById("notification").innerHTML = "A cop is coming to your rescue! \n" + JSON.stringify(copDetails);
});
function updateRequest(db, requestId, copId, status, callback) {
db.collection('requestsData').update({
"_id": requestId //Perform update for the given requestId
}, {
$set: {
"status": status, //Update status to 'engaged'
"copId": copId //save cop's userId
}
}, function(err, results) {
if (err) {
console.log(err);
} else {
callback("Issue updated")
}
});
}
exports.updateRequest = updateRequest;
//Listen to a 'request-accepted' event from connected cops
socket.on('request-accepted', function(eventData){

//Convert string to MongoDb's ObjectId data-type
var ObjectID = require('mongodb').ObjectID;
var requestId = new ObjectID(eventData.requestDetails.requestId);
//For the request with requestId, update request details
dbOperations.updateRequest(db, requestId, eventData.copDetails.copId, 'engaged’, function(results){
//Fire a 'request-accepted' event to the citizen and send cop details
io.sockets.in(eventData.requestDetails.citizenId).emit('request-accepted', eventData.copDetails);
});

});

What’s next?

By now it might have become obvious to you — the citizen page sends a hard-coded value of location every-time the button for help is clicked. Similarly the location info for all your sample cops have already been fed into the database earlier and are fixed values.

Enter Maps

There are lot of mapping options out there. Google Maps API are very robust and feature rich. I personally love Mapbox too, it uses OpenStreetMap protocols under the hood, and here is the best part — it’s open source and hugely customizable! So let’s use that for building the rest of your app.

Using Mapbox API

  • In order to begin using these APIs, you need to first create an account on MapBox and get the authentication key here.
    Depending on your needs, Mapbox offers different pricing plans to use these APIs in your apps — for now the free starter plan is sufficient.
  • Next, you’ll load mapbox.js library (current version 2.4.0) in both the pages using a script tag. It’s built on top of Leaflet (another JavaScript library).
<script src="https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js"></script>
<link href="https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css" rel="stylesheet" />
  • Display a marker on the map
  • Use the autocomplete feature offered by Mapbox geocoder api. This allows you to input for a place and choose from the autocomplete suggestions.
    After choosing the place, you can extract the place information and do whatever you want with it.
L.mapbox.accessToken = "YOUR_API_KEY";//Load the map and give it a default style
map = L.mapbox.map("map", "mapbox.streets");
//set it to a given lat-lng and zoom level
map.setView([12.9718915, 77.64115449999997], 9);
//Display a default marker
marker = L.marker([12.9718915, 77.64115449999997]).addTo(map);
//This will display an input box
map.addControl(L.mapbox.geocoderControl("mapbox.places", {
autocomplete: true, //will suggest for places as you type
}).on("select", function(data){
//This function runs when a place is selected
//data contains the geocoding results
console.log(data);
//Do something with the results //Extract address and coordinates from the results and save it
requestDetails.location = {
address: data.feature["place_name"],
latitude: data.feature.center[1],
longitude: data.feature.center[0]
};
//Set the marker to new location
marker.setLatLng( [data.feature.center[1], data.feature.center[0]]);
}));
//Show cop location on the map
L.marker([
copDetails.location.latitude,
copDetails.location.longitude
],{
icon: L.icon({
iconUrl: "/images/police.png", //image path
iconSize: [60, 28] //in pixels
})
}).addTo(map);
L.mapbox.accessToken = "YOUR_API_KEY";//Load the map and give it a default style
map = L.mapbox.map("map", "mapbox.streets");
//set it to a cop's lat-lng and zoom level
map.setView( [copDetails.location.latitude, copDetails.location.longitude ], 9);
//Display a default marker
marker = L.marker([copDetails.location.latitude, copDetails.location.longitude]).addTo(map);
//This will display an input box
map.addControl(L.mapbox.geocoderControl("mapbox.places", {
autocomplete: true, //will suggest for places as you type
}).on("select", function(data){
//This function runs when a place is selected
//data contains the geocoding results
console.log(data);
//Do something with the results //Set the marker to new location
marker.setLatLng([
data.feature.center[1],
data.feature.center[0]
]);
}));
//Show citizen location on the map
L.marker([
requestDetails.location.latitude,
requestDetails.location.longitude
],{
icon: L.icon({
iconUrl: "/images/citizen.png",
iconSize: [50,50]
})
}).addTo(map);

Data Visualization

A Picture is worth a thousand words

People love visualizing data. It helps you understand a certain topic better. For example in the metric system, I didn’t quite realize just how large a Gigameter really is, but I understood it better after I saw this picture:

htwins.net/scale2/
mongoimport --db myUberApp --collection requestsData --drop --file ./path/to/jsonfile.json
  1. Next, have an endpoint — /requests/info that fetches your requests from MongoDB, converts them to the GeoJSON format and returns them to the client.
  2. Create a page data.html that loads the visualization library and stylesheet.
  3. Using AJAX, fetch the data-set from MongoDB and create a heatmap!

Step 1:

Open app.js, and write this code to serve the visualization page:

app.get('/data.html', function(req, res) {
res.render('data.html');
});

Step 2:

Let’s write a function in db-operations.js that fetches all results from your requestsData table:

function fetchRequests(db, callback) {
var collection = db.collection('requestsData');
//Using stream to process potentially huge records
var stream = collection.find({}, {
requestTime: true,
status: true,
location: true
}).stream();
var requestsData = []; stream.on('data', function(request) {
requestsData.push(request);
});
//Runs after results are fetched
stream.on('end', function() {
callback(requestsData);
});
}
exports.fetchRequests = fetchRequests;
{
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [<longitude>, <latitude>]
},
properties: {
<field1>: <value1>,
<field2>: <value2>,
...
}
}
...
]
}
app.get('/requests/info', function(req, res){
dbOperations.fetchRequests(db, function(results){
var features = [];
for(var i=0; i<results.length; i++){
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: results[i].location.coordinates
},
properties: {
status: results[i].status,
requestTime: results[i].requestTime,
address: results[i].location.address
}
});
}
var geoJsonData = {
type: 'FeatureCollection',
features: features
}
res.json(geoJsonData);
});
});

Step 3:

Create a page data.html in your views folder, and load the stylesheet and library for the visualization:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Visualize Data</title>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.css" rel="stylesheet" />
</head>
<body> <div id="map" style="width: 800px; height: 500px">
<!--Load the map here -->
</div>
<!-- Load socket.io client library -->
<script src="/socket.io/socket.io.js"></script>
<!-- Load JQuery from a CDN -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<!-- Load Mapbox GL Library -->
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.js"></script>
<!-- load libraries before your JS code
Write rest of your JS code here -->
<script type="text/javascript">
var socket = io();
var map, marker;
mapboxgl.accessToken = "YOUR_ACCESS_TOKEN";
</script>
</body>
</html>
$.ajax({
url: "/requests/info",
type: "GET",
dataType: "json",
success: function(data) {
console.log(data);
}
error: function(httpRequest, status, error) {
console.log(error);
}
});
var map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/dark-v9",
center: [77.64115449999997, 12.9718915],
zoom: 10
});
map.on("load", function() { //Add a new source from our GeoJSON data
map.addSource("help-requests", {
type: "geojson",
data: data
});
//we can specify different color and styling formats by adding different layers map.addLayer({
"id": "help-requests",
"type": "circle",
"source": "help-requests",
"paint": {
//Apply a different color to different status fields
"circle-color": {
property: "status",
type: "categorical",
stops: [
//For waiting, show in red
["waiting", "rgba(255,0,0,0.5)"],
//For engaged, show in green
["engaged", "rgba(0,255,0,0.5)"]
]
},
"circle-radius": 20, //Radius of the circle
"circle-blur": 1 //Amount of blur
}
});
});

Conclusion

If you made it this far, congratulations! Hopefully this tutorial series gave you an insight on how to build a real time web application with ease — all you now need is the next big idea!

  • Set the status field to closed once the cop has helped the citizen out. Then, you can assign a different color to visualize closed issues on a heat-map. That way you’ll have an understanding of how efficient cops are in a given area.
  • Build a rating system with which a citizen and a cop can rate each other. This way, neither citizen nor cop will misuse the system, and cops can get performance reports.
  • Have a cool looking user interface, like Material UI.
  • Lastly, have a sign-up and login mechanism!

Liked what you read? You should subscribe. I won’t waste your time.

Thank you for reading.

Do recommend this if it helped you. In-case you have questions on any aspect of this tutorial series or need my help in understanding something, feel free to tweet or leave a comment below. I’d love to hear about your Uber-for-X ideas!

And here’s what you’ve been waiting for, the full source code!

freeCodeCamp.org

This is no longer updated.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

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