MapLibre GL JS + PickPoint
MapLibre GL JS is a community-maintained, MIT-licensed fork of Mapbox GL JS - created in 2020 when Mapbox changed its licence to require proprietary terms. The fork kept the full rendering engine: WebGL-powered vector tiles that stay pixel-sharp at every zoom, smooth 60fps pan/tilt, data-driven styling and 3D terrain. It is now used in production by major navigation, logistics and GIS platforms worldwide.
The key difference from Leaflet is the rendering model. Leaflet draws raster PNG tiles - each tile is a bitmap fetched from a server. MapLibre draws vector data with WebGL: the browser receives raw geometric data and applies a style definition locally. This means crisp rendering on retina and 4K displays, instant style changes without re-fetching tiles, and the ability to extrude buildings in 3D or colour road segments by speed.
We use OpenFreeMap vector tiles - fully free, no API key, with a clean professional style. For production, swap in MapTiler, Stadia Maps, or Protomaps self-hosted tiles.
[longitude, latitude] order (GeoJSON convention). The route is added as a GeoJSON LineString source - updating it only requires a setData() call with no layer rebuild.HTML scaffold
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet">
<style>
html, body, #map { height: 100%; margin: 0; }
#search-box { position: absolute; top: 12px; right: 12px; z-index: 1;
background: #fff; padding: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,.15); min-width: 280px; }
#query { width: 100%; box-sizing: border-box; padding: 6px 10px;
border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
#suggestions { list-style: none; margin: 4px 0 0; padding: 0; }
#suggestions li { padding: 6px 10px; cursor: pointer; border-radius: 4px; font-size: 13px; }
#suggestions li:hover { background: #f0f7f7; }
</style>
</head>
<body>
<div id="map"></div>
<div id="search-box">
<input id="query" type="search" placeholder="Search destination…" autocomplete="off">
<ul id="suggestions"></ul>
</div>
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<script src="app.js"></script>
</body>
</html>Initialise map and reserve the route layer
Pre-register an empty GeoJSON source and line layer - updating data later is instant with no layer recreation:
// app.js
const API_KEY = 'YOUR_PICKPOINT_KEY';
const map = new maplibregl.Map({
container: 'map',
// OpenFreeMap - free vector tiles, no API key required
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [-0.09, 51.505], // [lon, lat]
zoom: 12,
});
let originMarker = null;
let destMarker = null;
map.on('load', () => {
map.addSource('route', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addLayer({
id: 'route-line', type: 'line', source: 'route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#008080', 'line-width': 4 },
});
});Get the user's position
// MapLibre uses [lon, lat] everywhere
navigator.geolocation.getCurrentPosition(({ coords }) => {
const center = [coords.longitude, coords.latitude];
map.flyTo({ center, zoom: 14 });
originMarker = new maplibregl.Marker({ color: '#008080' })
.setLngLat(center).addTo(map);
});Address autocomplete
const input = document.getElementById('query');
const list = document.getElementById('suggestions');
let debounce;
input.addEventListener('input', () => {
clearTimeout(debounce);
const q = input.value.trim();
if (q.length < 2) { list.innerHTML = ''; return; }
debounce = setTimeout(async () => {
const b = map.getBounds();
// /v2/address/search uses bbox=minLon,minLat,maxLon,maxLat
const bbox = `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`;
const res = await fetch(
`https://api.pickpoint.io/v2/address/search?q=${encodeURIComponent(q)}&limit=6&bbox=${bbox}`,
{ headers: { 'X-Api-Key': API_KEY } }
);
const { features = [] } = await res.json();
// Response is GeoJSON: coordinates = [lon, lat], properties.name = label
list.innerHTML = features.map(f => {
const [lon, lat] = f.geometry.coordinates;
const label = [f.properties.name, f.properties.city].filter(Boolean).join(', ');
return `<li data-lat="${lat}" data-lon="${lon}">${label}</li>`;
}).join('');
}, 250);
});
list.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
const lat = parseFloat(li.dataset.lat);
const lon = parseFloat(li.dataset.lon);
input.value = li.textContent;
list.innerHTML = '';
destMarker?.remove();
destMarker = new maplibregl.Marker({ color: '#e06060' })
.setLngLat([lon, lat]).addTo(map);
if (originMarker) drawRoute(originMarker.getLngLat(), { lat, lon });
});Fetch route and update the map
shape is [lat, lon] pairs - flip to [lon, lat] for GeoJSON before calling setData():
async function drawRoute(origin, dest) {
const res = await fetch('https://api.pickpoint.io/v2/route', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': API_KEY },
body: JSON.stringify({
locations: [
{ lat: origin.lat, lon: origin.lng },
{ lat: dest.lat, lon: dest.lon },
],
costing: 'auto',
}),
});
const data = await res.json();
const shape = data.trip.legs[0].shape; // [[lat, lon], …]
// Flip to [lon, lat] for GeoJSON
const coords = shape.map(([lat, lon]) => [lon, lat]);
map.getSource('route').setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: coords },
});
const lons = coords.map(c => c[0]), lats = coords.map(c => c[1]);
map.fitBounds(
[[Math.min(...lons), Math.min(...lats)], [Math.max(...lons), Math.max(...lats)]],
{ padding: 60 }
);
}
// Real-time position updates via PickPoint Device Tracking WebSocket
// npm install socket.io-client
import { io } from 'socket.io-client';
const vehicleMarker = new maplibregl.Marker({ color: '#008080' })
.setLngLat([0, 0]).addTo(map);
const socket = io('wss://ws.pickpoint.io', {
transports: ['websocket'],
path: '/v2/tracking',
query: { 'x-api-key': API_KEY }, // subscriber auth - keep on server!
});
socket.on('connect', () => {
socket.emit('device:subscribe', { deviceUid: 'DEVICE_UID' });
});
socket.on('location:added', ({ latitude, longitude }) => {
vehicleMarker.setLngLat([longitude, latitude]); // MapLibre: [lon, lat]
map.panTo([longitude, latitude]);
});MapLibre-specific extras
- Animated route draw - animate
line-dasharrayto make the route appear stroke by stroke. - Data-driven styling - colour segments by speed or road class from the
maneuversarray. - Custom tile style - replace the demo style URL with any MapLibre-compatible style JSON.