/food-express

https://food-express.herokuapp.com/

Primary LanguageJavaScript

How To Track My Pizza, Realtime with Pusher and Google Maps

I was really hungry the other night and ordered something old school style, called up this small canteen which has some delicious, fresh and mouth-watering pizza! Now they did not have a tracker app and I couldn't wait to hog on the cheesy deliciousness so I decided to track it myself!

It wasn't much of an effort though, thanks to the great Pusher APIs the task was easy peasy! So I gave this link to my delivery hero (Anyone who delivers pizza is a hero) and he having a better smart phone than I do, got on-board quickly and I could track my pizza! :D

To give you a sneak peak this is how our app will look at the end of this post.

Step 0: Setting up our app

  • Start off by creating an index.html with this code snippet.
  • Our humble index.html doesn't do much except including a meta tag in the head section so that our app looks crisp and works great in mobile browsers as well.
<!DOCTYPE html>
<html>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <head>
    <title>Realtime Tracking with Pusher</title>
    <meta charset="utf-8">
  </head>
  <body>
    <h1>Food Express - Track your food delivery realtime!</h1>
  </body>
</html>
  • Since we'll be using Google Maps, let's add their JavaScript API and see the map in action!
  • But before you could integrate Google Maps you need to get your key. Click here to get the key
  • Once you get the key, copy it, and include their JS file in the index.html. Also add the script tag for app.js which will contain our app's code.
  • We'll also add a CSS file app.css for making our app look nice!
  • This is how your index.html will look like
<!DOCTYPE html>
<html>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <head>
    <title>Realtime Tracking with Pusher</title>
    <meta charset="utf-8">
    <link href="app.css" rel="stylesheet"></link>
  </head>
  <body>
    <div class="header">
      <h1>Food Express - Track your food delivery realtime!</h1>
    </div>
    <div class="container">
      <div id="map"></div>
    </div>
    <script src="https://maps.googleapis.com/maps/api/js?key=INSERT_YOUR_KEY_HERE"></script>
    <script type="text/javascript" src="src/app.js"></script>
  </body>
</html>
  • Let's add some code quickly to render the map in our app.js
(function () {
  // load the map
  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: -34.397, lng: 150.644},
    zoom: 8
  });
}());

Step 1: Capture device's location

  • We'll use Web's Geolocation API to get the user's location on start-up
  • With this code, we get the location and centre the map. See we're already into delivering a personalized experience!
// get the location via Geolocation API
if ('geolocation' in navigator) {
  var currentLocation = navigator.geolocation.getCurrentPosition(function (position) {
    map.setCenter({
      lat: position.coords.latitude,
      lng: position.coords.longitude
    });
  });
}

Step 2: Capture username

  • It's important to capture a username to identify my Delivery Hero on the map
  • Let's code a div which will take input and store it in-memory.
  • Add the following code in index.html
<div id="name-box" class="name-box">
  <h3>Enter your username</h3>
  <input id="name" type="text" placeholder="e.g. Mike">
  <button id="saveNameButton">Save</button>
</div>
  • And some JavaScript to get the name
var username;

// reference for DOM nodes
var saveNameButton = document.getElementById('saveNameButton');
var saveNameBox = document.getElementById('name-box');
var nameInput = document.getElementById('name');
var welcomeHeading = document.getElementById('welcome-message');
var deliveryHeroBox = document.getElementById('delivery-hero-box');

saveNameButton.addEventListener('click', saveName);

// all functions, event handlers
function saveName (e) {
  var input = nameInput.value;
  if (input && input.trim()) {
    username = input;

    // hide the name box
    saveNameBox.classList.add('hidden');

    // set the name
    welcomeHeading.innerHTML = 'Hi! <strong>' + username +
      (mode === 'user'
        ? '</strong>, type in your Delivery Hero\'s name to track your food.' 
        : '</strong>, type in the customer name to locate the address');
    // show the delivery hero's div now
     deliveryHeroBox.classList.remove('hidden');
  }
  return;
}

Step 3: Set up tracking logic, send events on location change

  • To track location of our pizza we'll use Pusher's real time capabilities. We'll trigger events whenever we change our location and also at the same time listen for the location change events of our Delivery Hero.
  • Signup for Pusher, or Login if you already have an account.
  • Once you login, create an app by giving an app-name and choosing a cluster in the Create App screen
  • Now that we've registered and created the app, add Pusher's JavaScript library in your index.html
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
  • Connect to your app by calling the Pusher constructor with your app key as shown in the below line
