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

Introduction

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:

adonis-vue-leaflet-demo

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:

1# if you don't have Adonis CLI installed on your machine. 
2      npm install -g @adonisjs/cli
3      
4    # Create a new adonis app and move into the app directory
5    $ adonis new realtime_map && cd realtime_map

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

1adonis serve --dev
2    
3    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:

adonis-vue-leaflet-default

Set up Pusher Channels and install other dependencies

o get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app.

Then you'll get 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:

1//.env
2        PUSHER_APP_KEY=<APP_KEY>
3        PUSHER_APP_SECRET=<APP_SECRET>
4        PUSHER_APP_ID=<APP_ID>
5        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:

1#if you want to use npm
2    npm install pusher
3    
4    #or if you prefer Yarn
5    yarn add pusher

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

1#if you want to use npm
2    npm install adonis-pusher
3    
4    #or if you prefer Yarn
5    yarn add adonis-pusher

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

1const providers = [
2        ...
3        'adonis-pusher/providers/Pusher'
4    ]

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

Run this command in your terminal:

1#if you want to use npm
2    npm install vue axios moment laravel-mix pusher-js  mysql cross-env
3    
4    #or if you prefer Yarn
5    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:

1"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",
2    "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",
3    "asset-watch-poll": "npm run watch -- --watch-poll",
4    "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",
5    "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:

1const mix = require('laravel-mix');
2    
3    mix.setPublicPath('public');
4    /*
5     |--------------------------------------------------------------------------
6     | Mix Asset Management
7     |--------------------------------------------------------------------------
8     |
9     | Mix provides a clean, fluent API for defining some Webpack build steps
10     | for your Laravel application. By default, we are compiling the Sass
11     | file for your application, as well as bundling up your JS files.
12     |
13     */
14     
15    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:

1//../resources/assets/js/bootstrap.js
2    window.axios = require('axios');
3    
4    window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
5    window.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
6    window.axios.defaults.headers.common.crossDomain = true;
7    window.axios.defaults.baseURL = '/';
8    
9    let token = document.head.querySelector('meta[name="csrf-token"]');
10    
11    if (token) {
12      window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
13    } else {
14      console.error('CSRF token not found: https://adonisjs.com/docs/4.1/csrf');
15    }
16    
17    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:

1DB_CONNECTION=mysql
2    DB_HOST=127.0.0.1
3    DB_PORT=3306
4    DB_USER=your_database_user
5    DB_PASSWORD=your_dtabase_password
6    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:

1//../database/migrations/*_location_schema.js
2    'use strict'
3    
4    const Schema = use('Schema')
5    
6    class LocationSchema extends Schema {
7      up() {
8        this.create('locations', (table) => {
9          table.increments()
10          table.string('lat')
11          table.string('long')
12          table.timestamps()
13        })
14      }
15    
16      down() {
17        this.drop('locations')
18      }
19    }
20    
21    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:

1const Route = use('Route')
2    
3    Route.on('/').render('map')
4    Route.get('/locations', 'LocationController.loadLocations');
5    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:

1//../app/Controllers/Http/LocationController.js
2    'use strict'
3    
4    const Event = use('Event');
5    const Location = use('App/Models/Location');
6    class LocationController {
7    
8        async loadLocations({request,response}) {
9            let locations = await Location.all();
10            return response.json(locations);
11        }
12        
13        async postLocation({request,response}) {
14            let location = await Location.create(request.all());
15            Event.fire('new::location', location);
16            
17            return response.json({
18                msg: 'location set'
19            });
20        }
21    }
22    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:

1//events.js
2    
3    const Pusher = use('Pusher')
4    const Event = use('Event');
5    const Env = use('Env');
6    
7    // set up Pusher
8    let pusher = new Pusher({
9        appId: Env.get('PUSHER_APP_ID'),
10        key: Env.get('PUSHER_APP_KEY'),
11        secret: Env.get('PUSHER_APP_SECRET'),
12        cluster: Env.get('PUSHER_APP_CLUSTER'),
13        useTLS: false
14    });
15    //listening to events and send data with Pusher channels
16    Event.when('new::location', async(location) => {
17        console.log('location from event :', location);
18        pusher.trigger('location-channel', 'new-location', {
19            location
20        })
21    });

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

