There may come a time in your mapping application where you need the user to identify a point of interest within a specific area. For example, having the user mark public bathrooms in a park, pinpoint hazards in a construction zone, or identify bus stops within a city.
For one project at Sparkgeo, the application required users to mark the building entrance for their place of business. For this feature, it was crucial that the data collected was accurate. We needed to ensure that the user did not drag the marker across the street, down the block, or halfway across the world. We needed to confirm that the marker was placed within the parcel associated with the address.
To solve this problem, we came up with the solution to confine the marker within a geofence of the address’s parcel. Let’s go over how this was accomplished.
This blog post assumes you have a prerequisite understanding of React and Mapbox GL, and the integration of the two. If you are unfamiliar with how to do so you can learn using Mapbox’s tutorial, Use Mapbox GL JS in a React app.
If you’d rather poke through the application itself instead of following step-by-step, here is a Code Sandbox of the working solution.
The entire solution can also be done without the use of React. A Code Sandbox of an implementation without React can be found here.
Initial setup
In our example, we will be using Vite to scaffold our project. Before we get into installing dependencies and implementing our geofenced marker, let’s look at what the base of our application looks like. This code snippet renders a map that spans the full width of the browser window.
import mapboxgl from 'mapbox-gl';
import { useEffect } from 'react';
import { useRef } from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const MAP_CENTER = [-123.14263, 49.30213];
export default function App() {
const mapContainer = useRef(null);
const map = useRef(null);
useEffect(() => {
if (map.current) return; // only render map once
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: MAP_CENTER,
zoom: 13.75,
});
})
return (
<div ref={mapContainer} style={{ height: '100vh', width: '100vw' }}/>
)
}
Adding a draggable marker
Set up a map load listener, then create and add a draggable marker inside the callback. We are also specifying a listener on the marker drag event which will be used later on.
map.current.on('load', () => {
const marker = new mapboxgl.Marker({ draggable: true })
.setLngLat(MAP_CENTER)
.addTo(map.current)
marker.on('drag', () => {
// we'll use this in a bit
})
})
Adding the geofence
We will be adding a geofence around Stanley Park in Vancouver using the City of Vancouver’s Open Data Portal. We can obtain the park’s geometry through a query.
Inside the map load event, add the GeoJSON source to the map, and then specify the layers.
// This is the GeoJSON from the query above
import STANLEY_PARK_GEOJSON from './stanley-park.json';
...
map.current.addSource('stanley-park', {
type: 'geojson',
data: STANLEY_PARK_GEOJSON,
});
map.current.addLayer({
id: 'stanley-park-fill',
type: 'fill',
source: 'stanley-park',
paint: {
'fill-color': 'purple',
'fill-opacity': 0.25,
},
});
map.current.addLayer({
'id': 'stanley-park-outline',
'type': 'line',
'source': 'stanley-park',
'layout': {},
'paint': {
'line-color': 'purple',
'line-width': 1
}
});
In the browser, our application should look like this:
Installing dependencies from Turf.js
To restrict our marker’s bounds, we will be using a few utility functions from Turf. Turf is a javascript library used for geospatial analysis that can be used both client- and server-side. It is often used in applications that require spatial unit conversions or operations relating to GeoJSON data.
booleanPointInPolygon
Turf’s booleanPointInPolygon function will help determine if our marker is within our area of interest. When it is detected that the marker is outside the bounds of the polygon, we can act accordingly.
npm install @turf/boolean-point-in-polygon
polygonToLine
Turf’s polygonToLine function converts our polygon into a LineString. The LineString will represent the boundary of our polygon. This is a necessary step that will allow us to compare the marker coordinates to the closest set of coordinates on the boundary of our polygon.
npm install @turf/polygon-to-line
nearestPointOnLine
Turf’s nearestPointOnLine function calculates the closest point on our boundary when the marker is outside the bounds. We will be able to use this point to reset our marker coordinates to stay within the boundary.
npm install @turf/nearest-point-on-line
Implementing the Geofence
Using the utility functions outlined above, we can:
- Detect when our marker is being dragged outside the boundary
- Calculate the closest point within the boundary relative to the current position of the cursor
- Reset the coordinates of our marker to prevent it from escaping
This takes place in the marker drag event we set up before.
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import nearestPointOnLine from "@turf/nearest-point-on-line";
import polygonToLine from "@turf/polygon-to-line";
...
marker.on('drag', () => {
const { lng, lat } = marker.getLngLat();
const markerPos = [lng, lat];
if (!booleanPointInPolygon(markerPos, STANLEY_PARK_GEOJSON)) {
const parkBoundary = polygonToLine(STANLEY_PARK_GEOJSON);
const closestPoint = nearestPointOnLine(parkBoundary, markerPos);
marker.setLngLat(closestPoint.geometry.coordinates);
};
});
Our end result is a marker that seamlessly stays within the boundary of Stanley Park.
And there you have it. With the help of a few functions from Turf, we have successfully been able to geofence our marker within our area of interest. By confining the marker’s bounds within a geofence, we’ve come up with a proactive solution that helps safeguard the data your application is collecting. This in turn reduces the potential effort required in cleaning and preparing your dataset.