Geofencing
Define a polygon boundary and check point containment Each library has its own approach: here's how they compare.
Quick Recommendations
Leaflet — Best for simple geofence visualization where you use a lightweight point-in-polygon check with Leaflet's polygon rendering.
MapLibre GL JS — Ideal when geofence boundaries are part of a data-driven map with expressions-based styling for inside/outside states.
OpenLayers — The right choice when you need built-in geometry operations: OpenLayers has native polygon intersection and containment methods.
MapTiler SDK — Brings MapLibre's geofence approach, with simplified map initialization.
At a Glance
| Leaflet | MapLibre | OpenLayers | SDK | |
|---|---|---|---|---|
| Coordinates | [lat, lng] | [lng, lat] | fromLonLat([lng, lat]) | [lng, lat] |
| Rendering | DOM + Canvas (2D) | WebGL (GPU, 60fps) | Canvas + WebGL | WebGL (GPU, 60fps) |
| Bundle size | ~42 KB gzipped | ~250 KB gzipped | ~290 KB gzipped (tree-shakeable) | ~280 KB gzipped (includes MapLibre) |
| Learning curve | Easiest (simple API, great tutorials) | Moderate (source/layer model, expressions) | Steepest (projections, OGC standards, verbose API) | Easy (MapLibre API with convenience helpers) |
| 3D support | No native 3D (2D only) | Full: terrain, fill-extrusion, globe, sky | Limited: WebGL layers, no native globe | Full (same as MapLibre) |
| Code lines | 22 | 27 | 25 | 28 |
Code Snippets
Click a library to expand its code. Each snippet shows only the core logic: the scaffold handles HTML boilerplate.
Leaflet22 linesShow code
const map = L.map('map').setView([48.8566, 2.3522], 13);
L.tileLayer('https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', {
tileSize: 256,
attribution: '© <a href="https://www.maptiler.com/">MapTiler</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
const fence = [[48.865, 2.335], [48.865, 2.365], [48.845, 2.365], [48.845, 2.335]];
L.polygon(fence, { color: '#e63946', fillOpacity: 0.15, weight: 2 }).addTo(map).bindPopup('Geofence zone');
function pointInPolygon(lat, lng, poly) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const [yi, xi] = poly[i], [yj, xj] = poly[j];
if ((yi > lng) !== (yj > lng) && lat < (xj - xi) * (lng - yi) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
map.on('click', (e) => {
const inside = pointInPolygon(e.latlng.lat, e.latlng.lng, fence);
L.popup()
.setLatLng(e.latlng)
.setContent(inside ? '<b style="color:#4CAF50">Inside zone</b>' : '<b style="color:#e63946">Outside zone</b>')
.openOn(map);
});MapLibre GL JS27 linesShow code
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);
});OpenLayers25 linesShow code
var fenceCoords = [[13.38, 52.53], [13.43, 52.53], [13.43, 52.51], [13.38, 52.51], [13.38, 52.53]];
var fenceFeature = new ol.Feature({
geometry: new ol.geom.Polygon([fenceCoords.map(function(c) { return ol.proj.fromLonLat(c); })])
});
var vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({ features: [fenceFeature] }),
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: '#e63946', width: 2 }),
fill: new ol.style.Fill({ color: 'rgba(230,57,70,0.15)' })
})
});
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({ source: new ol.source.XYZ({ url: 'https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', attributions: '© <a href="https://www.maptiler.com/copyright/">MapTiler</a>' }) }),
vectorLayer
],
view: new ol.View({ center: ol.proj.fromLonLat([13.405, 52.52]), zoom: 13 })
});
map.on('click', function(evt) {
var inside = fenceFeature.getGeometry().intersectsCoordinate(evt.coordinate);
var info = document.getElementById('info');
info.textContent = inside ? 'Inside zone' : 'Outside zone';
info.style.color = inside ? '#4CAF50' : '#e63946';
});MapTiler SDK28 linesShow code
maptilersdk.config.apiKey = 'YOUR_MAPTILER_KEY';
const map = new maptilersdk.Map({
container: 'map',
style: maptilersdk.MapStyle.STREETS,
center: [-0.1276, 51.5074],
zoom: 13
});
const fenceCoords = [[-0.15, 51.52], [-0.10, 51.52], [-0.10, 51.50], [-0.15, 51.50], [-0.15, 51.52]];
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': '#9C27B0', 'fill-opacity': 0.15 } });
map.addLayer({ id: 'fence-line', type: 'line', source: 'fence', paint: { 'line-color': '#9C27B0', 'line-width': 2 } });
});
map.on('click', (e) => {
const inside = pointInPolygon(e.lngLat.lng, e.lngLat.lat, fenceCoords);
new maptilersdk.Popup()
.setLngLat(e.lngLat)
.setHTML(inside ? '<b style="color:#4CAF50">Inside zone</b>' : '<b style="color:#e63946">Outside zone</b>')
.addTo(map);
});Things to Know
Implementation-specific details that matter for geofencing.
No built-in point-in-polygon test: you need a ray-casting algorithm or a library like Turf.js for containment checks.
Point-in-polygon logic must be implemented in JavaScript (ray-casting or Turf.js); the polygon itself is a fill/line layer.
ol.geom.Polygon has a built-in intersectsCoordinate() method that handles containment checks natively.
Same approach as MapLibre: manual point-in-polygon check with the boundary rendered as a GeoJSON fill layer.
Which Library to Use?
Define a polygon boundary and check whether a point is inside or outside the geofence.
Best for simple geofence visualization where you use a lightweight point-in-polygon check with Leaflet's polygon rendering.
Define a polygon boundary in MapLibre GL JS and check whether a clicked point falls inside or outside the geofence.
Ideal when geofence boundaries are part of a data-driven map with expressions-based styling for inside/outside states.
Define a polygon boundary in OpenLayers and check whether a clicked point falls inside or outside the geofence.
The right choice when you need built-in geometry operations: OpenLayers has native polygon intersection and containment methods.
Define a polygon boundary with MapTiler SDK and check whether a clicked point falls inside or outside the geofence.
Brings MapLibre's geofence approach, with simplified map initialization.
Build Maps with AI
Generate code like these examples using mapping library skills in your IDE.