var pusher = new Pusher('<INSERT_PUSHER_APP_KEY_HERE>', {
  cluster: '<INSERT_PUSHER_CLUSTER_HERE>',
  encrypted: true
});
  • Next, I need to start triggering events when my location changes, so that my Delivery Hero knows that I am at my friend's place now. ( He said he was hungry too and I am a good guy :D ).
  • While we'll trigger events for our location change, we need to secure these events so that only intended recipients can track us. We'll accomplish this by using Pusher's Channel concept
  • Channels are a way to filter and secure events. In our app each user will be represented as a channel. We'll be using Pusher's Private Channels
var myLocationChannel = pusher.subscribe('private-<USERNAME>');
  • A channel will be named after the username chosen by the user, and with this the other party can subscribe and listen the location change events for a particular user.
  • To use private channels, you must be authenticated. Pusher makes writing an auth server very easy. I used their NodeJS template here.
  • My server.js looks like this
var express = require('express');
var bodyParser = require('body-parser');
var Pusher = require('pusher');

var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// to serve our JavaScript, CSS and index.html
app.use(express.static('./'));

var pusher = new Pusher({
  appId: 'INSERT_YOUR_APP_ID_HERE',
  key: 'INSERT_YOUR_KEY_HERE',
  secret:  'INSERT_YOUR_SECRET_HERE',
  cluster: '<INSERT_PUSHER_CLUSTER_HERE>'
});

app.post('/pusher/auth', function(req, res) {
  var socketId = req.body.socket_id;
  var channel = req.body.channel_name;
  var auth = pusher.authenticate(socketId, channel);
  res.send(auth);
});

var port = process.env.PORT || 5000;
app.listen(port, () => console.log('Listening at http://localhost:5000'));
  • To trigger events we'll be using Pusher's Client Events as clients can directly trigger these events from their devices and they need not necessarily go via a server first.
  • You need to enable Client Events in your Settings tab on Pusher's Dashboard
  • Client Events should start with client-. (Note that Client Events have a number of restrictions that are important to know about while creating your awesome app. Read more about them here.)
  • On startup we create a channel using the below code, and then send our client events to it every time we change location
  • We'll also save the last location in an object (myLastKnownLocation) for later retrieval
function createMyLocationChannel (name) {
  var myLocationChannel = pusher.subscribe('private-' + name);
  myLocationChannel.bind('pusher:subscription_succeeded', function() {
    // safe to now trigger events
    // use the watchPosition API to watch the changing location
    // and trigger events with new coordinates
    locationWatcher = navigator.geolocation.watchPosition(function(position) {
      var location = {
        lat: position.coords.latitude,
        lng: position.coords.longitude
      };
      triggerLocationChangeEvents(myLocationChannel, location);
    });

    // also start a setInterval to keep sending the loction every 5 secs
    sendLocationInterval = setInterval(function () {
      // not using `triggerLocationChangeEvents` to keep the pipes different
      myLocationChannel.trigger('client-location', myLastKnownLocation)
    }, 5000);
  });
}
  • To handle the case when the user isn't moving, we add a setInterval to keep sending the last captured location. This means that my Delivery Hero can track my last location, if I dozed off!
 sendLocationInterval = setInterval(function () {
    // not using `triggerLocationChangeEvents` to keep the pipes different
    myLocationChannel.trigger('client-location', myLastKnownLocation)
  }, 5000);

// also update myLastKnownLocation everytime we trigger an event
function triggerLocationChangeEvents (channel, location) {
  // update myLastLocation
  myLastKnownLocation = location;
  channel.trigger('client-location', location);
}

Step 4: Subscribe to Delivery Hero's location channels

  • First up, code a div in index.html to enter Delivery Hero's username.
  <div id="delivery-hero-box" class="name-box hidden">
     <h3 id="welcome-message"></h3>
     <h4 id="delivery-heroes-list"></h4>
     <input id="deliveryHeroName" type="text" placeholder="e.g. Shelly">
     <button id="addDeliveryHeroButton">Add</button>
  </div>

Let's make the button functional by adding an event listener on it

deliveryHeroesAddButton.addEventListener('click', addDeliveryHero);
  • So every time you add a username, addDeliveryHero function would get called.
