Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Location Geofencing with MongoDB, Stitch, and Mapbox

TwitterFacebookRedditLinkedInHacker News

For a lot of organizations, when it comes to location, geofencing is often a very desirable or required feature. In case you’re unfamiliar, a geofence can be thought of as a virtual perimeter for a geographic area. Often, you’ll want to know when something enters or exits that geofence so that you can apply your own business logic. Such logic might include sending a notification or updating something in your database.

MongoDB supports GeoJSON data and offers quite a few operators that make working the location data easy.

When it comes to geofencing, why would you want to use a database like MongoDB rather than defining boundaries directly within your client-facing application? Sure, it might be easy to define and manage one or two boundaries, but when you’re working at scale, checking to see if something has exited or entered one of many boundaries could be a hassle.

In this tutorial, we’re going to explore the $near and $geoIntersects operators within MongoDB to define geofences and see if we’re within the fences. For the visual aspect of things, we’re going to make use of Mapbox for showing our geofences and our location.

To get an idea of what we’re going to build, take a look at the following animated image:

Geofencing with MongoDB and Mapbox

We’re going to implement functionality where a map is displayed and polygon shapes are rendered based on data from within MongoDB. When we move the marker around on the map to simulate actual changes in location, we’re going to determine whether or not we’ve entered or exited a geofence.

The Requirements

There are a few moving pieces for this particular tutorial, so it is important that the prerequisites are met prior to starting:

  • Must have a Mapbox account with an access token generated.
  • Must have a MongoDB Atlas cluster available.

Mapbox is a service, not affiliated with MongoDB. To render a map along with shapes and markers, an account is necessary. For this example, everything can be accomplished within the Mapbox free tier.

Because we’ll be using MongoDB Stitch in connection with Mapbox, we’ll need to be using MongoDB Atlas.

MongoDB Atlas can be used to deploy an M0 sized cluster of MongoDB for FREE.

The MongoDB Atlas cluster should have a location_services database with a geofences collection.

Understanding the GeoJSON Data to Represent Fenced Regions

To use the geospatial functionality that MongoDB offers, the data stored within MongoDB must be valid GeoJSON data. At the end of the day, GeoJSON is still JSON, which plays very nicely with MongoDB, but there is a specific schema that must be followed. To learn more about GeoJSON, visit the specification documentation.

For our example, we’re going to be working with Polygon and Point data. Take the following document model:

{
    "_id": ObjectId(),
    "name": string,
    "region": {
        "type": string,
        "coordinates": [
            [
                [double]
            ]
        ]
    }
}

In the above example, the region represents our GeoJSON data and everything above it such as name represents any additional data that we want to store for the particular document. A realistic example to the above model might look something like this:

{
    "_id": ObjectId("5ebdc11ab96302736c790694"),
    "name": "tracy",
    "region": {
        "type": "Polygon",
        "coordinates": [
            [
                [-121.56115581054638, 37.73644193427164],
                [-121.33868266601519, 37.59729761382843],
                [-121.31671000976553, 37.777700170855454],
                [-121.56115581054638, 37.73644193427164]
            ]
        ]
    }
}

We’re naming any of our possible fenced regions. This could be useful to a lot of organizations. For example, maybe you’re a business with several franchise locations. You could geofence the location and name it something like the address, store number, etc.

To get the performance we need from our geospatial data and to be able to use certain operators, we’re going to need to create an index on our collection. The index looks something like the following:

db.geofences.createIndex({ region: "2dsphere" })

The index can be created through Atlas, Compass, and with the CLI. The goal here is to make sure the region field is a 2dsphere index.

Configuring MongoDB Stitch for Client-Facing Application Interactions

Rather than creating a backend application to interact with the database, we’re going to make use of MongoDB Stitch. Essentially, the client-facing application will use the Stitch SDK to authenticate before interacting with the data.

Within the MongoDB Cloud, choose to create a new Stitch application if you don’t already have one that you wish to use. Make sure that the application is using the cluster that has your geofencing data.

Stitch Geofencing Application

Within the Stitch dashboard, choose the Rules tab and create a new set of permissions for the geofences collection. For this particular example, the Users can only read all data permission template is fine.

Next, we’ll want to choose an authentication mechanism. In the Users tab, choose Providers, and enable the anonymous authentication provider. In a more realistic production scenario, you’ll likely want to create geofences that have stricter users and rules design.

MongoDB Stitch Anonymous Authentication

Before moving onto actually creating an application, make note of your App ID within Stitch, as it will be necessary for connecting.

Interacting with the Geofences using Mapbox and MongoDB Geospatial Queries

With all the configuration out of the way, we can move into the fun part of creating an attractive client-facing application that queries the geospatial data in MongoDB and renders it on a map.

On your computer, create an index.html file with the following boilerplate code:

<!DOCTYPE html>
<head>
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.10.0/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.10.0/mapbox-gl.css" rel="stylesheet" />
    <script src="https://s3.amazonaws.com/stitch-sdks/js/bundles/4.6.0/stitch.js"></script>
