Table of Contents
Introduction
For products involving deliveries, ride-hailing, ecommerce, and logistics, it is important include an optimized last-mile delivery tracking experience. NextBillion.ai provides a set of mapping and location-based service APIs and SDKs that allow developers to easily integrate maps and last mile tracking into the product.
In this example, we will go through the steps to create a Swiggy like last mile delivery tracking experience with the NextBillion API and SDK using VueJS.
By the end of this tutorial you should be able to;
- Create a Map using NextBillion SDK in VueJS
- Add custom Markers on the NextBillion Map
- Get location address and discover locations using NextBillion API
- Use NextBillion Directions API to get and render directions for live order tracking.
Prerequisite
To follow this tutorial you must have
- Basic understanding on Vue. VueJs Docs are a great place to start learning Vue.
- NextBillion API host url and API key. Create a NextBillion account by visiting the Get API key page in NextBillion docs.
Setup
Now that we have NextBillion API host and API key, let us setup our Vue project with NextBillion SDK.
I’ll assume that as a foundation you have node.js
installed on your system and you will use the node package manager (npm)
to install the Vue CLI globally.
$ npm install -g vue
Create the default boilerplate project using Vue CLI in a directory where you want to manage your project.
npm init vue@latest
You will be prompted to add a project name and some project settings. Select the options as per your requirements. I will use nbai
as my project name. After the project is created, change directory to the project, install dependencies and start the dev server.
npm install
npm run dev
Visit http://localhost:3000 from your browser and you should be able to see the default Vue application running.
Let us now add NextBillion SDK
and some other dependencies to our project.
# NextBillion SDK
npm install nbmap-gl
# TailwindCSS for styling.
npm install -D tailwindcss postcss autoprefixer
# Fontawesome icons for map markers
npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
npm i --save @fortawesome/free-brands-svg-icons
Note: See TailwindCSS docs and FontAwesome docs for their respective setup documentation. You can use alternative styling and icon options as well.
Adding NextBillion SDK
Create a .env
file with VITE_NEXTBILLION_API_KEY
and VITE_NEXTBILLION_API_HOST
values.
Now modify src/main.js
file to integrate the NextBillion SDK.
javascript
import nextbillion from 'nbmap-gl'
nextbillion.setApiKey(import.meta.env.VITE_NEXTBILLION_API_KEY)
nextbillion.setApiHost(import.process.env.VITE_NEXTBILLION_API_HOST)
Now that our basic setup is complete, let’s start building.
Creating a Map Component
We will create a Vue Component
(NBapiMAP
) for our Map which will be responsible for:
- Rendering the map using NextBillion SDK.
- Given a geo-location, render Marker on the map.
- Given
origin
anddestination
, render directions for order tracking.
Displaying the Map
Lets start with simple component which renders the map using NextBillion SDK when mounted to the DOM.
<template> <div class="map-container" id="map"></div> </template> <script> import nextbillion from "nbmap-gl"; import { decodePolyline } from "nbmap-gl/src/maps/decoder"; export default { name: "NBaiMap", props: { center: { lat: 28.6139, lng: 77.209, }, zoom: Number, } data() { return { map: null } }, methods: { mapClicked: function(event) { this.$emit("update-location", event.lngLat); }, _renderMap: function () { this.map = new nextbillion.maps.Map(document.getElementById("map"), { zoom: this.zoom || 12, center: this.center, }); } this.map.on("click", this.mapClicked) }, mounted() { this._renderMap(); } } </script>
<style lang="postcss" scoped> /* The map container will expand to its parent's height and width*/ .map-container { @apply w-full min-h-full; } </style>
This component takes two props center
and zoom
for rendering the map. Notice that we have added a click event handler to the map. The component will emit a custom event update-location
with LatLng
whenever the map is click. We can use this to set user or store locations.
Lets use the NBaiMap
in another component to render the Map. Create a file src/views/MapPage.vue
and add following content to it.
<template>
<div class="h-screen flex flex-col flex-1">
<div class="h-full flex flex-1 flex-grow">
<NBaiMap ref="map" v-if="haveLocation" :center="userLocation"></NBaiMap>
</div>
</div>
</template>
<script>
import NBaiMap from "@/components/NBaiMap.vue";
export default {
name: "MapPage",
components: {
NbaiMap,
},
data() {
return {
haveLocation: false,
userLocation: {
lat: null,
lng: null,
},
};
},
methods: {
geolocationSuccess: function (geolocation) {
this.haveLocation = true;
const { latitude, longitude } = geolocation.coords;
this.userLocation.lat = latitude;
this.userLocation.lng = longitude;
},
},
mounted() {
if (window.navigator.geolocation) {
window.navigator.geolocation.getCurrentPosition(this.geolocationSuccess);
}
},
};
This page gets user location from window.navigator.geolocation.getCurrentPosition
browser API.
Adding Markers to the Map
Let us now add a method to render Marker
with custom font-awesome
icon.
...
methods: {
// ...
_renderMarker: function (postion, icon) {
const marker = new nextbillion.maps.Marker({
position: position,
map: this.map,
});
// Change the icon if provided
if (icon !== null){
const markerElement = marker.getElement();
markerElement.style.backgroundImage = null;
markerElement.innerHTML = ``;
}
return marker;
},
// ...
}
...
The method _renderMarker
takes position
(Object with lat
and lng
keys) and icon
(font-awesome
icon class) and renders the icon on given position
.
Lets render default Markers
on map click by listenting to update-location
event from NBaiMap
,
<template>
...
<NBaiMap @update-location="renderMarker"></NBaiMap>
...
</template>
<script>
//...
methods: {
renderMarker: function (event) {
this.$refs.map._renderMarker({lat: event.lat, lng: event.lng})
}
}
//...
</script>
Now click on the map and a marker should be rendered where you clicked.
We can also render custom Markers
by passing _renderMarker
with font-awesome icon class as secondary argument. For example, let’s add multiple stores with fa-store
icon.
this.$refs.map._renderMarker({lat: event.lat, lng: event.lng}, 'fa-store')
Fetching and displaying Directions
Add a method to render Directions given origin and destination coordinates. We will add a Polyline
for route as we can use it to modify the strokeColor
and strokeWidth
of the route.
<script>
import { decodePolyline } from "nbmap-gl/src/maps/decoder";
//...
data() {
// ...
origin: null,
destination: null,
originMarker: null,
destinationMarker: null,
deliveryExecutiveMarker: null,
// ...
},
methods: {
_fitBounds: function (bounds, options) {
if (!options) {
options = {};
}
options.padding = 100;
this.map.map.fitBounds(bounds, options);
},
renderDirections: async function (origin, destination) {
const directions = await nextbillion.api.Directions({
origin,
destination,
steps: true,
});
const coords = decodePolyline(directions.routes[0].geometry, 5);
const path = [coords.map((item) => ({ lat: item[0], lng: item[1] }))];
new nextbillion.maps.Polyline({
path: path,
strokeWeight: 5,
map: this.map,
});
this.originMarker = this._renderMarker(origin);
this.destinationMarker = this._renderMarker(destination)
this._fitBounds([origin, destination], {});
this.origin = origin;
this.destination = destination;
this.$emit("update-path-duration", directions.routes[0].duration);
}
}
</script>
Let’s add some helper methods to our NBaiMap
to add and remove custom origin, destination and live tracking Markers.
methods: {
// ...
_addOriginMarker: function (loc) {
if (this.originMarker === null) {
this.originMarker = this._renderMarker(loc, "fa-store");
}
},
_removeOriginMarker: function () {
if (this.originmarker !== null) {
this.originMarker.remove();
this.originMarker = null;
}
},
_addDestinationMarker: function (loc) {
if (this.destinationMarker === null) {
this.destinationmarker = this._renderMarker(loc, "fa-location-dot");
}
},
_removeDestinationMarker: function () {
if (this.destinationMarker !== null) {
this.destinationMarker.remove();
this.destinationMarker = null;
}
},
_addDeliveryMarker: function (loc) {
if (this.deliveryExecutiveMarker === null) {
this.deliveryExecutiveMarker = this._renderMarker(loc, "fa-moped");
}
},
_removeDeliveryMarker: function () {
if (this.deliveryExecutiveMarker !== null) {
this.deliveryExecutiveMarker.remove();
this.deliveryExecutiveMarker = null;
}
},
// ...
}
We can also change the route color by passing strokeColor: "#f59e0b"
to nextbillion.maps.Polyline
.
Add methods to move a marker. This will be used to move the live location marker and update the estimated delivery time.
methods: {
// ...
_moveMarker: function (markerRef, loc) {
if (markerRef !== null) {
markerRef.moveTo(loc);
}
},
updateLiveLocation: function(loc) {
this._moveMarker(this.deliveryExecutiveMarker, loc);
const directions = await nextbillion.api.Directions({
origin: loc,
destination: this.destination,
})
this.$emit("update-path-duration", directions.routes[0].duration)
}
// ...
}
You can see the complete component at github link.
Getting User and Store Location
Let us now use our NBaiMap
component and create SetAddress
component to get user and store location. I will use localStorage
to save these locations. You would want to store these locations to your database using a backend server.
Add basic template and methods. Styles are removed for brevity.
<template>
<div class="h-screen flex flex-1 flex-col">
<div class="header">Set {{ addressKey }} Address</div>
<div class="address-controls-container">
<div v-if="!addressSet" class="search-container">
<div class="input z-index-50">
<input id="searchbox" class="input-box" v-model="q" />
<div class="suggestion-container" v-show="showSuggestions">
<div class="suggestion">
<div v-for="place in suggestions" :key="place" @click="flyToLocation(place)">{{ place.address.label }}</div>
</div>
</div>
</div>
<button class="search-btn" @click="discover">Search</button>
</div>
<div v-else class="flex my-auto">
<div class="selected-address">{{ address.address.label }}</div>
<button class="btn" @click="confirmLocation">Confirm</button>
<button class="btn" @click="resetLocation">Change</button>
</div>
</div>
<div class="">
<NBaiMap @update-location="setNewAddress" ref="map" :center="userLocation" :zoom="14"></NBaiMap>
</div>
</div>
</template>
<script>
import NBaiMap from "@/components/NbaiMap.vue";
export default {
name: "SetAddress",
components: {
NbaiMap,
},
props: {
addressKey: {
type: String,
default: "Home",
},
},
data() {
return {
q: "",
userLocation: {
lat: null,
lng: null
}
}
},
methods: {
confirmLocation: function () {
localStorage.removeItem(this.addressKey);
localStorage.setItem(this.addressKey, JSON.stringify(this.address));
this.$emit("address-set");
},
resetLocation: function () {
if (this.marker) {
this.marker.remove();
}
this.addressSet = false;
this.address = null;
},
renderMarker: function (loc) {
if (this.marker) {
this.marker.remove();
}
this.marker = this.$refs.map.renderMarker(loc);
},
geolocationSuccess: function (geolocation) {
const { latitude, longitude } = geolocation.coords;
this.userLocation.lat = latitude;
this.userLocation.lng = longitude;
},
flyToLocation: function (place) {
this.address = place;
this.addressSet = true;
this.showSuggestions = false;
this.$refs.map.map.flyTo({
center: place.position,
zoom: 14,
});
this.renderMarker(place.position);
},
},
mounted() {
if (window.navigator.geolocation) {
window.navigator.geolocation.getCurrentPosition(this.geolocationSuccess);
}
}
}
</script>
Let’s add the discover
method to fetch address suggestions using NextBillion discover API.
methods : {
// ...
discover: async function () {
const resp = await axios.get(`https://${import.meta.env.VITE_NEXTBILLION_API_HOST}/h/discover`, {
params: {
key: import.meta.env.VITE_NEXTBILLION_API_KEY,
q: this.q,
at: `${this.userLocation.lat},${this.userLocation.lng}`,
},
});
this.suggestions = resp.data.items;
this.showSuggestions = true;
},
// ...
}
We can select any suggested location to render a Marker on the Map and confirm the store or home location.
Creating the Order Tracking Component
Let us now put everything together and create an Order Tracking screen. We will use the NBaiMap
component created in above section to create a live order tracking component. I am using the storeLocation
and homeLocation
saved to localStorage
from previous step. You will want to save these loctions to your backend and fetch them to get Directions.
<template>
<div class="h-screen flex flex-col flex-1">
<div class="h-full flex flex-1 flex-grow">
<NBaiMap @update-path-duration="updatePathDuration" ref="nbaimap"></NbaiMap>
</div>
<div class="delivery-time-container">
<div class="delivery-time">
<div class="delivery-duration">{{ duration }}</div>
<div class="text-sm">mins</div>
</div>
<div v-if="homeAddress.address !== undefined" class="delivery-address">
Delivering to - {{ homeAddress.address.label }}
</div>
</div>
</div>
</template>
<script lang="ts">
import NBaiMap from "@/components/NBaiMap.vue";
export default {
name: "OrderTracking",
components: {
NBaiMap,
},
data() {
return {
duration: 0,
storeAddress: {},
homeAddress: {},
};
},
methods: {
updatePathDuration: function (duration) {
this.duration = Math.floor(duration / 60);
},
renderDirections: async function () {
this.$refs.nbaimap.renderDirections(
this.storeAddress.position,
this.homeAddress.position
);
},
updateLiveLocation: async function () {
// fetch live location from your backend
let liveLocationLatLng = await axios.get('backend-url');
this.$refs.nbaimap.updateLiveLocation(liveLocationLatLng)
}
},
mounted() {
const storeAddress = JSON.parse(localStorage.getItem("Store"));
const homeAddress = JSON.parse(localStorage.getItem("Home"));
// or fetch these locations from backend
if (storeAddress === null || homeAddress === null) {
this.$router.push({ name: "home" });
return;
}
this.storeAddress = storeAddress;
this.homeAddress = homeAddress;
this.renderDirections();
},
};</script>
Thats it! We can now use the
OrderTracking
component anywhere throughout our application for last mile delivery status.
Next Steps
The full code for this example project can be found at Github Repo URL. Clone the project and get mapping!
In our subsequent articles, we’ll be looking into optimizing delivery and delivery tracking using NextBillion Route Optimization
Ready to get started?
Request a DemoTable of Contents