We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Build a live map with Leaflet, Vue.js and Adonis.js

  • Ethiel Adiassa
February 4th, 2019
You will need Node 8.9+ and MySQL installed on your machine.

Introduction

In this tutorial, we will build a realtime map using Leaflet, Vue.js and Pusher Channels. If you aren’t familiar with Leaflet, it’s a JavaScript library designed to build visually intuitive and interactive maps with just a few lines of code.

Take a look at their official site:

Leaflet is designed with simplicity, performance and usability in mind. It works efficiently across all major desktop and mobile platforms, can be extended with lots of plugins, has a beautiful, easy to use and well-documented API and a simple, readable source code that is a joy to contribute to.

We will combine the flexibility of Vue.js with the simplicity of Leaflet.js and then add a taste of realtime with Pusher Channels. The result will be an appealing realtime map.

Demo

At the end of the tutorial, you will have the following final result:

Prerequisites

For you to follow along with the tutorial, knowledge of JavaScript and Node.js is required. You should also have the following tools installed on your machine:

Initialize our Adonis.js project

Before going any further, we should install Adonis.js on our local machine if this is not done yet. Open your terminal and run this command in order to do so:

    # if you don't have Adonis CLI installed on your machine. 
      npm install -g @adonisjs/cli

    # Create a new adonis app and move into the app directory
    $ adonis new realtime_map && cd realtime_map

Now start the server and test if everything is working fine:

    adonis serve --dev

    2018-09-23T12:25:30.326Z - info: serving app on http://127.0.0.1:3333

If the steps above were successful, open your browser and make a request to : http://127.0.0.1:3333.

You should see the following:

Set up Pusher and install other dependencies

Head over to Pusher and create an account or sign in if you already have an account.

In the Pusher Channels dashboard create a new Pusher app instance, you will be then provided with credentials which can be used to communicate securely with the created Pusher instance. Copy the App ID, Key, Secret, and Cluster from the App Keys section and put them in the .env file located at you project root:

    //.env
        PUSHER_APP_KEY=<APP_KEY>
        PUSHER_APP_SECRET=<APP_SECRET>
        PUSHER_APP_ID=<APP_ID>
        PUSHER_APP_CLUSTER=<APP_CLUSTER>

These keys will be used further in this tutorial to link Pusher with our Adonis project.

Next, we need to install the Pusher SDK as well as other dependencies we’ll need to build our app. We won’t use the Pusher SDK directly but instead use a Pusher provider for Adonis. This provider will help us use easily the Pusher SDK with the Adonis.js ecosystem. But we should first install the Pusher SDK by running this command:

    #if you want to use npm
    npm install pusher

    #or if you prefer Yarn
    yarn add pusher

Now, you can install the Pusher provider for Adonis with this command:

    #if you want to use npm
    npm install adonis-pusher

    #or if you prefer Yarn
    yarn add adonis-pusher

You will need to add the provider to AdonisJS at start/app.js:

    const providers = [
        ...
        'adonis-pusher/providers/Pusher'
    ]

Last, let’s install other dependencies that we’ll use to build our app.

Run this command in your terminal:

    #if you want to use npm
    npm install vue axios moment laravel-mix pusher-js  mysql cross-env

    #or if you prefer Yarn
    yarn add vue axios moment laravel-mix pusher-js mysql cross-env

Dependencies we will use:

  • vue and vuex respectively to build the frontend of our app and manage our data store,
  • axios to make HTTP requests to our API endpoints
  • laravel-mix to provide a clean, fluent API for defining basic webpack build steps
  • pusher-js to listen to events emitted from our server
  • mysql, Node.js driver for MySQL to set up our database as this app will use MySQL for storage
  • cross-env to run scripts that set and use environment variables across platforms

Set up our build workflow

We’ll use laravel-mix to build and compile our application assets in a fluent way. But first we must tell our app to use it for that purpose. Open your package.json file and paste the following in the scripts section:

    "asset-dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "asset-watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "asset-watch-poll": "npm run watch -- --watch-poll",
    "asset-hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
    "asset-prod": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"

