Build a live map with Leaflet, Vue.js and Adonis.js
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:
- Node.js (>=8.9.0)
- Pusher Channels
- NPM(bundled with Node.js installer) or Yarn (>=1.12.1)
- MySQL database
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
andvuex
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 servermysql
, Node.js driver for MySQL to set up our database as this app will use MySQL for storagecross-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 locationlong
: 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 newLocation
instance with the request queries. We then fire an event namednew::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:
'© <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.
4 February 2019
by Ethiel Adiassa