1//../resources/assets/js/components/Map.vue
2    <template>
3      <div id="map"></div>
4    </template>
5    
6    <script>
7    export default {
8      mounted() {
9        let lat = 51.505, long = -0.03;
10        const myMap = L.map("map").setView([lat, long], 13);
11        
12        var marker = L.marker([lat, long])
13          .addTo(myMap)
14          .bindPopup(
15            `<h2> Initial Location </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
16          );
17        var circle = L.circle([lat, long], {
18          color: "red",
19          fillColor: "#f03",
20          fillOpacity: 0.5,
21          radius: 500
22        }).addTo(myMap);
23        //set up Leaflet
24       
25        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
26          maxZoom: 16,
27          attribution:
28            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
29        }).addTo(myMap);
30        
31        //set up Pusher
32        var pusher = new Pusher("your_pusher_app_key", {
33          cluster: "eu",
34          forceTLS: false
35        });
36        
37        //Subscribe to the channel we specified in our Adonis Application
38        let channel = pusher.subscribe("location-channel");
39        channel.bind("new-location", data => {
40          let { lat, long } = data.location; //ES6 DESTRUCTURING
41          myMap.setView([lat, long], 13);
42          var marker = L.marker([lat, long])
43            .bindPopup(
44              `<h2> Your Position </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
45            )
46            .addTo(myMap);
47          var circle = L.circle([lat, long], {
48            color: "red",
49            fillColor: "#f03",
50            fillOpacity: 0.5,
51            radius: 500
52          }).addTo(myMap);
53        });
54        
55        this.loadLocations(myMap);
56        this.sendLocation();
57        
58      },
59      methods: {
60        loadLocations(map) {
61          axios
62            .get("locations")
63            .then(res => {
64              // const myMap = L.map("map");
65              console.log(res.data);
66              res.data.forEach(location => {
67                // alert("location");
68                let { lat, long } = location; //ES6 DESTRUCTURING
69                lat = parseFloat(lat);
70                long = parseFloat(long);
71                var marker = L.marker([lat, long])
72                  .addTo(map)
73                  .bindPopup(
74                    `<h2> Position </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
75                  );
76                var circle = L.circle([lat, long], {
77                  color: "red",
78                  fillColor: "#f03",
79                  fillOpacity: 0.5,
80                  radius: 500
81                }).addTo(map);
82              });
83            })
84            .catch(err => {
85              console.log(err);
86            });
87        },
88        sendLocation() {
89          if ("geolocation" in navigator) {
90            navigator.geolocation.getCurrentPosition(function(position) {
91              axios.post("locations", {
92                  lat: position.coords.latitude,
93                  long: position.coords.longitude
94                })
95                .then(res => {
96                  console.log(res.data.msg);
97                })
98                .catch(err => console.log(err));
99            });
100          } else {
101            alert("Your browser doesn't support HTML5 geolocation API");
102          }
103        }
104      }
105    };
106    </script>
107    <style scoped>
108    #map {
109      width: 100%;
110      height: 100%;
111    }
112    </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:

1var marker = L.marker([lat, long])
2          .addTo(myMap)
3          .bindPopup(
4            `<h2> Initial Location </h2> lat:<b>${lat}</b>, long:<b>${long}</b>`
5          );
6        var circle = L.circle([lat, long], {
7          color: "red",
8          fillColor: "#f03",
9          fillOpacity: 0.5,
10          radius: 500
11        }).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:

1import './bootstrap';
2    window.Vue = require('vue');
3    
4    import LocationMap from './components/Map';
5    const app = new Vue({
6        el: '#app',
7        components: {
8            LocationMap
9        }
10    });

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:

1//../resources/views/map.edge
2    
3    <!DOCTYPE html>
4    <html lang="en">
5    <head>
6      <meta charset="UTF-8"/>
7      <title>Realtime map with Vue.js, Leaflet and Pusher Channels</title>
8      <meta name="csrf-token" content="{{csrfToken}}">
9      <meta name="viewport"
10            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
11            <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" />
12      <script async src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
13    </head>
14    <body>
15    
16    <div id="app">
17       <div class="container">
18         <location-map></location-map>
19      </div>
20    </div>
21    {{ script('js/app.js') }}
22    </body>
23    </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 😎.

NOTE: 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.