After that create a webpack.mix.js file at the root of your project and paste this code:

    const mix = require('laravel-mix');

    mix.setPublicPath('public');
    /*
     |--------------------------------------------------------------------------
     | Mix Asset Management
     |--------------------------------------------------------------------------
     |
     | Mix provides a clean, fluent API for defining some Webpack build steps
     | for your Laravel application. By default, we are compiling the Sass
     | file for your application, as well as bundling up your JS files.
     |
     */

    mix.js('resources/assets/js/app.js', 'public/js')

The code above builds, compiles and bundles all our JavaScript code into a single JS file created automatically in public/js directory. Create the following directory assets/js inside your resources one.

Now, create this file bootstrap.js and paste this code inside:

    //../resources/assets/js/bootstrap.js
    window.axios = require('axios');

    window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
    window.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
    window.axios.defaults.headers.common.crossDomain = true;
    window.axios.defaults.baseURL = '/';

    let token = document.head.querySelector('meta[name="csrf-token"]');

    if (token) {
      window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
    } else {
      console.error('CSRF token not found: https://adonisjs.com/docs/4.1/csrf');
    }

    window.Pusher = require('pusher-js');

You will notice we require dependencies to build our app. We also globally registered some headers to the Axios library in order to handle some security issues and to tackle in a proper way our API endpoints. These headers enable respectively ajax request, define Content-Type for our post requests, CORS and register the CSRF token.

Next, create this file: assets/js/app.js and paste the following inside:

    require('./bootstrap')

When we import our bootstrap.js file, laravel-mix will compile our app.js file. Our app is now ready to use laravel-mix for building and compiling our assets. By running this command: npm run asset-dev you should see a public/js/app.js file after the build process. Great!

Build our location model and migration

First, we need to set up our database, we’ll use a MySQL database for storage in this tutorial. Open your .env file and update the database section with your own identifiers:

    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_USER=your_database_user
    DB_PASSWORD=your_dtabase_password
    DB_DATABASE=your_database_name

Next, open your terminal and run the command below to generate our Location model as well as its corresponding controller and migration file which will be used to build the schema for our locations table:

    adonis make:model Location -mc

Here we are creating our Location model that which represents a user location at the time he is visiting our app.

Inside your product migration file, copy and paste this code:

    //../database/migrations/*_location_schema.js
    'use strict'

    const Schema = use('Schema')

    class LocationSchema extends Schema {
      up() {
        this.create('locations', (table) => {
          table.increments()
          table.string('lat')
          table.string('long')
          table.timestamps()
        })
      }

      down() {
        this.drop('locations')
      }
    }

    module.exports = LocationSchema

Our location schema is really simple to understand You can see we defined our locations table fields as:

  • lat: to hold the user latitute location
  • long: to hold the user’s longitude location

Now if your run this command: adonis migration:run in your terminal it will create a locations table in your database.

Create routes and the controller

In this section of the tutorial, we’ll create our routes and define controller functions responsible for handling our HTTP requests.

We’ll create three basic routes for our application, one for rendering our realtime map, one for fetching existing locations from the database and the last one for storing new locations into the database.

Go to the start/routes.js file and replace the content with:

    const Route = use('Route')

    Route.on('/').render('map')
    Route.get('/locations', 'LocationController.loadLocations');
    Route.post('/locations', 'LocationController.postLocation');

The first route / renders the map (which will be created further in the tutorial) view to the user.

Now, let’s create our controller functions. Open your LocationController.js file and paste the following:

    //../app/Controllers/Http/LocationController.js
    'use strict'

    const Event = use('Event');
    const Location = use('App/Models/Location');
    class LocationController {

        async loadLocations({request,response}) {
            let locations = await Location.all();
            return response.json(locations);
        }

        async postLocation({request,response}) {
            let location = await Location.create(request.all());
            Event.fire('new::location', location);

            return response.json({
                msg: 'location set'
            });
        }
    }
    module.exports = LocationController

