How to Build Your Own Uber-for-X App - Part 2
just for the heck of it
- 🗓️ Date:
- ⏱️ Time to read:
Table of Contents
Note: Published in Free Code Camp’s Medium Publication or on the official freeCodeCamp News website.
Featured in Mybridge’s Top Ten NodeJS articles from Jan-Feb 2017 and Top 50 NodeJS article of the year (v.2018)
Welcome to PART 2 of this series Building your own Uber-for-X App.
In PART 1, you used an example of a civilian-cop app and learnt how to fetch cops located near a given pair of latitude and longitude coordinates. In this part, you’ll continue building the same app and learn to implement these features -
- Having the civilian and the cop exchange data with each other in real time using web sockets.
- Using maps to show location details of the civilian and the cop
- Visualizing crime data
Make sure you first read Part 1 thoroughly and try out the examples before proceeding with the rest of this tutorial.
Project Set-up and organization
Let’s analyze the project files that we currently have, from the previous part:
- app.js contains our server set-up and database configs. Every time you need to start the server you’ll use this file by typing
node app.js
in your terminal. - routes.js - you’ll use this file to write end-points and handlers
- db/db-models - contains the schema and model for your database collections
- db/db-operations - where you’ll write database operations
- views will contain the HTML pages. You’ll add three html files here - civilian.html, cop.html, and data.html.
- public will contain sub-folders for storing JavaScripts, stylesheets and images.
Now that the directory structure is set-up, it’s time to start implementing features! Before continuing, it’ll be helpful to keep the following points in mind -
- Write JS code inside the script tag in the HTML document. You may choose to write it inside a
.js
file, in which case you should save the JS file(s) inside /public/js folder and load it in your page. Make sure that you load the libraries and other dependencies first! - It’ll be helpful if you keep the developer console open in your browser to key an eye on network requests and reponses, and also logs for debugging. Keep a watch on your terminal output too.
- The words event and signal will be used interchangeably in this tutorial - both mean the same thing.
Let’s start hacking!
Serving civilian and Cop Pages
If you recall in the earlier part, we had these lines in app.js -
app.set('views', 'views');
app.use(express.static('./public'));
app.set('view engine', 'html');
app.engine('html', consolidate.handlebars);
The first line tells our app to look for HTML files inside the views folder whenever it gets a request for a particular page. The second line sets the folder from which static assets like stylesheets and JavaScripts will be served when a page loads on the browser. The next two lines tell our application to use the handlebars template engine to parse any html files.
If you’ve used Uber before, you’re aware that there’s the driver-facing app, and a rider facing app. Let’s try implementing the same - civilian.html will show the civilian facing side of the app and cop.html will show the cop facing app. We’ll save these files inside the views folder. Open civilian.html in your text editor and write this-
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset="utf-8"/>
<title>Civilian {{ userId }} </title>
</head>
<body data-userId="{{ userId }}">
<h1>Hello Civilian {{ 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>
Repeat the same for cop.html as well (Replace the word Civilian with Officer).
The data-userId
is an attribute that begins with the prefix data-
, which we can use to store some information as strings, that doesn’t necessarily need to have a visual representation. {{ userId }}
would appear to be a strange looking syntax, but don’t worry - our handlebars template engine understands that anything that’s between {{
and }}
is a variable. So while parsing our html file on the server, the templating engine will substitute the variable userId for the actual value that we supply, before the page is served.
The reason you’re doing template parsing here is to “simulate” different users. So for example, you could go to localhost:8000/civilian.html?userId=ashwin, which would load the civilian.html page with “Hello Civilian ashwin” displayed on the page. In a larger project however, you’ll want to actually add the functionality to store different user accounts in the database and add authentication methods.
So let’s render the civilian page on going to http://localhost:8000/civilian.html, and the cop page on going to http://localhost:8000/cop.html. To do this, open routes.js and add these lines -
router.get('/civilian.html', (req, res) => {
res.render('civilian.html', {
userId: req.query.userId
});
});
router.get('/cop.html', (req, res) => {
res.render('cop.html', {
userId: req.query.userId
});
});
Save your files, re-start your server and load the civilian and cop pages. You should see Hello Civilian on the page. If you pass userId
as query parameters in the URL, for example - http://localhost:8000/civilian.html?userId=YOURNAME then you’ll see Hello Civilian YOURNAME. That’s because our template engine substituted the variable userId
with the value that we passed from the query parameters, and served the page back.
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.
Over the centuries, newer and better forms of communication have emerged. The advent of computers and the internet sparked something really innovative - and with the development of the OSI model, socket programming and the smart-phone revolution, one-on-one communication has become quite sophisticated. The basic principles remain the same, but now much more interesting than setting something on fire and throwing it.
Using Sockets, you can send and receive information via events, or in other words signals. There can be different types of such signals, and if the parties involved know what kind of signal to ‘listen’ to, then there can be an exchange of information.
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.
But briefly put, traditional HTTP requests like GET and POST initiate a new connection request and later close the connection after the server sends back the response. If you were to attempt building a real time app using HTTP, the client would have to initiate requests at regular intervals to check for new information (which may or may not be available). This is because of the fact that the server itself is unable to push information on its own.
And this is highly inefficient - the client would waste resources in constantly interrupting the server and saying “Hi, I’m XYZ - let’s shake hands. Do you have something new for me?”, and the server will be like - “Hi (shaking hands). No I don’t. Good-bye!” over and over again, which means even the server is wasting resources!
Web-sockets however, create a persistent connection between a client and the server. So this way the client need not keep asking the server, the server can push information when it needs to. This method is much more efficient for building real time applications.
Web-sockets have support in all major browsers, but for few browsers that don’t - there are other fallback options/techniques to rely on, like Long Polling. These fallback techniques and the Web Sockets APIs are bundled together within Socket.IO, so you wouldn’t have to worry about browser compatibility. Here is an excellent answer on Stack Overflow that compares lots of those options.
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 axios to make XMLHttpRequests to the server from your client. So go ahead, write this in both the html pages -
<!-- Load socket.io client library -->
<script src="/socket.io/socket.io.js"></script>
<!-- Load Axios from a CDN -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- load libraries before your JS code
Write rest of your JS code here -->
<script type="text/javascript">
const socket = io();
// Fetch userId from the data-atribute of the body tag
const 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
let requestDetails = {};
let copDetails = {};
let map, marker;
</script>
The first script
tag loads Socket.IO’s client library (once we serve the page using socket.io server), which exposes a global io
object. Your app will make use of this object to emit events/signals to the server and listen to events from the server.
Now you have to make your express server listen to socket events. Create a new file socket-events.js and add these lines -
const dbOperations = require('./db/db-operations');
const mongoose = require('mongoose');
function initialize(server) {
// Creating a new socket.io instance by passing the HTTP server object
const io = require('socket.io')(server);
io.on('connection', (socket) => { // Listen on the 'connection' event for incoming sockets
console.log('A user just connected');
socket.on('join', (data) => { // Listen to any join event from connected users
socket.join(data.userId); // User joins a unique room/channel that's named after the userId
console.log(`User joined room: ${data.userId}`);
});
})
}
exports.initialize = initialize;
Now import this file in app.js and call the initialize
method inside server.listen
callback -
socketEvents.initialize(server);
If you restart the server and refresh your pages, you’ll see the message A user just connected
in your terminal. Try it with http://localhost:8000/cop.html?userId=02 and http://localhost:8000/civilian.html?userId=tyrion, you should get a similar output - the server will also create a new room once it receives a join
event from the connected clients, so you’ll see another message printed - User joined room <userId>
.
Perfect - now that you have integrated socket.io, you can begin building the rest of your application!
Civilian-cop communication
The entire process can be broadly divided into two sets of features -
- Requesting for help and notifying nearby cops
- Accepting the request and notifying the civilian
Let’s try to understand how to implement each of these features in detail.
Requesting for help and notifying nearby cops
- First create an end-point /cops/info inside routes.js, that will call a function to fetch a cop’s profile info, and return the results in the form of JSON to the client -
router.get('/cops/info', async (req, res) => {
const userId = req.query.userId // extract userId from query params
const copDetails = await dbOperations.fetchCopDetails(userId);
res.json({
copDetails: copDetails
});
});
- Next, we’ll write the function
fetchCopDetails
in db-operations.js, that takes in a cop’suserId
and returns a promise, which when resolved would give us the cop’s information. This function will use Mongoose’s findOne query to fetch a cop’s info with a givenuserId
from the database -
function fetchCopDetails(userId) {
return Cop.findOne({
userId: userId
}, {
copId: 1,
displayName: 1,
phone: 1,
location: 1
})
.exec()
.catch(error => {
console.log(error);
});
}
exports.fetchCopDetails = fetchCopDetails;
- Inside cop.html :
Now that you’ve created the endpoint, you can call it using Axios API to fetch the cop’s profile info and display it inside an empty div id="copDetails"
. You’ll also configure the cop page to begin listening to any help requests -
// First send a GET request using Axios and get the cop's details and save it
axios.get(`/cops/info?userId=${userId}`)
.then( (response) => {
copDetails = response.data.copDetails;
copDetails.location = {
address: copDetails.location.address,
longitude: copDetails.location.coordinates[0],
latitude: copDetails.location.coordinates[1]
};
document.getElementById("copDetails").innerHTML =
`Display Name: ${copDetails.displayName}
Address: ${copDetails.location.address}
`;
})
.catch((error) => {
console.log(error);
});
// Listen for a "request-for-help" event
socket.on("request-for-help", (eventData) => { // This function runs once a request-for-help event is received
requestDetails = eventData; // Contains info of civilian
// display civilian info
document.getElementById("notification").innerHTML =
`Civilian ${requestDetails.civilianId} is being attacked by a wildling and needs help!
They're at ${requestDetails.location.address}`;
});
If you restart the server and go to http://localhost:8000/cop.html?userId=02, you’ll find the cop’s info displayed on the page. Your cop page has also begun to listen to any request-for-help
events.
- Inside civilian.html
The next step is to create a button for the civilian 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 civilian back to the server -
<button onclick="requestForHelp()">
Request for help
</button>
Write the handler for generating the event in the script
tag -
// civilian's info
requestDetails = {
civilianId: userId,
location: {
address: "Indiranagar, Bengaluru, Karnataka, India",
latitude: 12.9718915,
longitude: 77.64115449999997
}
}
// When button is clicked, fire request-for-help and send civilian's userId and location
function requestForHelp(){
socket.emit("request-for-help", requestDetails);
}
- Inside db-model.js
Now you’ll define a schema and create a model to save help requests.
const requestSchema = mongoose.Schema({
requestTime: { type: Date },
location: {
coordinates: [ Number ],
adress: { type: String }
},
civilianId: { type: String },
copId: { type: String },
status: { type: String }
});
const Request = mongoose.model('Request', requestSchema);
exports.Request = Request;
The status
flag will tell whether a cop has responded to a request or not.
- Inside db-operations.js
Import the Request
model in db-operations.js and create a new function that can be used to save the request details:
function saveRequest(requestId, requestTime, location, civilianId, status){
const request = new Request({
"_id": requestId,
requestTime: requestTime,
location: location,
civilianId: civilianId,
status: status
});
return request.save()
.catch(error => {
console.log(error)
});
}
- socket-events.js
Finally, you must configure your socket server to listen to request-for-help
events from a civilian. When it gets one, it’ll save the details of the request, fetch nearby cops and emit a request-for-help event to all of them.
That’s it, you’ve built the first set of features! Re-start the server and test this out by opening 4 tabs, one for a civilian and cop pages 01, 02 and 03.
Once you press the help button, you’ll notice that cop 01 does not get the request because that cop is far away from the civilian’s location. However cop02 and cop 03 pages show the help request!
Awesome, you managed to send a request from a civilian and notify all nearby cops! Now, for the second set of features - this involves notifying the civilian once a cop accepts the request.
Accepting the request and notifying the civilian
- Inside cop.html:
The cop should be able to click a button to inform the civilian 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="helpCivilian()">
Help Civilian
</button>
and the event handler will look like this -
function helpCivilian() {
//Fire a "request-accepted" event/signal and send relevant info back to server
socket.emit("request-accepted", {
requestDetails: requestDetails,
copDetails: copDetails
});
}
- Inside civilian.html:
The civilian page will start listening to any request-acceptedevents 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 = eventData; // Save cop details
// Display Cop address
document.getElementById("notification").innerHTML =
`${copDetails.displayName} is near ${copDetails.location.address} and will be arriving at your location shortly.
You can reach them at their mobile ${copDetails.phone}`;
});
- Now the server needs to handle the
request-accepted
event as shown in the illustration. First you’ll write a function in db-operations.js that will update the request in the database with the cop’suserId
and change thestatus
field fromwaiting
toengaged
-
function updateRequest(issueId, copId, status) {
return Request.findOneAndUpdate({
"_id": issueId
}, {
status: status,
copId: copId
}).catch(error => {
console.log(error);
});
}
exports.updateRequest = updateRequest;
When the server listens to a request-accepted
event, it’ll use the above function to save the request details and then emit a request-accepted
event to the civilian. So go ahead, write this in your socket-events.js file -
socket.on('request-accepted', async (eventData) => { //Listen to a 'request-accepted' event from connected cops
console.log('eventData contains', eventData);
//Convert string to MongoDb's ObjectId data-type
const requestId = mongoose.Types.ObjectId(eventData.requestDetails.requestId);
//Then update the request in the database with the cop details for given requestId
await dbOperations.updateRequest(requestId, eventData.copDetails.copId, 'engaged');
//After updating the request, emit a 'request-accepted' event to the civilian and send cop details
io.sockets.in(eventData.requestDetails.civilianId).emit('request-accepted', eventData.copDetails);
});
Great, you’ve finished building the second set of features! Re-start your server, refresh your pages, and try it out!
What’s next?
By now it might have become obvious to you - the civilian page sends a hard-coded value of location every-time the button for help is clicked. Similarly the location info for all our sample cops have already been fed into the database earlier and are fixed values.
However in the real world, both the civilian and the cop don’t have a fixed location because they keep moving around - and therefore we’ll need a way to test this behaviour out!
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 our 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, we’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>
- You’ll also load the stylesheet used by mapbox.js inside the
<head>
tag of your HTML -
<link href="https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css" rel="stylesheet" />
Once you’ve done this, it’s time for you to start writing the logic -
- First, load the map and set it to show some location as default
- 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.
Leaflet exposes all it’s APIs inside a global variable L
. Since mapbox.js is built on top of Leaflet, the APIs that you’re gonna use will also be exposed in a global L
variable.
- In civilian.html - write this in your JavaScript
L.mapbox.accessToken = "YOUR_API_KEY";
map = L.mapbox.map("map", "mapbox.streets"); // Load the map and give it a default style
map.setView([12.9718915, 77.64115449999997], 9); // set it to a given lat-lng and zoom level
marker = L.marker([12.9718915, 77.64115449999997]).addTo(map); // Display a default marker
// This will display an input box on the map
map.addControl(L.mapbox.geocoderControl("mapbox.places", {
autocomplete: true, // will suggest for places as you type
}).on("select", (data) => { // This function runs when a place is selected
console.log(data); // data contains the geocoding 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]
};
marker.setLatLng([data.feature.center[1], data.feature.center[0]]); // Set the marker to new location
}));
The above code extracts the place information once you select a place and updates the location details, so the next time you click the help button, you’ll send the new location along with your request.
Once a cop accepts the request, you can show the location of the cop using a custom marker. First save this image inside /public/images, then write this code inside the event-handler of the request-accepted
event -
// 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);
That’s it! Now let’s repeat the same for the cop page as well -
- Inside cop.html
Our cop’s page fetches the cop’s location info from the server, so all we need to do is set the map and the marker to point to it. Let’s write this code inside the success
callback of our GET request -
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", (data) => {
console.log(data); // data contains the geocoding results
marker.setLatLng([data.feature.center[1], data.feature.center[0]]); // Set the marker to new location
}));
Once a cop gets a request, you can use a custom marker to display the civilian’s location. Download the marker image and save it in /public/images. Next, let’s write the logic inside the event handler of our request-for-help
event -
// Show civilian location on the map
L.marker([
requestDetails.location.latitude,
requestDetails.location.longitude
],{
icon: L.icon({
iconUrl: "/images/civilian.png",
iconSize: [50,50]
})
}).addTo(map);
Cool, let’s try this out - open cop pages 04, 05 and 06. In the civilian page, type “the forum bengaluru”, select the first result and watch the app in action when you ask for help!
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 -
Unlike computers, humans don’t understand numbers laid out on spreadsheets very easily - the larger the data-set, the harder it becomes for us to identify any meaningful patterns in it. Lot’s of meaningful information could go undetected, simply because the human brain is not trained to pour over large number of tables filled with text and numbers.
It’s much easier to process information and identify patterns if the data can be visualized. There are many ways to do that, in the form of graphs, charts etc. and there are several libraries that allows you to do those things in a screen.
At this point, I’m assuming that you probably have played around with your app a little bit, and saved help requests in MongoDB. If not, you can download the data-set and then import it to your database by typing this in your terminal:
mongoimport --db uberForX --collection requests --drop --file ./path/to/jsonfile.json
As you already know, the saved requests contain useful information like the location
details, the status
field which indicates whether a citizen has received help or not, and so forth. Perfect for using this information to visualize crime data on a heat-map! Here’s an example from Mapbox.
I’m gonna use MapBox GL JS - it’s a library that uses WebGL to help visualize data inside maps and make them very interactive. It’s extremely customizable - with features like colors, transitions and lighting. Feel free to try your own styles later!
For the heat-map feature, the library accepts data-sets in the GeoJSON format, and then plots data-points on the map. GeoJSON is a format for encoding a variety of geographic data structures. Hence you need to convert your saved data to adhere to this format.
So, here are the following steps:
- An endpoint to serve our visualization page data.html.
- Next, have an endpoint -
/requests/info
that fetches our requests from MongoDB, converts them to the GeoJSON format and returns them to the client. - Create a page data.html that loads the visualization library and stylesheet.
- Using AJAX, fetch the data-set from MongoDB and create a heatmap!
- Step 1
Open routes.js, and write this code to serve the visualization page -
router.get('/data.html', (req, res) => {
res.render('data.html');
});
- Step 2:
Let’s write a function in db-operations.js that fetches all results from our requests
table -
function fetchRequests() {
return new Promise( (resolve, reject) => {
try {
const requestsData = [];
const stream = Request.find({}, {
requestTime: 1,
status: 1,
location: 1,
_id: 0
}).stream();
stream.on("data", function (request) {
requestsData.push(request);
});
stream.on("end", function () {
resolve(requestsData);
});
} catch (err) {
console.log(err);
reject(err);
}
});
}
exports.fetchRequests = fetchRequests;
In the above code, you query the requests
table to return all documents. You can specify which fields to include and exclude from the results using boolean values - true
to include the field and false
to exclude the field. The results are then returned back to when the promise is resolved.
How does GeoJSON look like?
Information stored in GeoJSON has the following format -
{
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [<longitude>, <latitude>]
},
properties: {
<field1>: <value1>,
<field2>: <value2>,
...
}
}
...
]
}
We’ll need to convert each object returned by our function into feature objects. The properties
field can hold optional meta-data like status
, requestTime
, address
etc. We’ll write the handle in routes.js that will call the function, convert it to GeoJSON and then return it back -
router.get('/requests/info', async (req, res) => {
const results = await dbOperations.fetchRequests();
const features = [];
for (let 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
}
});
}
const 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/v1.1.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>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.1.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>
Now we’ll use Axios to call our endpoint and fetch the GeoJSON data -
axios.get("/requests/info")
.then( (response) => {
console.log(response.data);
.catch(function(error){
console.log(error);
});
Cool - save your code, re-start your server and point your browser to http://localhost:8000/data.html. You’ll see the results of your AJAX call in the console.
Now, let’s use it to generate a heat-map. Write this inside the success
callback of your request call -
map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/dark-v9",
center: [77.64115449999997, 12.9718915],
zoom: 10
});
map.on("load", () => {
// Add a new source from our GeoJSON data and set the
// 'cluster' option to true.
map.addSource("help-requests", {
type: "geojson",
data: response.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: [
['waiting', 'rgba(255,0,0,0.5)'], // For waiting, show in red
['engaged', 'rgba(0,255,0,0.5)'] // For engaged, show in green
]
},
"circle-radius": 20, // Radius of the circle
"circle-blur": 1 // Amount of blur
}
});
});
Refresh your page to see a cool looking heatmap generated from your data-set!
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!
I’m sure you’re aware that there are still plenty of places to improve upon in the app that you just built. You can try adding more features to it and make it more ‘intelligent’, for example -
- Mimic a moving cop and a moving civilian that continuously send location updates to each other in real time, and update the marker icons on the map.
- Set the
status
field toclosed
once the cop has helped the civilian out. Then, you can assign a different colour 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 civilian and a cop can rate each other. This way, neither civilian 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!
Using a library like React or a framework like Angular might help you implement features in a robust and scalable manner. You could also experiment with charting libraries like D3.js to visualize information in the forms of bar-charts, pie-charts, line-charts etc.
At some point you could deploy your app on a cloud hosting service provider - like Amazon Web Services, Google Cloud Platform or Heroku to show people what you made and have them test out features. It’ll be a nice way to get feedback and ideas, and who knows? - your app might turn out to be a life saver some day!
And here’s what you’ve been waiting for:
Source code
Checkout the Github repository, and please consider giving it a 🌟 if this helped you!