MapSnippets logoMapSnippets logo

Geofencing with MapLibre GL JS

MapLibre GL JS

Define a polygon boundary in MapLibre GL JS and check whether a clicked point falls inside or outside the geofence.

progress_activityLoading map...
const map = new maplibregl.Map({
  container: 'map',
  style: 'https://api.maptiler.com/maps/streets-v4/style.json?key=YOUR_MAPTILER_KEY',
  center: [14.42, 50.08],
  zoom: 13
});
const fenceCoords = [[14.40, 50.09], [14.44, 50.09], [14.44, 50.07], [14.40, 50.07], [14.40, 50.09]];
function pointInPolygon(lng, lat, ring) {
  let inside = false;
  for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
    const [xi, yi] = ring[i], [xj, yj] = ring[j];
    if ((yi > lat) !== (yj > lat) && lng < (xj - xi) * (lat - yi) / (yj - yi) + xi) inside = !inside;
  }
  return inside;
}
map.on('load', () => {
  map.addSource('fence', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [fenceCoords] } } });
  map.addLayer({ id: 'fence-fill', type: 'fill', source: 'fence', paint: { 'fill-color': '#e63946', 'fill-opacity': 0.15 } });
  map.addLayer({ id: 'fence-line', type: 'line', source: 'fence', paint: { 'line-color': '#e63946', 'line-width': 2 } });
});
map.on('click', (e) => {
  const inside = pointInPolygon(e.lngLat.lng, e.lngLat.lat, fenceCoords);
  new maplibregl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(inside ? '<b style="color:#4CAF50">Inside zone</b>' : '<b style="color:#e63946">Outside zone</b>')
    .addTo(map);
});

How it works

This example defines a rectangular geofence over central Prague as a GeoJSON polygon. The fence is rendered on the map using a fill layer for the shaded area and a line layer for the border. Sources and layers are added inside the load event to ensure the style is ready.

When the user clicks anywhere, the pointInPolygon() function uses the ray-casting algorithm to test whether the click coordinates fall inside the fence ring. A popup displays the result with color-coded feedback. The polygon coordinates use [lng, lat] order, consistent with GeoJSON.

In production, combine this with the browser Geolocation API to continuously test the user’s position against the fence boundary.

Key APIs

API Description Docs
map.addSource() Add a GeoJSON data source maplibre.org docs
map.addLayer() Add a visual layer maplibre.org docs
map.on('click') Map click event maplibre.org docs

FAQ

Can I use an irregular polygon for the geofence?

Yes. Replace fenceCoords with any valid GeoJSON polygon ring. The ray-casting algorithm handles convex and concave shapes.

How do I trigger an action when the user enters the zone?

Combine with the Geolocation API (navigator.geolocation.watchPosition). On each position update, run pointInPolygon() and compare with the previous state to detect enter/exit transitions.

Common mistakes

  • Using [lat, lng] instead of [lng, lat]: GeoJSON and MapLibre both use longitude first
  • Not closing the polygon ring: GeoJSON requires the first and last coordinates to be identical
  • Adding sources before the load event: the style is not ready and addSource will throw
auto_awesome

Build maps using MapLibre GL JS with AI

Generate code like this geofencing with maplibre gl js example using the MapLibre GL JS skill in your IDE.