First lines import the Event service provider and the Location model.

We have two functions in the code above:

  • loadLocations fetches locations from our database and returns them to our client, the browser as it happens in our case,
  • postLocation creates a new Location instance with the request queries. We then fire an event named new::location with the new instance. We can listen to this event and manipulate the data it carries.

Emit event with Pusher channels

This section will focus on how to broadcast from the backend with Pusher Channels. If you want clearer explanations on the process, you can take a look at this tutorial. Create a filename event.js in the start directory, then paste the following inside:

    //events.js

    const Pusher = use('Pusher')
    const Event = use('Event');
    const Env = use('Env');

    // set up Pusher
    let pusher = new Pusher({
        appId: Env.get('PUSHER_APP_ID'),
        key: Env.get('PUSHER_APP_KEY'),
        secret: Env.get('PUSHER_APP_SECRET'),
        cluster: Env.get('PUSHER_APP_CLUSTER'),
        useTLS: false
    });
    //listening to events and send data with Pusher channels
    Event.when('new::location', async(location) => {
        console.log('location from event :', location);
        pusher.trigger('location-channel', 'new-location', {
            location
        })
    });

We need to pull in the Event, Pusher (using the adonis-pusher package we installed earlier) and Env service providers. Then, we configure Pusher with the credentials provided, then we defined a listener for the new::location event which was registered in the LocationController.postLocation function we created above to handle comment creation. At last, we trigger a new-location event on the location-channel with the trigger method.

Build the map component

Our map will be a Vue component built with Leaflet library. Every time a user visits the app, we’ll grasp their position coordinates, and then send them to our backend. The backend at its turn will emit an event through the Pusher Channel location-channel we defined earlier and at last having subscribed to this channel in our component, we’ll be able to listen to realtime position updates and react properly to them.

