خريطة التتبع هي الشاشة الأعلى تفاعلًا في أي تطبيق لتوصيل الطعام أو مشاركة الرحلات. فهي ما يحدق فيه العملاء أثناء الانتظار. وإتقانها، أي حركة سلسة وأوقات وصول دقيقة وخط مسار واضح، هو ما يفرق بين تطبيق يبدو احترافيًا وآخر يبدو غير موثوق.
في هذا الدرس نبني خريطة التتبع التي يراها العميل من الصفر: خادم خلفي يبث مواقع GPS عبر WebSocket، وواجهة أمامية تستقبلها وتحرك علامة السائق دون قفزات، وخط مسار من MapAtlas Routing API، وعرض مباشر لوقت الوصول المتوقع (ETA). التطبيق الكامل أقل من 70 سطرًا من JavaScript في جهة العميل، ومصمم ليتكامل مع أي خادم خلفي قادر على إرسال رسائل WebSocket.
تصلح هذه البنية لتوصيل الطعام والبقالة ومشاركة الرحلات والخدمات الميدانية، ولأي حالة استخدام تتحرك فيها مركبة نحو وجهة ثابتة بينما يتابعها العميل في الوقت الفعلي.
نظرة عامة على البنية
قبل كتابة الكود، من المفيد أن تفهم مسار البيانات:
- تطبيق السائق (هاتف محمول أو جهاز GPS) يرسل خط العرض وخط الطول إلى الخادم الخلفي كل 3 إلى 5 ثوانٍ.
- الخادم الخلفي (Node.js أو Python أو Go، حسب اختيارك) يحفظ آخر موقع معروف ويبثه عبر WebSocket إلى كل المشتركين المتصلين بالطلب.
- متصفح العميل يستقبل رسائل WebSocket ويحرك علامة على الخريطة بحركة انتقالية سلسة.
- Routing API يُستدعى مرة واحدة عند إنشاء الطلب لجلب المسار المخطط. ويُعرض الخط المتعدد (polyline) بعد فك ترميزه كطبقة خطوط.
- وقت الوصول المتوقع يُعاد حسابه بمقارنة المسافة المتبقية بمتوسط السرعة، أو بإعادة استدعاء Routing API من الموقع الحالي للسائق.
تنفيذ الخادم الخلفي خارج نطاق هذا الدرس، لكن أي خادم WebSocket يرسل رسائل بهذا التنسيق يعمل مع كود الواجهة الأمامية أدناه:
{
"type": "position_update",
"orderId": "order-8821",
"lat": 52.3741,
"lng": 4.8952,
"heading": 92,
"speed": 28,
"timestamp": 1738234521000
}
الخطوة 1: تهيئة الخريطة
جهّز الخريطة بحيث تتمركز على نقطة انطلاق التوصيل. يأتي استدعاء Routing API في الخطوة 4، لذا اكتفِ الآن بتهيئة مساحة الخريطة.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'tracking-map',
style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 14
});
// علامة الوجهة (المطعم أو نقطة الاستلام)
const destination = [4.9001, 52.3791];
new mapmetricsgl.Marker({ color: '#EF4444' })
.setLngLat(destination)
.setPopup(new mapmetricsgl.Popup().setHTML('<strong>Pickup point</strong>'))
.addTo(map);
الخطوة 2: علامة السائق مع الدوران حسب الاتجاه
أنشئ علامة السائق بشكل منفصل حتى تتمكن من تحديث موقعها مع كل إشارة GPS. ويتيح لك عنصر HTML مخصص تدوير أيقونة العلامة لتعكس اتجاه السائق، وهي تفصيلة صغيرة تجعل التتبع أكثر واقعية بكثير.
// عنصر مخصص حتى نتمكن من تدويره
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)`;
}
الخطوة 3: اتصال WebSocket والحركة الانتقالية السلسة
هذا هو جوهر خريطة التتبع. الاتصال بـ WebSocket سطر واحد، أما الجزء المهم فهو حساب المواقع الوسيطة للعلامة بين إشارات GPS حتى تنزلق بسلاسة بدلًا من أن تقفز من مكان إلى آخر.
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) {
// حركة سلسة من الموقع السابق إلى الموقع الجديد
interpolateMarker(prevPos.lat, prevPos.lng, lat, lng, 400);
} else {
// أول إشارة، ضع العلامة فورًا
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.');
});
تتناسب نافذة الحركة الانتقالية البالغة 400 مللي ثانية بشكل جيد مع الفاصل المعتاد بين إشارات GPS، وهو من 3 إلى 5 ثوانٍ. تتأخر العلامة دائمًا قليلًا عن الواقع، لكنها لا تقفز بشكل ملحوظ أبدًا.
الخطوة 4: رسم المسار المخطط من Routing API
اجلب المسار الكامل عند إسناد الطلب. احفظ إحداثيات الخط المتعدد وارسمها كطبقة خطوط GeoJSON. يغطي درس Route Optimization API سيناريوهات التوقفات المتعددة، أما في توصيل بسيط من نقطة إلى أخرى فالطلب مباشر.
async function fetchAndDrawRoute(originLat, originLng, destLat, destLng) {
const url = new URL('https://gateway.mapmetrics-atlas.net/directions/');
url.searchParams.set('token', 'YOUR_API_KEY');
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
locations: [
{ lat: originLat, lon: originLng },
{ lat: destLat, lon: destLng },
],
costing: 'auto',
format: 'osrm',
shape_format: 'geojson',
}),
});
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; // بالثواني
}
الخطوة 5: حساب وقت الوصول المتوقع وعرضه
احسب وقت الوصول المتوقع بمقارنة الموقع الحالي للسائق بالوجهة. وللحصول على دقة عالية، أعد استدعاء Routing API من الموقع الحالي للسائق كل 30 ثانية للحصول على تقدير محدث لزمن الرحلة.
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);
// تقدير سريع في جهة العميل (بافتراض متوسط سرعة 30 كم/س داخل المدينة)
const minutesEstimate = Math.ceil((distKm / 30) * 60);
document.getElementById('eta').textContent =
distKm < 0.1
? 'Arriving now'
: `ETA: ${minutesEstimate} min (${distKm.toFixed(1)} km)`;
// إعادة الجلب من Routing API كل 30 ثانية لزيادة الدقة
const now = Date.now();
if (now - lastRouteFetch > 30_000) {
lastRouteFetch = now;
const url = new URL('https://gateway.mapmetrics-atlas.net/directions/');
url.searchParams.set('token', 'YOUR_API_KEY');
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
locations: [
{ lat: driverLat, lon: driverLng },
{ lat: destLat, lon: destLng },
],
costing: 'auto',
format: 'osrm',
shape_format: 'geojson',
}),
});
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 الخاصة بالسائق بيانات شخصية وفق المادة 4(1) من GDPR. ومبادئ GDPR التي تنطبق هنا هي تقليل البيانات وتقييد مدة التخزين الواردة في المادة 5:
تقليل البيانات: تتبّع الحقول التي تحتاجها للتوزيع فقط، أي الموقع والاتجاه والسرعة. لا تسجل سجل GPS الخام بما يتجاوز الحاجة التشغيلية.
حدود الاحتفاظ: يجب حذف بيانات تتبع الرحلة التفصيلية أو إخفاء هويتها بشكل لا رجعة فيه بعد اكتمال الطلب. ويمكن الاحتفاظ ببيانات المسارات المجمعة (دون ربطها بسائق بعينه) لفترة أطول لتحسين الشبكة.
الأساس القانوني: عادةً ما تغطي المصلحة المشروعة وفق المادة 6(1)(f)، المدعومة باختبار موازنة موثق، أو تنفيذ العقد وفق المادة 6(1)(b) تتبع التوزيع في الوقت الفعلي. أما أي استخدام ثانوي لبيانات التتبع (التحليلات أو المقارنات المعيارية)، فتحتاج إلى توثيق أساس قانوني منفصل له.
الشفافية مع السائقين: أدرج إفصاحًا واضحًا عن التتبع ضمن إجراءات انضمام السائقين. يجب إبلاغ السائقين بما يُجمع، ومدة الاحتفاظ به، ومن يمكنه الوصول إليه.
موقع تخزين البيانات: يعالج MapAtlas جميع طلبات API داخل الاتحاد الأوروبي. وهذا يلغي مسألة نقل البيانات إلى دول ثالثة. راجع دليل المطور الأوروبي إلى واجهات خرائط متوافقة مع GDPR للاطلاع على صورة الامتثال كاملة.
وفي حالة استخدام قطاع مشاركة الرحلات والتنقل تحديدًا، يوفر MapAtlas وثائق DPA وضمانات الخوادم الأوروبية بشكل قياسي. وتغطي صفحة قطاع اللوجستيات والتوصيل سيناريوهات الأساطيل وتعدد السائقين.
تجهيز الميزة لبيئة الإنتاج
قبل إطلاق ميزة التتبع للعملاء، تحقق من هذه النقاط:
- إعادة اتصال WebSocket: أضف
ws.addEventListener('close', reconnect)مع تأخير أُسّي متزايد بين المحاولات. فشبكات الهاتف المحمول تقطع الاتصال كثيرًا. - التعامل مع المواقع القديمة: إذا لم يصل أي تحديث خلال 15 ثانية، فاعرض حالة "جارٍ تحديد موقع السائق" بدلًا من ترك آخر موقع ظاهرًا.
- اكتشاف الوصول: عندما يتحقق الشرط
distKm < 0.1، فعّل حالة "وصل"، وأغلق اتصال WebSocket، واعرض شاشة تأكيد. - الكاميرا تتبع السائق: استدعِ
map.panTo([lng, lat])مع كل تحديث للموقع لإبقاء السائق في وسط الخريطة. وامنح المستخدمين زر "قفل" لتعطيل وضع التتبع إن أرادوا استكشاف الخريطة.
الخطوات التالية
- سجّل للحصول على مفتاح MapAtlas API مجاني وابدأ البناء
- اقرأ درس Route Optimization API لإضافة التوزيع متعدد التوقفات إلى تطبيق التوصيل
- استكشف درس خريطة العقارات لمثال آخر على طبقات خرائط ديناميكية مبنية على البيانات
الأسئلة الشائعة
كيف أعرض موقع السائق وهو يتحرك بسلاسة على الخريطة؟
تصل إشارات GPS كل بضع ثوانٍ، ما يسبب قفزات مرئية إذا حدّثت موقع العلامة مباشرة. تحرك الحركة الانتقالية السلسة العلامة بين الموقع السابق والموقع الجديد خلال مدة قصيرة (300 إلى 500 مللي ثانية)، باستخدام requestAnimationFrame لتحريك العلامة بخطوات صغيرة. وهذا يعطي انطباعًا بحركة متواصلة حتى مع تحديثات GPS المتباعدة.
هل تخضع بيانات موقع السائق لـ GDPR؟
نعم. إحداثيات GPS اللحظية للسائق بيانات شخصية وفق المادة 4 من GDPR. وينبغي للمنصات الاحتفاظ ببيانات التتبع للمدة اللازمة فقط، ويُعد حذفها أو إخفاء هويتها بعد اكتمال الرحلة الخيار الافتراضي الأكثر أمانًا. وتتطلب المعالجة أساسًا قانونيًا، ويجب الإفصاح عنها في إشعار الخصوصية الخاص بالسائق.
هل يمكنني استخدام MapAtlas Routing API لعرض المسار المخطط على خريطة التتبع؟
نعم. اجلب المسار من Routing API عند إنشاء الرحلة، وفك ترميز الخط المتعدد، ثم أضفه كطبقة خطوط GeoJSON على الخريطة. ومع تحرك السائق، يمكنك إعادة جلب المسار من الموقع الحالي لإعادة حساب وقت الوصول المتوقع بشكل ديناميكي.
الأسئلة الشائعة
كيف أعرض موقع السائق وهو يتحرك بسلاسة على الخريطة؟
تصل إشارات GPS كل بضع ثوانٍ، ما يسبب قفزات مرئية إذا حدّثت موقع العلامة مباشرة. تحرك الحركة الانتقالية السلسة العلامة بين الموقع السابق والموقع الجديد خلال مدة قصيرة (300 إلى 500 مللي ثانية)، باستخدام `requestAnimationFrame` لتحريك العلامة بخطوات صغيرة. وهذا يعطي انطباعًا بحركة متواصلة حتى مع تحديثات GPS المتباعدة.
هل تخضع بيانات موقع السائق لـ GDPR؟
نعم. إحداثيات GPS اللحظية للسائق بيانات شخصية وفق المادة 4 من GDPR. وينبغي للمنصات الاحتفاظ ببيانات التتبع للمدة اللازمة فقط، ويُعد حذفها أو إخفاء هويتها بعد اكتمال الرحلة الخيار الافتراضي الأكثر أمانًا. وتتطلب المعالجة أساسًا قانونيًا (عادةً المصلحة المشروعة لعمليات التوزيع أو تنفيذ العقد)، ويجب الإفصاح عنها في إشعار الخصوصية الخاص بالسائق.
هل يمكنني استخدام MapAtlas Routing API لعرض المسار المخطط على خريطة التتبع؟
نعم. اجلب المسار من Routing API عند إنشاء الرحلة، وفك ترميز الخط المتعدد، ثم أضفه كطبقة خطوط GeoJSON على الخريطة. ومع تحرك السائق، يمكنك إعادة جلب المسار من الموقع الحالي لإعادة حساب وقت الوصول المتوقع بشكل ديناميكي.

