import type { Map as MlMap } from 'maplibre-gl'; /** * تحميل آيقونات الـ POI عند الطلب من ملفات SVG في /icons. * * لماذا لا sprite: الستايل كان يشير إلى demotiles.maplibre.org (خادم MapLibre * التجريبي). حين يفشل تحميله يرسم MapLibre نص التسمية بلا آيقونة وبصمت — فتظهر * الخريطة بأسماء بلا رموز. البديل المعتاد هو بناء sprite sheet، لكنه يتطلب * سلسلة أدوات (cairo/resvg) واستضافة ملف إضافي ومزامنته مع كل آيقونة جديدة. * * `styleimagemissing` ينطلق مرة واحدة لكل آيقونة تطلبها طبقة ولا يجدها المحرك، * فنحمّلها وقتها من الـ SVG الموجود أصلاً في المستودع. لا خطوة بناء، ولا ملف * يُستضاف، وإضافة آيقونة جديدة = إسقاط ملف SVG في المجلد فقط. * * ALIASES تسدّ الفجوة بين ما تطلبه طبقات الستايل وما هو متوفر فعلاً كملف. */ const ALIASES: Record = { rail: 'train', college: 'tourist', school: 'tourist', cafe: 'cafe', restaurant: 'restaurant', }; const SIZE = 24; export function attachIconLoader(map: MlMap): () => void { const pending = new Set(); const onMissing = (e: { id: string }) => { const id = e.id; // الحدث قد يتكرر لنفس الآيقونة قبل اكتمال التحميل غير المتزامن if (!id || pending.has(id) || map.hasImage(id)) return; pending.add(id); const file = ALIASES[id] ?? id; const img = new Image(SIZE, SIZE); img.crossOrigin = 'anonymous'; img.onload = () => { // الفحص مكرر عمداً: التحميل غير متزامن وقد يكون الستايل تبدّل أثناءه if (!map.hasImage(id)) { try { map.addImage(id, img, { pixelRatio: 1 }); } catch { /* الستايل تبدّل تحتنا — تُطلب مجدداً عند الحاجة */ } } pending.delete(id); }; img.onerror = () => { // بلا ملف مقابل: نضيف بكسلاً شفافاً حتى لا يعيد MapLibre إطلاق الحدث // في كل إطار. النص يظهر بلا رمز، وهو أفضل من حلقة لا تنتهي. if (!map.hasImage(id)) { map.addImage(id, { width: 1, height: 1, data: new Uint8Array(4) }); } pending.delete(id); console.warn(`[mapIcons] لا يوجد /icons/${file}.svg للآيقونة "${id}"`); }; img.src = `/icons/${file}.svg`; }; map.on('styleimagemissing', onMissing); return () => map.off('styleimagemissing', onMissing); }