Create a components folder inside your ../assets/js directory, create your Map.vue component inside. Take a look at the following code, don’t forget to paste inside your component file

    //../resources/assets/js/components/Map.vue
    <template>
      <div id="map"></div>
    </template>

    <script>
    export default {
      mounted() {
        let lat = 51.505, long = -0.03;
        const myMap = L.map("map").setView([lat, long], 13);

        var marker = L.marker([lat, long])
          .addTo(myMap)
          .bindPopup(
            `<h2> Initial Location </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
          );
        var circle = L.circle([lat, long], {
          color: "red",
          fillColor: "#f03",
          fillOpacity: 0.5,
          radius: 500
        }).addTo(myMap);
        //set up Leaflet

        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
          maxZoom: 16,
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(myMap);

        //set up Pusher
        var pusher = new Pusher("your_pusher_app_key", {
          cluster: "eu",
          forceTLS: false
        });

        //Subscribe to the channel we specified in our Adonis Application
        let channel = pusher.subscribe("location-channel");
        channel.bind("new-location", data => {
          let { lat, long } = data.location; //ES6 DESTRUCTURING
          myMap.setView([lat, long], 13);
          var marker = L.marker([lat, long])
            .bindPopup(
              `<h2> Your Position </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
            )
            .addTo(myMap);
          var circle = L.circle([lat, long], {
            color: "red",
            fillColor: "#f03",
            fillOpacity: 0.5,
            radius: 500
          }).addTo(myMap);
        });

        this.loadLocations(myMap);
        this.sendLocation();

      },
      methods: {
        loadLocations(map) {
          axios
            .get("locations")
            .then(res => {
              // const myMap = L.map("map");
              console.log(res.data);
              res.data.forEach(location => {
                // alert("location");
                let { lat, long } = location; //ES6 DESTRUCTURING
                lat = parseFloat(lat);
                long = parseFloat(long);
                var marker = L.marker([lat, long])
                  .addTo(map)
                  .bindPopup(
                    `<h2> Position </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
                  );
                var circle = L.circle([lat, long], {
                  color: "red",
                  fillColor: "#f03",
                  fillOpacity: 0.5,
                  radius: 500
                }).addTo(map);
              });
            })
            .catch(err => {
              console.log(err);
            });
        },
        sendLocation() {
          if ("geolocation" in navigator) {
            navigator.geolocation.getCurrentPosition(function(position) {
              axios.post("locations", {
                  lat: position.coords.latitude,
                  long: position.coords.longitude
                })
                .then(res => {
                  console.log(res.data.msg);
                })
                .catch(err => console.log(err));
            });
          } else {
            alert("Your browser doesn't support HTML5 geolocation API");
          }
        }
      }
    };
    </script>
    <style scoped>
    #map {
      width: 100%;
      height: 100%;
    }
    </style>

The template section has a simple <div> which is given a map id.

In the script part, we defined a set of coordinates to initialize our map.

Then we’ll initialize the map and set its view to our chosen geographical coordinates and a zoom level: const myMap = L.map("map").setView([lat, long], 13); . We also add a marker, and a circle to our map:

    var marker = L.marker([lat, long])
          .addTo(myMap)
          .bindPopup(
            `<h2> Initial Location </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
          );
        var circle = L.circle([lat, long], {
          color: "red",
          fillColor: "#f03",
          fillOpacity: 0.5,
          radius: 500
        }).addTo(myMap);

We bind a popup to the marker which will be shown when the marker is clicked. The popup contains the location coordinates. The circle can take some options to style its appearance as you can see. But for all these we pass the coordinates as an argument. Next we simply add a tile layer to add to our map.

After setting up our map, we initalize Pusher and subscribe to our location-channel thus we can be able to listen to events broadcasted:

let channel = pusher.subscribe("location-channel");

Do not forget to add your Pusher app key when initializing Pusher

The subscription returns back a channel object that we use to listen to the new-location event; this enables us to get visitors’ location updates in realtime: we pull in their coordinates, set the map view to this position instantly, then we add a marker and a circle to this particular position.

You may have also noticed two methods:

  • loadLocations: it does nothing but gets existing locations from the database, loops through them and for each one, adds it to the map with a proper marker and a circle. This is done with the help of the Axios JS library
  • sendLocation: in this method, we check if the user’s browser supports geolocation, if so we get its location coordinates and send it to our backend through a post request, if not we tell the user that his browser doesn’t support yet this functionality.

In the style section, we just defined a proper style to our map so that it can fit the entire page.

After the previous steps, you have to update your app.js file like the following:

    import './bootstrap';
    window.Vue = require('vue');

    import LocationMap from './components/Map';
    const app = new Vue({
        el: '#app',
        components: {
            LocationMap
        }
    });

We import our Map.vue component, initialize Vue and bind our component to the Vue instance. Also note that the Vue dependency is registered globally in order to access it everywhere: window.Vue = require('vue');

Finalize the app

Now, let’s create our map.edge file which contains our three Vue.js components. Run this command: adonis make:view map to create the file. Then paste this code inside:

    //../resources/views/map.edge

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8"/>
      <title>Realtime map with Vue.js, Leaflet and Pusher Channels</title>
      <meta name="csrf-token" content="{{csrfToken}}">
      <meta name="viewport"
            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
            <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" />
      <script async src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
    </head>
    <body>

    <div id="app">
       <div class="container">
         <location-map></location-map>
      </div>
    </div>
    {{ script('js/app.js') }}
    </body>
    </html>

We are almost done! Open your terminal and run npm run asset-dev to build your app. This can take a few seconds. After this step, run adonis serve --dev and open your browser to localhost:3333 to see your nice map. A new visitor’s location will be added instantly as intended 😎.

Warning: Please note that the map may not move automatically to your postion, then you’ll have to zoom out in order to see your position.

Conclusion

This is the end of the tutorial. I do hope you’ve enjoyed what you learned here: building a live map with Vue.js, Leaflet and Pusher Channels. The knowledge acquired here can help you achieve more astonishing things. You can get the full source code here.

Clone the project repository
  • Adonis.js
  • JavaScript
  • Location
  • Maps
  • Vue.js
  • Channels

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.