追踪地图是任何食物配送或拼车应用中参与度最高的屏幕。这是顾客等待时盯着看的东西。做对了,平顺的移动、准确的预计到达时间、清晰的路线,这是应用感觉专业和感觉不可靠之间的区别。
本教程从第一原则构建面向客户的追踪地图:一个通过WebSocket广播GPS位置的后端、一个接收它们并移动驾驶员标记而不跳跃的前端、来自MapAtlas Routing API的路线线和实时ETA显示。完整的实现不到70行客户端JavaScript,设计用于与任何可以推送WebSocket消息的后端集成。
该架构适用于食物配送、杂货店、拼车、现场服务以及任何其他车辆朝着固定目的地移动并且客户实时观察其发生的用例。
Architecture Overview
Before writing code, it helps to understand the data flow:
- Driver app (mobile, GPS hardware) sends latitude/longitude to your backend every 3–5 seconds.
- Backend (Node.js, Python, Go, your choice) persists the last known position and broadcasts it via WebSocket to all connected order subscribers.
- Customer browser receives WebSocket messages and moves a marker on the map using interpolated animation.
- Routing API is called once when the order is created to fetch the planned route. The decoded polyline is displayed as a line layer.
- ETA is recalculated by comparing distance remaining to average speed, or by re-calling the Routing API from the driver's current position.
The backend implementation is outside the scope of this tutorial, but any WebSocket server that sends messages in this format works with the frontend code below:
{
"type": "position_update",
"orderId": "order-8821",
"lat": 52.3741,
"lng": 4.8952,
"heading": 92,
"speed": 28,
"timestamp": 1738234521000
}
Step 1: Map Initialisation
Set up the map centred on the delivery origin. The Routing API call happens in Step 4, so for now just initialise the canvas.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'tracking-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 14
});
// Destination marker (restaurant or pickup point)
const destination = [4.9001, 52.3791];
new mapmetricsgl.Marker({ color: '#EF4444' })
.setLngLat(destination)
.setPopup(new mapmetricsgl.Popup().setHTML('<strong>Pickup point</strong>'))
.addTo(map);
Step 2: Driver Marker with Heading Rotation
Create the driver marker separately so you can update its position on each GPS ping. A custom HTML element lets you rotate the marker icon to reflect the driver's heading, a small detail that makes the tracking feel much more realistic.
// Custom element so we can rotate it
const driverEl = document.createElement('div');
driverEl.innerHTML = '🚗';
driverEl.style.cssText = 'font-size:28px;transform-origin:center;transition:transform 0.3s';
const driverMarker = new mapmetricsgl.Marker({ element: driverEl, anchor: 'center' })
.setLngLat([4.9041, 52.3676])
.addTo(map);
function setDriverHeading(heading) {
driverEl.style.transform = `rotate(${heading}deg)`;
}
Step 3: WebSocket Connection and Smooth Interpolation
This is the core of the tracking map. Connecting to the WebSocket is one line; the interesting part is interpolating the marker position between GPS pings so it slides smoothly rather than teleporting.
let prevPos = null; // { lat, lng }
let animFrame = null;
function interpolateMarker(fromLat, fromLng, toLat, toLng, durationMs) {
const startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const t = Math.min(elapsed / durationMs, 1); // 0 → 1
const lat = fromLat + (toLat - fromLat) * t;
const lng = fromLng + (toLng - fromLng) * t;
driverMarker.setLngLat([lng, lat]);
if (t < 1) {
animFrame = requestAnimationFrame(step);
}
}
if (animFrame) cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(step);
}
const ws = new WebSocket('wss://your-backend.example.com/track/order-8821');
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== 'position_update') return;
const { lat, lng, heading } = msg;
if (prevPos) {
// Smooth animation from previous to new position
interpolateMarker(prevPos.lat, prevPos.lng, lat, lng, 400);
} else {
// First ping, place marker immediately
driverMarker.setLngLat([lng, lat]);
map.flyTo({ center: [lng, lat], zoom: 15 });
}
setDriverHeading(heading);
updateETA(lat, lng);
prevPos = { lat, lng };
});
ws.addEventListener('close', () => {
console.log('Driver has arrived or connection closed.');
});
The 400ms interpolation window matches a typical GPS ping interval of 3–5 seconds reasonably well, the marker is always slightly behind reality but never jumps noticeably.
Step 4: Draw the Planned Route from the Routing API
Fetch the full route when the order is assigned. Store the polyline coordinates and draw them as a GeoJSON line layer. The Route Optimization API tutorial covers multi-stop scenarios; for a simple A-to-B delivery the request is straightforward.
async function fetchAndDrawRoute(originLat, originLng, destLat, destLng) {
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${originLat},${originLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.routes?.length) return;
const route = data.routes[0];
map.on('load', () => {
map.addSource('route', {
type: 'geojson',
data: {
type: 'Feature',
geometry: route.geometry // GeoJSON LineString
}
});
map.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: {
'line-color': '#3B82F6',
'line-width': 4,
'line-opacity': 0.8
}
});
});
return route.duration; // seconds
}
Step 5: ETA Calculation and Display
Calculate ETA by comparing the driver's current position to the destination. For high accuracy, re-call the Routing API from the driver's current position every 30 seconds to get a fresh travel time estimate.
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
let lastRouteFetch = 0;
async function updateETA(driverLat, driverLng) {
const [destLng, destLat] = destination;
const distKm = haversineKm(driverLat, driverLng, destLat, destLng);
// Fast client-side estimate (assume 30 km/h urban average)
const minutesEstimate = Math.ceil((distKm / 30) * 60);
document.getElementById('eta').textContent =
distKm < 0.1
? 'Arriving now'
: `ETA: ${minutesEstimate} min (${distKm.toFixed(1)} km)`;
// Re-fetch from Routing API every 30 seconds for accuracy
const now = Date.now();
if (now - lastRouteFetch > 30_000) {
lastRouteFetch = now;
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${driverLat},${driverLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (data.routes?.length) {
const mins = Math.ceil(data.routes[0].duration / 60);
document.getElementById('eta').textContent = `ETA: ${mins} min`;
}
}
}
司机追踪的GDPR考虑事项
驾驶员GPS坐标是GDPR第4(1)条下的个人数据。管理EU食物配送和拼车平台的规定在这一点上是不模糊的:
数据最小化:仅追踪您需要的字段以用于调度,位置、航向、速度。不要在操作上不需要的范围之外记录原始GPS历史。
保留限制:一旦订单完成,应删除或不可逆地匿名化粒度行程追踪数据。聚合路线数据(不与单个驾驶员相关联)可能会保留更长时间以进行网络优化。
法律基础:第6(1)(f)条下的正当利益涵盖实时调度追踪。对于追踪数据的任何次要使用(分析、基准测试),您需要记录单独的基础。
驾驶员透明度:在司机入职中包括明确的追踪披露。必须告知驾驶员收集了什么、保留多久以及谁可以访问它。
数据驻留:MapAtlas在EU内处理所有API请求。这消除了与基于美国的地图提供商相关的第三国转移问题。有关完整的合规图景,请参阅EU Developer's Guide to GDPR-Compliant Map APIs。
对于ridesharing and mobility industry用例,MapAtlas包括作为标准的DPA文档和EU服务器保证。logistics and delivery行业页面涵盖车队和多驾驶员场景。
Production Hardening
Before shipping a tracking feature to customers, check these items:
- WebSocket reconnection: Add
ws.addEventListener('close', reconnect)with exponential backoff. Mobile networks drop connections frequently. - Stale position handling: If no update arrives for 15 seconds, show a "locating driver" state rather than leaving the last position visible.
- Arrival detection: When
distKm < 0.1, trigger an "arrived" state, close the WebSocket, and show a confirmation screen. - Camera follows driver: Call
map.panTo([lng, lat])on each position update to keep the driver centred. Give users a "lock" toggle to disable follow mode if they want to explore the map.
Next Steps
- Sign up for your free MapAtlas API key and start building
- Read the Route Optimization API tutorial to add multi-stop dispatch to your delivery app
- Explore the Real Estate Property Map tutorial for another example of dynamic, data-driven map layers
Frequently Asked Questions
How do I show a driver's location moving smoothly on a map?
GPS pings arrive every few seconds, creating visible jumps if you update the marker position directly. Smooth interpolation animates the marker between the previous position and the new one over a short duration (300–500ms), using requestAnimationFrame to move the marker in small increments. This gives the appearance of continuous movement even with infrequent GPS updates.
Is driver location data subject to GDPR?
Yes. A driver's real-time GPS coordinates are personal data under GDPR Article 4. EU food delivery and rideshare platforms must minimise retention, tracking data should be deleted or anonymised once the trip is complete. Processing requires a legal basis and must be disclosed in the driver's privacy notice.
Can I use the MapAtlas Routing API to show the planned route on the tracking map?
Yes. Fetch the route from the Routing API when a trip is created, decode the polyline, and add it as a GeoJSON line layer on the map. As the driver moves, you can optionally re-fetch the route from the current position to recalculate the ETA dynamically.
