خريطة البحث عن مقدمي الرعاية الصحية من أكثر الخرائط التي يبنيها المطور تأثيرًا في حياة الناس. فالمرضى الذين يستخدمونها غالبًا ما يكونون قلقين أو متألمين أو يتعاملون مع نظام لا يعرفونه. والخريطة التي تُحمَّل ببطء، أو تُرجع نتائج غير ذات صلة، أو ترسل رمزهم البريدي دون علمهم إلى خدمة خارجية مقرها الولايات المتحدة، ليست مجرد تجربة استخدام سيئة، بل مخالفة محتملة لـ GDPR.
في هذا الدرس نبني أداة بحث عن مقدمي الرعاية جاهزة للإنتاج: فلتر حسب التخصص، وبحث بالرمز البريدي باستخدام Geocoding API، وطبقة خط زمن متساوٍ (isochrone) تعرض "الأطباء على بعد 20 دقيقة"، وخريطة عاملة عبر MapAtlas Maps API. وكل ذلك مع امتثال لـ GDPR مدمج في البنية منذ البداية، لا مضاف لاحقًا.
التطبيق الكامل مكتوب بـ JavaScript خالص دون الحاجة إلى أي إطار عمل. ويعمل مع أي بيئة تقنية: React أو Vue أو HTML عادي أو WordPress أو نظام CMS قديم لدى مستشفى. ويغطي الدرس أيضًا السياق التنظيمي الأوروبي، مثل بيانات CQC في المملكة المتحدة، وKassenärztliche Vereinigung في ألمانيا، وCNAM في فرنسا، حتى تعرف من أين تحصل على بيانات مقدمي الرعاية بطريقة مشروعة.
لماذا تتحمل تطبيقات المواقع الصحية التزامات إضافية وفق GDPR
تجمع التطبيقات العادية بيانات شخصية (الاسم، البريد الإلكتروني، بيانات الاستخدام). أما تطبيقات الرعاية الصحية فتحمل خطرًا ثانيًا: استنتاج معلومات صحية من بيانات الموقع.
عندما يبحث المستخدم عن "أطباء قلب قرب SE1 7PB"، يكون رمزه البريدي بيانات شخصية. والجمع بين الرمز البريدي والتخصص الطبي قد يجعلها بيانات من فئة خاصة وفق المادة 9(1) من GDPR، أي بيانات تتعلق بالصحة، لأن محكمة العدل الأوروبية قضت بأن البيانات التي تكشف معلومات حساسة بشكل غير مباشر تندرج تحت المادة 9. وينطبق ذلك حتى لو لم تنشئ حساب مستخدم أو تخزن ملفًا شخصيًا.
المخاطر ملموسة:
- استدعاء Geocoding API من جهة العميل يتضمن الرمز البريدي الذي كتبه المستخدم يُرسل إلى خادم طرف ثالث. يرى هذا الخادم الرمز البريدي والتخصص المبحوث عنه وعنوان IP. ودون اتفاقية معالجة بيانات (DPA) وضمان بإبقاء البيانات داخل الاتحاد الأوروبي، يصبح هذا نقلًا إشكاليًا للبيانات.
- قد يحتفظ الإكمال التلقائي وسجل المتصفح بالرمز البريدي واستعلام التخصص على جهاز المستخدم. هذا خارج سيطرتك، لكن من المفيد ذكره في معلومات الخصوصية.
- إذا كنت تسجل استجابات API على الخادم (وهو أمر شائع لتصحيح الأخطاء)، فقد تسجل تركيبات من الرمز البريدي والتخصص دون أن تنتبه.
الحل هو وكيل (proxy) في الخادم الخلفي: يستقبل خادمك طلب البحث، ويستدعي MapAtlas Geocoding API، ويُرجع الإحداثيات فقط، فلا يُرسل الاستعلام الذي كتبه المريض من متصفحه إلى API طرف ثالث أبدًا. نشرح ذلك بالتفصيل في قسم التطبيق.
بنية بيانات مقدمي الرعاية
يجب أن تأتي بيانات مقدمي الرعاية من مصادر رسمية موثوقة، لا من مجموعات بيانات مجمعة بالكشط:
- المملكة المتحدة: Directory of Healthcare Services (Service Search) API من NHS England وقوائم مقدمي الرعاية المسجلين لدى CQC
- ألمانيا: البيانات المفتوحة من Kassenärztliche Bundesvereinigung (KBV) وArztsuche API
- فرنسا: Ameli API من CNAM وسجل répertoire RPPS
نظّم بيانات كل مقدم رعاية كعنصر GeoJSON:
const providers = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9051, 52.3680] },
properties: {
id: "prov-001",
name: "Dr. Maria van den Berg",
specialty: "general-practitioner",
accepting: true,
address: "Prinsengracht 263, 1016 GV Amsterdam",
phone: "+31 20 423 5678",
languages: ["Dutch", "English"],
nextAvailable: "2026-02-18"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8890, 52.3610] },
properties: {
id: "prov-002",
name: "Dr. Jan Smits",
specialty: "cardiologist",
accepting: false,
address: "Leidseplein 15, 1017 PS Amsterdam",
phone: "+31 20 612 3456",
languages: ["Dutch"],
nextAvailable: null
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9200, 52.3750] },
properties: {
id: "prov-003",
name: "Anna Fischer, MD",
specialty: "dermatologist",
accepting: true,
address: "Plantage Middenlaan 14, 1018 DD Amsterdam",
phone: "+31 20 789 0123",
languages: ["Dutch", "English", "German"],
nextAvailable: "2026-02-20"
}
}
]
};
الخطوة 1: تهيئة الخريطة مع فلتر التخصص
ابدأ بالخريطة وقائمة منسدلة للتخصصات. يحدّث الفلتر العلامات الظاهرة في جهة العميل دون أي طلب عبر الشبكة.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'provider-map',
style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
map.on('load', () => {
map.addSource('providers', {
type: 'geojson',
data: providers
});
// علامات مقدمي الرعاية، الأخضر = يقبل مرضى جددًا، الرمادي = مكتمل
map.addLayer({
id: 'provider-markers',
type: 'circle',
source: 'providers',
paint: {
'circle-radius': 10,
'circle-color': [
'case',
['==', ['get', 'accepting'], true], '#16A34A',
'#9CA3AF'
],
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
buildList(providers.features);
});
// فلتر التخصص
document.getElementById('specialty-select').addEventListener('change', (e) => {
const specialty = e.target.value;
if (specialty === 'all') {
map.setFilter('provider-markers', null);
} else {
map.setFilter('provider-markers', ['==', ['get', 'specialty'], specialty]);
}
});
الخطوة 2: البحث بالرمز البريدي عبر وكيل في الخادم الخلفي
بدلًا من استدعاء Geocoding API من المتصفح (ما يكشف الرمز البريدي للمريض لخادم طرف ثالث)، مرّر طلب الترميز الجغرافي عبر نقطة نهاية في خادمك الخلفي. يستدعي خادمك MapAtlas ويُرجع الإحداثيات فقط.
الواجهة الأمامية (متصفح المريض):
async function geocodePostcode(postcode) {
// استدعِ الخادم الخلفي الخاص بك، لا Geocoding API مباشرة
const res = await fetch(`/api/geocode?q=${encodeURIComponent(postcode)}`);
const { lat, lng } = await res.json();
return { lat, lng };
}
document.getElementById('postcode-form').addEventListener('submit', async (e) => {
e.preventDefault();
const postcode = document.getElementById('postcode-input').value.trim();
const { lat, lng } = await geocodePostcode(postcode);
map.flyTo({ center: [lng, lat], zoom: 13 });
fetchIsochrone(lat, lng);
});
الوكيل في الخادم الخلفي (Node.js/Express):
// /api/geocode، على الخادم فقط، والرمز البريدي لا يغادر بنيتك التحتية
app.get('/api/geocode', async (req, res) => {
const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
url.searchParams.set('text', req.query.q);
url.searchParams.set('token', process.env.MAPATLAS_API_KEY); // مفتاح على الخادم
url.searchParams.set('size', '1');
const upstream = await fetch(url.toString());
const data = await upstream.json();
if (!data.features?.length) {
return res.status(404).json({ error: 'Postcode not found' });
}
const [lng, lat] = data.features[0].geometry.coordinates;
// أرجع الإحداثيات فقط، دون تسجيل الرمز البريدي ودون التخصص في الاستجابة
res.json({ lat, lng });
});
يبقى مفتاح API على الخادم، ولا يتصل متصفح المريض بخدمة الترميز الجغرافي مباشرة أبدًا، ولا يمرر خادمك سوى الرمز البريدي نفسه، دون التخصص أو أي شيء يحدد هوية المريض.
الخطوة 3: طبقة خط الزمن المتساوي، "الأطباء على بعد 20 دقيقة"
يعرض مضلع خط الزمن المتساوي كل نقطة يمكن الوصول إليها خلال زمن قيادة محدد. وعرضه على أداة البحث عن مقدمي الرعاية يجيب عن أكثر أسئلة المريض عملية: "كم طبيبًا أستطيع الوصول إليه فعلًا؟"
async function fetchIsochrone(lat, lng) {
// استدعِ عبر الوكيل في الخادم الخلفي لأسباب الخصوصية نفسها
const res = await fetch(
`/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&costing=auto`
);
const geojson = await res.json();
// إزالة طبقة خط الزمن المتساوي الحالية إن وُجدت
if (map.getLayer('isochrone-fill')) map.removeLayer('isochrone-fill');
if (map.getLayer('isochrone-border')) map.removeLayer('isochrone-border');
if (map.getSource('isochrone')) map.removeSource('isochrone');
map.addSource('isochrone', { type: 'geojson', data: geojson });
map.addLayer({
id: 'isochrone-fill',
type: 'fill',
source: 'isochrone',
paint: {
'fill-color': '#16A34A',
'fill-opacity': 0.12
}
}, 'provider-markers'); // أدرجها أسفل العلامات حتى تظهر الدبابيس في الأعلى
map.addLayer({
id: 'isochrone-border',
type: 'line',
source: 'isochrone',
paint: {
'line-color': '#16A34A',
'line-width': 2,
'line-dasharray': [3, 2]
}
}, 'provider-markers');
}
وكيل خط الزمن المتساوي في الخادم الخلفي:
app.get('/api/isochrone', async (req, res) => {
const { lat, lng, minutes, costing } = req.query;
const url = new URL('https://gateway.mapmetrics-atlas.net/isochrone/');
url.searchParams.set('token', process.env.MAPATLAS_API_KEY);
const upstream = await fetch(url.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
locations: [{ lat: Number(lat), lon: Number(lng) }],
costing: costing || 'auto',
contours: [{ time: Number(minutes) }],
polygons: true,
}),
});
const data = await upstream.json();
res.json(data); // مضلع GeoJSON
});
يتعمق مقال شرح خرائط خطوط الزمن المتساوي في حالات استخدام خطوط الزمن المتساوي، ومنها خطوط النقل العام (المفيدة للمرضى الذين لا يملكون سيارة)، والطبقات متعددة النطاقات الزمنية التي تعرض مناطق 10 و20 و30 دقيقة معًا.
الخطوة 4: نافذة تفاصيل مقدم الرعاية
عندما ينقر المريض على علامة طبيب، اعرض التفاصيل الأساسية: الاسم، والتخصص، وحالة قبول المرضى، واللغات، وأقرب موعد متاح.
map.on('click', 'provider-markers', (e) => {
const {
name, specialty, accepting, address, phone, languages, nextAvailable
} = e.features[0].properties;
const coords = e.features[0].geometry.coordinates.slice();
const statusBadge = accepting
? '<span style="color:#16A34A;font-weight:600">Accepting patients</span>'
: '<span style="color:#9CA3AF">Not accepting new patients</span>';
const apptLine = accepting && nextAvailable
? `<p style="margin:4px 0">Next available: <strong>${nextAvailable}</strong></p>`
: '';
new mapmetricsgl.Popup({ maxWidth: '280px' })
.setLngLat(coords)
.setHTML(`
<strong style="font-size:15px">${name}</strong>
<p style="margin:4px 0;text-transform:capitalize">${specialty.replace('-', ' ')}</p>
${statusBadge}
${apptLine}
<p style="margin:6px 0;font-size:13px;color:#64748b">${address}</p>
<p style="margin:4px 0;font-size:13px">
Languages: ${Array.isArray(languages) ? languages.join(', ') : languages}
</p>
<a href="tel:${phone}" style="color:#2563EB">${phone}</a>
`)
.addTo(map);
});
map.on('mouseenter', 'provider-markers', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'provider-markers', () => { map.getCanvas().style.cursor = ''; });
الخطوة 5: لوحة قائمة متزامنة مع الخريطة
تساعد القائمة المعروضة بجانب الخريطة مستخدمي قارئات الشاشة، ومن يفضلون التمرير على النقر على الدبابيس.
function buildList(features) {
const list = document.getElementById('provider-list');
list.innerHTML = '';
features.forEach(({ properties, geometry }) => {
const { name, specialty, accepting, address, nextAvailable } = properties;
const card = document.createElement('div');
card.className = `provider-card ${accepting ? 'accepting' : 'full'}`;
card.innerHTML = `
<strong>${name}</strong>
<p>${specialty.replace('-', ' ')}</p>
<p style="font-size:12px;color:#64748b">${address}</p>
<small>${accepting ? `Next: ${nextAvailable || 'call to confirm'}` : 'Not accepting'}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 15 });
});
list.appendChild(card);
});
}
تُعرض القائمة بالترتيب الذي تصل به بيانات مقدمي الرعاية. ولترتيبها حسب الزمن الفعلي للوصول إلى كل مقدم رعاية، اجعل خادمك الخلفي يرسل نقطة انطلاق البحث ومجموعة مقدمي الرعاية بعد التصفية إلى Distance Matrix API، ثم رتّب البطاقات حسب أزمنة التنقل التي يُرجعها. ومرّر هذا الطلب عبر الوكيل نفسه في الخادم الذي تستخدمه للترميز الجغرافي، للسبب المذكور في قائمة التحقق أدناه، وستُعالج البيانات داخل الاتحاد الأوروبي.
قائمة التحقق لتطبيق GDPR
إذا كنت تطور تطبيقات رعاية صحية في الاتحاد الأوروبي، فتعامل مع هذه القائمة كحد أدنى للامتثال:
- وكيل في الخادم الخلفي للترميز الجغرافي: يجب ألا تُرسل الرموز البريدية للمرضى من جهة العميل إلى واجهات API لطرف ثالث.
- عدم تسجيل مصطلحات البحث في الجلسات: إذا كنت تسجل الطلبات لتصحيح الأخطاء، فاحذف الرمز البريدي قبل الكتابة إلى السجلات.
- إشعار الخصوصية: يجب أن يفصح إشعار الخصوصية في تطبيقك عن معالجة بيانات الموقع للعثور على مقدمي رعاية قريبين، وعن الأساس القانوني وفق المادة 6، وعن شرط من شروط المادة 9(2) مثل الموافقة الصريحة حين يمكن أن تكشف بيانات البحث معلومات صحية، وعن مدة الاحتفاظ.
- الموافقة على الوصول إلى الموقع: إذا كنت تستخدم Geolocation API في المتصفح (
navigator.geolocation.getCurrentPosition)، فاعرض توضيحًا واضحًا للغرض قبل إظهار طلب الإذن من المتصفح. - موقع تخزين البيانات: يعالج MapAtlas جميع طلبات API داخل الاتحاد الأوروبي. أشر إلى اتفاقية معالجة البيانات من MapAtlas عند إعداد سجلات أنشطة المعالجة.
- الاحتفاظ بالبيانات: إذا كنت تخزن نتائج خطوط الزمن المتساوي أو إحداثيات البحث مؤقتًا على الخادم لتحسين الأداء، فحدد مدة احتفاظ (مثل 24 ساعة) واجعل الحذف تلقائيًا.
يغطي دليل المطور الأوروبي إلى واجهات خرائط متوافقة مع GDPR اتفاقيات المعالجة وموقع تخزين البيانات وأنماط الموافقة بالتفصيل. وتعرض صفحة قطاع خدمات الرعاية الصحية ميزات MapAtlas المحددة ذات الصلة بتطبيقات الصحة الرقمية.
ما الذي تبنيه بعد ذلك
- سجّل للحصول على مفتاح MapAtlas API مجاني، مفتاح واحد للخرائط والترميز الجغرافي والتوجيه وخطوط الزمن المتساوي
- أضف نمط درس محدد مواقع المتاجر لشبكات العيادات التي تضم أكثر من بضعة مواقع
- استكشف صفحة قدرات Geocoding API للبحث بالرمز البريدي، والترميز الجغرافي العكسي، والتحقق المجمع من عناوين مقدمي الرعاية
الأسئلة الشائعة
لماذا ينطبق GDPR على خريطة البحث عن الأطباء حتى لو لم أخزن بيانات المرضى؟
يكشف موقع البحث الذي يدخله المريض في أداة البحث عن الرعاية الصحية مكان سكنه، وبالاستنتاج الحالات الصحية التي قد يعاني منها (مثل البحث عن أطباء أورام قرب رمز بريدي معين). ووفق المادة 9 من GDPR، قد تُصنف الاستنتاجات الصحية المستمدة من بيانات الموقع بيانات من فئة خاصة. وحتى دون تخزين ملفات شخصية، فإن طلبات الترميز الجغرافي اللحظية المرسلة من جهة العميل إلى واجهات API لطرف ثالث تُنشئ سجل معالجة يتطلب أساسًا قانونيًا وإفصاحًا.
ما هو خط الزمن المتساوي ولماذا هو مفيد في أداة البحث عن مقدمي الرعاية؟
خط الزمن المتساوي (isochrone) مضلع يعرض كل نقطة يمكن الوصول إليها خلال زمن تنقل محدد من موقع انطلاق. وفي أداة البحث عن الرعاية الصحية، تجيب طبقة خط الزمن المتساوي عن سؤال "أي الأطباء على بعد 20 دقيقة بالسيارة؟"، وهو سؤال أنفع بكثير من المسافة الخام، لأنه يراعي الطرق وحدود السرعة وأنماط حركة المرور.
هل يمكنني استخدام MapAtlas في تطبيق موجه للمرضى تابع لـ NHS أو للصحة العامة؟
نعم. يُستضاف MapAtlas داخل الاتحاد الأوروبي في مراكز بيانات حاصلة على شهادة ISO 27001، ويوفر اتفاقية معالجة بيانات متوافقة مع GDPR وفق المادة 28. وهذا يلبي متطلبات المشتريات المعتادة في القطاع العام. كما أن إبقاء المعالجة داخل الاتحاد الأوروبي يزيل أسئلة النقل الدولي للبيانات التي تطرحها فرق المشتريات في القطاع العام وقطاع الصحة بشكل معتاد.
الأسئلة الشائعة
لماذا ينطبق GDPR على خريطة البحث عن الأطباء حتى لو لم أخزن بيانات المرضى؟
يكشف موقع البحث الذي يدخله المريض في أداة البحث عن الرعاية الصحية مكان سكنه، وبالاستنتاج الحالات الصحية التي قد يعاني منها (مثل البحث عن أطباء أورام قرب رمز بريدي معين). ووفق المادة 9 من GDPR، قد تُصنف الاستنتاجات الصحية المستمدة من بيانات الموقع بيانات من فئة خاصة. وحتى دون تخزين ملفات شخصية، فإن طلبات الترميز الجغرافي اللحظية المرسلة من جهة العميل إلى واجهات API لطرف ثالث تُنشئ سجل معالجة يتطلب أساسًا قانونيًا وإفصاحًا.
ما هو خط الزمن المتساوي ولماذا هو مفيد في أداة البحث عن مقدمي الرعاية؟
خط الزمن المتساوي (isochrone) مضلع يعرض كل نقطة يمكن الوصول إليها خلال زمن تنقل محدد من موقع انطلاق. وفي أداة البحث عن الرعاية الصحية، تجيب طبقة خط الزمن المتساوي عن سؤال «أي الأطباء على بعد 20 دقيقة بالسيارة؟»، وهو سؤال أنفع بكثير من المسافة الخام، لأنه يراعي الطرق وحدود السرعة وأنماط حركة المرور.
هل يمكنني استخدام MapAtlas في تطبيق موجه للمرضى تابع لـ NHS أو للصحة العامة؟
نعم. يُستضاف MapAtlas داخل الاتحاد الأوروبي في مراكز بيانات حاصلة على شهادة ISO 27001، ويوفر اتفاقية معالجة بيانات متوافقة مع GDPR وفق المادة 28. وهذا يلبي متطلبات المشتريات المعتادة في القطاع العام. كما أن إبقاء المعالجة داخل الاتحاد الأوروبي يزيل أسئلة النقل الدولي للبيانات التي تطرحها فرق المشتريات في القطاع العام وقطاع الصحة بشكل معتاد.