function addDeliveryHero (e) {
    var deliveryHeroName = deliveryHeroNameInput.value;
    // if already present return
    if (deliveryHeroesLocationMap[deliveryHeroName]) return;
    if (deliveryHeroName) {
      var deliveryHeroChannelName = 'private-' + deliveryHeroName;
      var deliveryHeroChannel = pusher.subscribe(deliveryHeroChannelName);
      deliveryHeroChannel.bind('client-location', function (nextLocation) {
        // first save the location
        // bail if location is same
        var prevLocation = deliveryHeroesLocationMap[deliveryHeroName] || {};
        deliveryHeroesLocationMap[deliveryHeroName] = nextLocation;
        showDeliveryHeroOnMap(deliveryHeroName, false, true, prevLocation);
      });
    }

    // add the name to the list
    var deliveryHeroTrackButton = document.createElement('button');
    deliveryHeroTrackButton.classList.add('small');
    deliveryHeroTrackButton.innerHTML = deliveryHeroName;
    deliveryHeroTrackButton.addEventListener('click', showDeliveryHeroOnMap.bind(null, deliveryHeroName, true, false, {}));
    deliveryHeroesList.appendChild(deliveryHeroTrackButton);
  }
  • In the above code, we first subscribe to the private Pusher channel of the hero.
var deliveryHeroChannelName = 'private-' + deliveryHeroName;
var deliveryHeroChannel = pusher.subscribe(deliveryHeroChannelName);
  • And listen to all the events triggered on that channel
deliveryHeroChannel.bind('client-location', function (nextLocation) {
   // first save the location
   // bail if location is same
   var prevLocation = deliveryHeroesLocationMap[deliveryHeroName] || {};
   deliveryHeroesLocationMap[deliveryHeroName] = nextLocation;
   showDeliveryHeroOnMap(deliveryHeroName, false, true, prevLocation);
});
  • We keep the event name, same i.e. client-location as every user has a distinct channel.
  • Read more about keeping the data private here.
  • Each new event contains the latest location and we save that in an object to retrieve later.
  • Also we take the help of another function to plot the location on a map, showDeliveryHeroOnMap
function showDeliveryHeroOnMap (deliveryHeroName, center, addMarker, prevLocation) {
    if (!deliveryHeroesLocationMap[deliveryHeroName]) return;
    // first center the map
    if (center) map.setCenter(deliveryHeroesLocationMap[deliveryHeroName]);
    var nextLocation = deliveryHeroesLocationMap[deliveryHeroName];
    
    // add a marker
    if ((prevLocation.lat === nextLocation.lat) && (prevLocation.lng === nextLocation.lng)) {
      return;
    }
    
    if (addMarker) {
      var marker = deliveryHeroesMarkerMap[deliveryHeroName];
      marker = marker || new google.maps.Marker({
        map: map,
        label: deliveryHeroName,
        animation: google.maps.Animation.BOUNCE,
      });
      marker.setPosition(deliveryHeroesLocationMap[deliveryHeroName]);
      deliveryHeroesMarkerMap[deliveryHeroName] = marker;
    }
  }
  • The above function adds a marker at the new location on the map, and bails if the new location is same as previous location.
  • Also if we're already tracking a hero, it updates the new location in the same marker.
  • So that if the person is moving, you can see the marker moving on the map in realtime. ZOMG! I KNOW! More excited because as soon as I finish writing this I can eat my tracked pizza! :D

Step 5: Finding multiple heroes, all at once

  • Now that you've learned how to find a Delivery Hero in the above step, we can do it for multiple heroes. Yay! I am a foodie and I know it.
  • Notice that we are also adding a button(deliveryHeroTrackButton) for all the heroes that we add via the textbox
  • On clicking that button we'll center the screen with the last known location of that Delivery Hero.

Next Steps

  • Since we're using the awesome concept of Pusher Channels, we can easily build on top of it to track anything, food parcel or an e-commerce delivery
  • Also, Google Maps API integration is a piece-of-cake.

Running locally

  • clone the repository
  git clone git@github.com:ankeetmaini/food-express.git
  cd food-express
  npm install
  npm start
  • Generate Pusher API keys and insert in src/app.js as well as server.js

  • Generate Google Maps API key and add in index.html

  • open http://localhost:5000

Running the Demo

  • To track your food delivery open https://food-express.herokuapp.com/
  • Login with your username, any name for sake of simplicity
  • The Delivery Hero should also Login at this address https://food-express.herokuapp.com/?mode=delivery
  • In a typical Food Delivery service, once you place an order, you receive some confirmation with the expected time of delivery and Delivery person's contact information and name/username. Assuming you received an SMS/E-mail of the same
  • Type in the username of the Delivery person to track your Food!

Show me the code