Detecting and Mapping User Location using Capacitor Plugins
In this article we will create a mobile phone application that can detect a user’s geolocation and pinpoint it on a map by utilizing the power of Ionic Capacitor and its plugins. What’s more, is that the application will be able to collect geolocation information even if the app is on the background. This feature is very useful when it’s incorporated into fitness and navigation projects.
Necessary Technologies and Tools
- Next.js for React
- React Bootstrap for basic styling
- Ionic Capacitor for native app build
- Background Geolocation Plugin for background geolocation tracking
- Local Notifications Plugin for scheduling device notifications locally
- React Leaflet for open-source interactive maps
Setting Up the Project
Setting up a Next.js project with Capacitor is a little bit tricky, so please take a look into the “Building a Native Mobile App with Next.js and Capacitor” article by Simon Grimm before continuing to the next steps. It’s a great article and covers everything that we need to start the project.
If you have your project folder all set up and have already added the iOS and Android platforms, then it’s time to install the rest of the libraries and tools.
- Start by installing Bootstrap and React Bootstrap
npm install react-bootstrap bootstrap
Make sure to also include the stylesheet in order to be able to use the components. Include the following line in the _app.js file.
import 'bootstrap/dist/css/bootstrap.min.css'
- Install React Leaflet
npm install react-leaflet
Also, include the following link inside the <Head> component of the _document.js file.
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossOrigin="" />
- Install the Local Notifications and Background Geolocation plugins and sync the project
npm install @capacitor/local-notifications
npm install @capacitor-community/background-geolocation
npx cap sync
Add the following permissions to your AndroidManifest.xml file:
<!-- Permissions -->
...
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />
...
And the following keys to the Info.plist file:
<dict>
...
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need to track your location</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need to track your location while your device is locked.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
...
</dict>
Now that we have every tool installed, we can start coding our project! :)
Coding Time
There are three main files that we need to work on to make our application run smoothly. Two of them are components and the other one is the index.js. Let’s start by creating the OpenStreetMap.js file under the components folder.
import React, { useState, useRef } from 'react'
import { MapContainer, TileLayer } from 'react-leaflet'
import LocationMarker from './LocationMarker'
import 'leaflet/dist/leaflet.css'
import styles from './openstreemap.module.css'
const OpenStreetMap = (props) => {
const [center, setCenter] = useState({ lat: 36.028514, lng: 138.576491 })
const ZOOM_LEVEL = 3
const mapRef = useRef()
const position = props.position
return (
<>
<div className='container'>
<MapContainer center={center} zoom={ZOOM_LEVEL} ref={mapRef} className={styles.map}>
<LocationMarker position={position}/>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
</MapContainer>
</div>
</>
)
}
export default OpenStreetMap
In this file we utilize the MapContainer and TileLayer components from React Leaflet. The map will be served thanks to OpenStreetMap and a marker will be placed on the position that we receive from the props. LocationMarker is another component that we should place under the components folder.
import React, {useEffect} from "react";
import { Marker, useMap } from "react-leaflet";
import { Icon } from "leaflet";
import 'leaflet/dist/leaflet.css';
const LocationMarker = (props) => {
const position = props.position
const positionIsEmpty = position == null || position.length == 0
const map = useMap()
useEffect(() => {
if (!positionIsEmpty){
map.flyTo(position)
console.log("flying to position", position)
}
},[position])
return positionIsEmpty ? null : (
<Marker position={position} icon={new Icon({iconUrl: './marker-icon.png'})}></Marker>
)
}
export default LocationMarker
In this file we return a react leaflet marker if the position sent through the props in not null. Also, with the use of useEffect, every time a new position will be detected, the map will automatically ‘fly’ to that position, meaning that the map will be centered around the position’s geographic coordinates.
Finally, we are able to use the above components, as well as the Local Notifications and Background Geolocation plugins inside the index.js file.
import Head from 'next/head'
import { Inter } from 'next/font/google'
import styles from '../styles/Map.module.css'
import Button from 'react-bootstrap/Button';
import React, { useState, useEffect } from 'react';
import { registerPlugin } from "@capacitor/core";
import dynamic from 'next/dynamic';
import { LocalNotifications } from '@capacitor/local-notifications';
const BackgroundGeolocation = registerPlugin("BackgroundGeolocation")
const inter = Inter({ subsets: ['latin'] })
const OpenStreetMap = dynamic(() => import('../components/OpenStreetMap'), {
ssr: false,
})
export default function Map() {
const [id, setId] = useState(0)
const [position, setPosition] = useState([])
const started = Date.now()
function timestamp(time) {
return String(Math.floor((time - started) / 1000));
}
function log_for_watcher(text, time = Date.now(), colour = "gray") {
const li = document.createElement("li");
li.style.color = colour;
li.innerText = (
"L" + timestamp(time) + ":W" + timestamp(Date.now()) + ":" + text
);
const container = document.getElementById("log");
return container.insertBefore(li, container.firstChild);
}
function request_permissions() {
LocalNotifications.requestPermissions().then(
function (status) {
log_for_watcher("Notification permissions " + status.display);
}
);
}
function make_guess() {
BackgroundGeolocation.addWatcher(
{
backgroundMessage: "Cancel to prevent battery drain.",
requestPermissions: true,
stale: true
},
function callback(location) {
if (location === null)
log_for_watcher("null", Date.now())
else {
log_for_watcher([location.latitude.toFixed(4), location.longitude.toFixed(4)].map(String).join(":"), location.time)
setPosition([location.latitude, location.longitude])
}
}
).then(function retain_callback_id(the_id) {
setId(the_id);
log_for_watcher(the_id)
});
}
return (
<>
<Head>
<title>Geolocation</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<main className={`${inter.className} ${styles.main}`}>
<OpenStreetMap position={position} />
<div className="border border-secondary mt-3" style={{ height: 80 + 'px', overflow: 'auto', width: 90 + '%' }}>
<div id="log" className="p-2">Log details...</div>
</div>
<div className="row my-3">
<Button className='col btn-warning me-2' onClick={() => request_permissions()}>PERMISSIONS</Button>
<Button className="col btn-success me-2" onClick={() => { make_guess() }}>START</Button>
<Button className="col btn-danger" onClick={() => {
log_for_watcher("Stopping...")
BackgroundGeolocation.removeWatcher({ id })
}}>STOP</Button>
</div>
</main>
</>
)
}
This file renders a simple screen with three key elements; a dynamic map, a section for logging purposes and a trio of action-inducing buttons. First, let’s take a look at the buttons. We have the PERMISSIONS button which if clicked, the application asks for permission to send notifications to the user. This is the first button that should be clicked by the user before any other following action. If not, then the background geolocation tracking feature will not work.
For the app to start tracking the geolocation, the user needs to click the START button. This will trigger the make_guess() function, which then will invoke a watcher with the help of the Background Geolocation plugin. The callback function inside the watcher will be triggered every time a new location is detected. The key to make the location tracking feature work on the background too, is by setting up the message for the backgroundMessage option inside the watcher.
By pressing the STOP button, the user halts the tracking process and consequently the watcher that was invoked earlier is removed.
The section for logging is mainly for testing and debugging purposes as we can print inside the location returned by the plugin. To use the map, we have to dynamically import the OpenStreetMap component from our components folder, use it inside <main> and pass the position (which is being updated by the callback function inside the watcher) as a prop.
Running the Application
To have the application running natively on both iOS and Android phones, we first have to run a few commands.
Take into consideration that the Background Geolocation plugin doesn’t work on desktops.
npm run static
npx cap sync
npx cap open ios/android
The last command will open Xcode or Android Studio accordingly. From there we can build our native app and run it on an emulator or a device.
Conclusion
In this article we have showcased the integration of two Capacitor plugins in the development of an application for geolocation collection and mapping. There are many more plugins available, both from the official team as well as the community, that can be used to create a variety of applications. Don’t hesitate to explore and make use of them.
You can find the code from this article in this GitHub repository.