</head>
<html>
    <body style="margin: 0">
        <div id="map" style="width: 100vw; height: 100vh"></div>
        <script>
            // Logic in here ...
        </script>
    </body>
</html>

In the above HTML, we’re importing the Mapbox and MongoDB Stitch SDKs, and we are defining an HTML container to hold our interactive map. Interacting with MongoDB and the map will be done in the <script> tag that follows.

Within the <script> tag, the first things we want to accomplish are around connecting to MongoDB Stitch and configuring map:

const client = stitch.Stitch.initializeDefaultAppClient("MONGODB_STITCH_APP_ID_HERE");
const db = client.getServiceClient(stitch.RemoteMongoClient.factory, "mongodb-atlas").db("location_services");
let currentLocationMarker;

client.auth.loginWithCredential(new stitch.AnonymousCredential());

mapboxgl.accessToken = "MAPBOX_ACCESS_TOKEN_HERE";
let map = new mapboxgl.Map({
    container: "map",
    style: "mapbox://styles/mapbox/streets-v11",
    center: [-121.4252, 37.7397],
    zoom: 9
});

The map should be centered somewhere around Tracy, CA, and MongoDB Stitch was configured to use the location_services database. Make sure to swap the tokens with your actual Mapbox and Stitch tokens.

The next step is to populate the map with markers and polygons when it loads:

map.on("load", async () => {
    currentLocationMarker = new mapboxgl.Marker().setLngLat([-121.29473735351542, 37.94575186984845]).addTo(map);
    map.addSource("UNIQUE_ID", {
        "type": "geojson",
        "data": {
            "type": "Feature",
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [-121.56115581054638, 37.73644193427164],
                        [-121.33868266601519, 37.59729761382843],
                        [-121.31671000976553, 37.777700170855454],
                        [-121.56115581054638, 37.73644193427164]
                    ]
                ]
            }
        }
    });
    map.addLayer({
        "id": "UNIQUE_ID",
        "type": "fill",
        "source": "UNIQUE_ID",
        "layout": {},
        "paint": {
            "fill-color": "#088",
            "fill-opacity": 0.8
        }
    });
});

In the above load event, we are creating a marker somewhere outside the center of the map and one single polygon shape. The thing is, we don’t want to hard-code our polygon shapes that represent geofence regions. Instead, add the GeoJSON data to MongoDB along with other possible fences.

We can change our load event to the following:

map.on("load", async () => {
    let fences = await db.collection("geofences").find({}).asArray();
    currentLocationMarker = new mapboxgl.Marker().setLngLat([-121.29473735351542, 37.94575186984845]).addTo(map);
    fences.forEach(fence => {
        map.addSource(fence.name, {
            "type": "geojson",
            "data": {
                "type": "Feature",
                "geometry": fence.region
            }
        });
        map.addLayer({
            "id": fence.name,
            "type": "fill",
            "source": fence.name,
            "layout": {},
            "paint": {
                "fill-color": "#088",
                "fill-opacity": 0.8
            }
        });
    });
});

In the above code, we query our collection for all documents and add them each as a layer on the map. We can do better though. In the above example, the geofences amount could be quite large and it doesn’t necessarily make sense to show all the fences that aren’t even remotely close to the current location. This would slow down the application for the client.

Instead, we can change the query to the following:

let fences = await db.collection("geofences").find({
    region: {
        $near: {
            $geometry: {
                type: "Point",
                coordinates: [-121.4252, 37.7397]
            },
            $maxDistance: 50000
        }
    }
}).asArray();

In the above code, we’re saying that we only want geofence results that are within 50,000 meters of our center point coordinate set. While our example doesn’t have many documents, this could be very beneficial in terms of performance.

Depending on the geofence data in MongoDB, you likely have some polygons drawn on the map as well as your marker. The next step is to move the marker around to simulate a change in location. We can do this with the click event for Mapbox:

map.on("click", async (e) => {
    currentLocationMarker.setLngLat([e.lngLat.lng, e.lngLat.lat]);
    let result = await db.collection("geofences").find({
        region: {
            $geoIntersects: {
                $geometry: {
                    type: "Point",
                    coordinates: [e.lngLat.lng, e.lngLat.lat]
                }
            }
        }
    }, { projection: { name: 1 }}).asArray();
    if(result.length > 0) {
        alert(`Within the ${result[0].name} fence!`);
    }
});

In the above code we make use of the $geoIntersects operator in our query. What this is doing is returning all documents where our point sits within the polygon shape. The marker is updated to wherever the map was clicked.

If there is an intersection, we just print out the first result to an alert. We do this because there could be overlapping geofences and for the scope of this example, we don’t need to worry about that.

Conclusion

You just saw how to leverage MongoDB and its ability to do geospatial queries to create geofences for a maps and location services type application. In the application we built, we stored GeoJSON data within MongoDB and queried for it using the $near and $geoIntersects operators. This allowed us to figure out what data we wanted based on a point location. We made use of Mapbox to give us a visual element as to whether or not our queries would return data.

This content first appeared on MongoDB.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

The Polyglot Developer

Subscribe

Subscribe to the newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

The Polyglot Developer

Support This Site