عندما تحتاج خريطة داخل تطبيقك، لصفحة الفروع أو لتتبع المندوبين أو لعرض مواقع العملاء، فالطريقة البسيطة هي أن تنسخ كود Google Maps أو Mapbox، وتضع مفتاح API، وتنتهي المهمة في ربع ساعة. ولكن بعد ذلك سوف تجد أن كل زيارة للخريطة تمر على سيرفر شركة أخرى، وأن الفاتورة تكبر مع عدد الزوار، وأن الشروط والأسعار يمكن أن تتغير في أي وقت، وأن مواقع عملائك ومندوبيك تظهر في سجلات غيرك. وقد يتساءل البعض: لماذا لا أستخدم خرائط OpenStreetMap المجانية مباشرة؟ والإجابة أن البيانات مفتوحة فعلاً، ولكن سيرفرات المربعات Tiles التي تعرضها على موقعهم ليست خدمة مجانية لتطبيقات الآخرين، وسوف نرى شروطها بعد قليل.
والحل الذي نشرحه هنا هو أن تكون الخريطة كلها على سيرفرك: بيانات OpenStreetMap، في ملف واحد بصيغة PMTiles، يقدمه أي سيرفر ويب عادي، وترسمه مكتبة MapLibre GL JS في متصفح الزائر. وهذا هو المقال الأول في مرحلة الخدمات المتخصصة: الخرائط والبث المباشر ومعالجة الوسائط، وسوف نناقش فيه ما يلي:
- ما هي بيانات OpenStreetMap، وما الذي تطلبه رخصة ODbL منك عند عرضها.
- المربعات النقطية Raster Tiles والمربعات المتجهة Vector Tiles، ولماذا تغير الثانية طريقة استضافة الخرائط.
- صيغة PMTiles وطلبات النطاق HTTP Range Requests التي تجعل ملفاً واحداً يكفي دون سيرفر خرائط.
- مختبر كامل على Docker: خريطة الرياض من سيرفرك بالعربية والإنجليزية، مع المخرجات الحقيقية.
- اختبار صادق للنصوص العربية على الخريطة، ونكسر الإعداد عمداً بسيرفر لا يدعم Range ونقرأ الخطأ.
بيانات OpenStreetMap ورخصتها
OpenStreetMap قاعدة بيانات جغرافية للعالم يحررها متطوعون، فيها الطرق والمباني والأحياء والمحلات وأسماؤها بلغات كثيرة، ومنها الأسماء العربية في name:ar والإنجليزية في name:en. والمهم هنا أنها بيانات Data وليست خريطة جاهزة، فأنت تأخذ البيانات وتصنع منها خريطتك بالألوان واللغة التي تريدها.
والبيانات مرخصة برخصة Open Database License واختصاراً ODbL، وملخصها الرسمي يطلب منك ثلاثة أشياء:
- نسبة البيانات Attribution: أي استخدام علني للبيانات أو لعمل ناتج منها يجب أن ينسب إليها، وإرشادات النسبة من مؤسسة OpenStreetMap تقول إن النسبة تكون إلى «OpenStreetMap»، ويفضل أن تكون رابطاً إلى صفحة
openstreetmap.org/copyright، وموضعها التقليدي الزاوية السفلية اليمنى من الخريطة مع قبول أي زاوية. - المشاركة بالمثل Share-Alike: إذا عدلت قاعدة البيانات نفسها ونشرت النسخة المعدلة، أو استخدمتها علناً، فعليك أن تتيح هذه النسخة بالرخصة نفسها.
- إبقاؤها مفتوحة Keep open: إذا وزعت البيانات بقيود تقنية فعليك أن توزع معها نسخة دون هذه القيود.
وقد يتساءل البعض: هل يعني ذلك أن مواقع فروعي وعملائي تصبح مفتوحة إذا عرضتها فوق خريطة OpenStreetMap؟ والإجابة في الأسئلة القانونية الشائعة للمؤسسة: الخريطة المرسومة تعتبر عملاً ناتجاً Produced Work يمكنك ترخيصه كما تريد، وإذا جمعت بياناتك المستقلة مع بيانات OSM كقواعد منفصلة فهذه قاعدة مجمعة Collective Database وليست قاعدة مشتقة Derivative Database، لذلك لا يلزمك نشر بياناتك، ويبقى عليك شرط النسبة الظاهرة على الخريطة. أما إذا عدلت بيانات OSM نفسها، كأن تصحح الطرق في نسختك وتعرضها، فالنسخة المعدلة تبقى تحت ODbL.
ولماذا لا نأخذ المربعات من سيرفرات OpenStreetMap مباشرة؟ والسبب أن سياسة استخدام المربعات تقول إن الخدمة على أفضل جهد دون أي اتفاقية مستوى خدمة SLA، وإن الاستخدام الثقيل قد يحجب دون إنذار، وتمنع التحميل الجماعي Bulk Downloading والتحميل المسبق للمربعات التي لا يراها المستخدم، وتنصح من لا تناسبه هذه الشروط باستضافة الخرائط بنفسه أو بمزود آخر، وهذا بالضبط ما سوف نفعله.
المربعات Tiles: النقطية والمتجهة
الخريطة على الويب لا تحمل كصورة واحدة، وإنما تقسم إلى مربعات Tiles في هرم من مستويات التقريب Zoom: المستوى 0 فيه مربع واحد للعالم كله، والمستوى 1 فيه أربعة، وكل مستوى فيه أربعة أضعاف ما قبله، حتى يصبح في المستوى 15 أكثر من مليار مربع، ولكل مربع عنوان من ثلاثة أرقام z/x/y. والمتصفح يطلب فقط المربعات التي تظهر على الشاشة في المستوى الحالي، والصورة التالية تبين الهرم والفرق بين نوعي المربعات:

- المربع النقطي Raster Tile: صورة PNG أو JPEG مرسومة على السيرفر، فاللغة والألوان فيها ثابتة، وتحتاج مجموعة مربعات كاملة لكل لغة ولكل تنسيق، وتبدو ضبابية بين مستويين، ولكنها تعمل في أي عارض ولا تحتاج شيئاً من المتصفح.
- المربع المتجه Vector Tile: بيانات مضغوطة بصيغة Mapbox Vector Tile واختصاراً MVT، فيها الخطوط والمضلعات والنقاط وأسماؤها بكل اللغات، والمتصفح يرسمها بنفسه بملف تنسيق Style، لذلك تعرض الخريطة نفسها بالعربية أو بالإنجليزية أو بألوان مختلفة من المربعات نفسها، وتبقى حادة عند كل تقريب، ولكنها تحتاج WebGL في المتصفح، وملفات خطوط Fonts، وملف تنسيق.
والسبب الذي يجعل المربعات المتجهة مناسبة للاستضافة الذاتية أن السيرفر لم يعد يرسم شيئاً، فكل ما يفعله أن يرسل بايتات جاهزة، والرسم كله على جهاز الزائر.
PMTiles: ملف واحد بدل سيرفر خرائط
الطريقة التقليدية لتقديم المربعات المتجهة هي سيرفر خرائط Tile Server يقرأ من قاعدة SQLite بصيغة MBTiles أو من PostGIS ويجيب على كل طلب z/x/y، وهذه خدمة إضافية تشغلها وتراقبها وتحدثها. أما PMTiles فهي صيغة أرشيف من ملف واحد، والفكرة أن المتصفح يطلب من الملف الأجزاء التي يحتاجها فقط عبر طلبات النطاق HTTP Range Requests، وهي ميزة في HTTP يدعمها أي سيرفر ويب وأي تخزين كائنات Object Storage مثل S3، فلا تحتاج أي برنامج على السيرفر غير سيرفر الملفات. والتوثيق يلخص الفرق: MBTiles مصممة لتقرأ من القرص داخل عملية سيرفر، وPMTiles مصممة لتقرأ عن بعد عبر HTTP مع طلبين وسيطين على الأكثر يمكن تخزينهما مؤقتاً Cache. والصورة التالية تبين ما يحدث عند فتح الخريطة:

ومواصفة PMTiles v3 تحدد أن الملف يبدأ بترويسة Header طولها 127 بايت، وأن الدليل الأول Root Directory يجب أن يكون داخل أول 16384 بايت، حتى يجلبهما الكلاينت في طلب واحد، ثم الأدلة تحول عنوان كل مربع z/x/y إلى موضع وطول داخل الملف. ومصدر الملف عندنا هو الخريطة الأساسية من Protomaps، وهي بناء يومي للعالم كله من بيانات OpenStreetMap من المستوى 0 إلى 15، وصفحة التحميل تقول إنها موزعة كعمل ناتج Produced Work تحت ODbL مع اشتراط نسبة OpenStreetMap، وإن الربط المباشر Hotlinking بملفات التحميل غير مستحب، وإن عليك نسخ الملف إلى تخزينك. والكود والتنسيق في مستودع basemaps برخصة BSD-3 والتصميم البصري CC0.
MapLibre GL JS: مكتبة الرسم في المتصفح
MapLibre GL JS مكتبة مفتوحة المصدر بترخيص BSD-3-Clause ترسم المربعات المتجهة بال WebGL، وكما يذكر ملف المشروع فقد بدأت نسخة مفتوحة من mapbox-gl-js قبل أن تنتقل Mapbox إلى رخصة غير مفتوحة في ديسمبر 2020. وفي الطبقة نفسها نستخدم ثلاث مكتبات صغيرة: pmtiles التي تعلم MapLibre قراءة روابط pmtiles:// بطلبات النطاق، و@protomaps/basemaps التي تبني طبقات التنسيق Layers بلغة تختارها، وملفات الخطوط والرموز Sprites من مستودع basemaps-assets، والخطوط فيه برخصة SIL Open Font License والرموز مشتقة من أيقونات برخصة MIT.
التجربة العملية: خريطة الرياض من سيرفرك
سوف نقص مستطيل مدينة الرياض من آخر بناء يومي لخريطة Protomaps، ونقدمه مع ملفات MapLibre والخطوط من nginx على 127.0.0.1 فقط، ثم نفتح الخريطة في المتصفح ونتحقق أن كل الطلبات خرجت إلى سيرفرنا وحده. والصورة التالية تبين المسار كاملاً ومن يرخص كل جزء:

قص المنطقة التي تحتاجها
نبدأ بأداة go-pmtiles من Docker Image المثبتة على الإصدار v1.31.2، ونقرأ وصف آخر بناء يومي دون تحميله:
mkdir -p maps-lab/web/tiles && cd maps-lab
docker run --rm --name lab-maps-pmtiles protomaps/go-pmtiles:v1.31.2 \
show https://build.protomaps.com/20261008.pmtilesوالمخرج سوف يكون كما يلي (اختصرنا بعض الأسطر):
pmtiles spec version: 3
tile type: mvt
bounds: (long: -180.000000, lat: -85.051129) (long: 180.000000, lat: 85.051129)
min zoom: 0
max zoom: 15
addressed tiles count: 1431655765
tile entries count: 178156644
tile contents count: 136266273
clustered: true
tile compression: gzip
planetiler:osm:osmosisreplicationtime 2026-10-08T04:00:00Z
planetiler:version 0.10.2
version 4.15.2
attribution <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap</a>
description Basemap layers derived from OpenStreetMap and Natural Earth
name Protomaps Basemapفي المخرج أعلاه لاحظ التالي:
- الأمر قرأ الترويسة والبيانات الوصفية من ملف على الإنترنت حجمه 138,703,391,160 بايت، أي قرابة 139 جيجابايت بحسب قائمة البناءات، دون أن يحمله، وهذه هي طلبات النطاق نفسها التي سوف يستخدمها المتصفح.
- البيانات من OpenStreetMap حتى الساعة الرابعة من صباح اليوم نفسه، وبنيت بأداة Planetiler، والنسبة المطلوبة مكتوبة داخل الملف في
attribution.
الآن نقص مستطيل الرياض بالإحداثيات بالترتيب: خط الطول الغربي، ثم خط العرض الجنوبي، ثم الشرقي، ثم الشمالي:
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 \
extract https://build.protomaps.com/20261008.pmtiles /data/riyadh.pmtiles \
--bbox=46.45,24.45,47.05,25.00والمخرج سوف يكون كما يلي (حذفنا أسطر شريط التقدم):
2026/10/08 19:58:02 extract.go:401: fetching 9 dirs, 9 chunks, 8 requests
2026/10/08 19:58:07 extract.go:441: Region tiles 4249, result tile entries 4184
2026/10/08 19:58:07 extract.go:450: fetching 4184 tiles, 94 chunks, 32 requests
2026/10/08 19:58:15 extract.go:606: Completed in 15.25124163s with 4 download threads (274.3383177626678 tiles/s).
2026/10/08 19:58:15 extract.go:611: Extract required 43 total requests.
2026/10/08 19:58:15 extract.go:612: Extract transferred 18 MB (overfetch 0.05) for an archive size of 17 MBأي أن مدينة الرياض كاملة من المستوى 0 إلى 15 أصبحت ملفاً واحداً حجمه 16,989,977 بايت، أي قرابة 17 ميغابايت، في 15 ثانية و43 طلباً فقط. ونتأكد من الملف بأمرين:
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 show /data/riyadh.pmtiles
docker run --rm --name lab-maps-pmtiles -v "$PWD/web/tiles:/data" protomaps/go-pmtiles:v1.31.2 verify /data/riyadh.pmtilesوالمخرج سوف يكون كما يلي (من الأمر الأول أول اثني عشر سطراً، ومن الثاني السطر الأخير):
pmtiles spec version: 3
tile type: mvt
bounds: (long: 46.450000, lat: 24.450000) (long: 47.050000, lat: 25.000000)
min zoom: 0
max zoom: 15
center: (long: 46.750000, lat: 24.725000)
center zoom: 0
addressed tiles count: 4249
tile entries count: 4184
tile contents count: 4123
clustered: true
internal compression: gzip
2026/10/08 20:04:53 verify.go:169: Completed verify in 16.505271ms.لاحظ أن عدد المربعات المختلفة 4123 وهو أقل من 4184، والسبب أن المربعات المتطابقة، مثل مربعات الصحراء الفارغة، تحفظ مرة واحدة ويشير إليها أكثر من عنوان.
ملفات الموقع
بعد ذلك نضع ملفات المكتبات بإصدارات مثبتة داخل مجلد web، حتى لا يطلب المتصفح أي شيء من خارج سيرفرك:
cd web && mkdir -p lib assets && cd lib
for f in maplibre-gl.mjs maplibre-gl-worker.mjs maplibre-gl.css; do
curl -sfLO https://unpkg.com/[email protected]/dist/$f
done
curl -sfL -o pmtiles.mjs https://unpkg.com/[email protected]/dist/esm/index.js
curl -sfL -o fflate.mjs https://unpkg.com/[email protected]/esm/browser.js
curl -sfL -o basemaps.mjs https://unpkg.com/@protomaps/[email protected]/dist/esm/index.js
cd ../assets
curl -sfL -o assets.zip https://github.com/protomaps/basemaps-assets/archive/refs/heads/main.zip
unzip -q assets.zip && mv basemaps-assets-main/fonts basemaps-assets-main/sprites . && rm -rf basemaps-assets-main assets.zip
cd ../.. && du -sh web/*والمخرج سوف يكون كما يلي:
19M web/assets
1.8M web/lib
17M web/tilesثم صفحة الخريطة web/index.html:
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>خريطة الرياض</title>
<link rel="stylesheet" href="/lib/maplibre-gl.css">
<style>html, body, #map { margin: 0; height: 100%; }</style>
<script type="importmap">{ "imports": { "fflate": "/lib/fflate.mjs" } }</script>
</head>
<body>
<div id="map"></div>
<script type="module">
import * as maplibregl from '/lib/maplibre-gl.mjs';
import { Protocol } from '/lib/pmtiles.mjs';
import { layers, namedFlavor } from '/lib/basemaps.mjs';
// let MapLibre read pmtiles:// URLs with HTTP range requests
const protocol = new Protocol();
maplibregl.addProtocol('pmtiles', protocol.tile);
const lang = new URLSearchParams(location.search).get('lang') || 'ar';
window.map = new maplibregl.Map({
container: 'map',
center: [46.6753, 24.7136],
zoom: 12,
style: {
version: 8,
glyphs: '/assets/fonts/{fontstack}/{range}.pbf',
sprite: `${location.origin}/assets/sprites/v4/light`,
sources: {
protomaps: {
type: 'vector',
url: 'pmtiles:///tiles/riyadh.pmtiles',
attribution: '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap</a>'
}
},
layers: layers('protomaps', namedFlavor('light'), { lang })
}
});
map.addControl(new maplibregl.NavigationControl());
map.on('error', (e) => { window.__err = String(e.error && e.error.message); console.error(e.error); });
</script>
</body>
</html>في الإعداد أعلاه لاحظ التالي:
- MapLibre 6 تأتي كوحدات ES Modules، ومكتبة pmtiles بصيغة ESM تستورد مكتبة الضغط
fflateباسمها، لذلك وضعنا Import Map يحول هذا الاسم إلى الملف المحلي. addProtocol('pmtiles', …)هو السطر الذي يجعل الرابطpmtiles:///tiles/riyadh.pmtilesيقرأ بطلبات النطاق من الملف نفسه.- الدالة
layersمن basemaps تبني طبقات التنسيق، والخيارlangيحدد لغة الأسماء، فنغيره من الرابط?lang=enدون أن نغير المربعات. - الخطوط Glyphs والرموز Sprites من مجلد
assetsعلى السيرفر نفسه، وattributionهي النسبة التي تظهر في زاوية الخريطة كما تطلب الرخصة.
ثم إعداد nginx في nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
types {
text/html html;
text/css css;
text/javascript js mjs;
application/json json;
image/png png;
application/x-protobuf pbf;
application/vnd.pmtiles pmtiles;
}
location / {
try_files $uri $uri/ =404;
}
}وحددنا أنواع الملفات MIME Types بأنفسنا، لأن المتصفح يرفض تشغيل وحدة JavaScript من ملف .mjs إذا لم يصل بنوع JavaScript. أما طلبات النطاق فلا تحتاج أي سطر، لأن nginx يدعمها للملفات الثابتة كما سوف نرى في السجل. وأخيراً ملف compose.yaml:
name: lab-maps
services:
web:
image: nginx:1.29-alpine
container_name: lab-maps-web
volumes:
- ./web:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "127.0.0.1:18701:80"تشغيل الخريطة
docker compose up -d
curl -s -r 0-126 -D - -o /dev/null http://127.0.0.1:18701/tiles/riyadh.pmtilesوالمخرج سوف يكون كما يلي:
Container lab-maps-web Created
Container lab-maps-web Starting
Container lab-maps-web Started
HTTP/1.1 206 Partial Content
Server: nginx/1.29.8
Date: Thu, 08 Oct 2026 20:00:27 GMT
Content-Type: application/vnd.pmtiles
Content-Length: 127
Last-Modified: Thu, 08 Oct 2026 19:58:15 GMT
Connection: keep-alive
ETag: "6ac7f5d7-1033f19"
Content-Range: bytes 0-126/16989977طلبنا الترويسة فقط، أي البايتات من 0 إلى 126، فرد nginx بالحالة 206 Partial Content وأرسل 127 بايتاً من ملف حجمه 16,989,977 بايت، وهذا هو كل ما تحتاجه PMTiles من السيرفر. الآن افتح http://127.0.0.1:18701 في المتصفح، والصورة التالية هي الخريطة كما ظهرت:

ولنعرف من أين جاءت الخريطة فعلاً، نقرأ قائمة الطلبات Resource Timing في المتصفح بعد اكتمال الرسم، بلصق السطر التالي في أدوات المطور Console:
JSON.stringify((() => { const r = performance.getEntriesByType('resource'); const hosts = [...new Set(r.map(e => new URL(e.name).host))]; const t = r.filter(e => e.name.includes('.pmtiles')); return { hosts, requests: r.length, pmtilesRequests: t.length, pmtilesBytes: t.reduce((a, e) => a + e.transferSize, 0), zoom: map.getZoom() }; })())والمخرج سوف يكون كما يلي:
{"hosts":["127.0.0.1:18701"],"requests":28,"pmtilesRequests":13,"pmtilesBytes":651775,"zoom":12}أي أن الشاشة الأولى احتاجت 28 طلباً كلها إلى سيرفرنا، منها 13 طلب نطاق على ملف المربعات بمجموع 651,775 بايت، ولم يخرج أي طلب إلى Google أو Mapbox أو حتى إلى OpenStreetMap. وفي سجل nginx سوف تجد هذه الطلبات بالحالة 206:
docker logs lab-maps-web 2>&1 | grep "riyadh.pmtiles" | awk '{print $9, $10}' | head -6206 127
206 16384
206 57529
206 69931
206 68549
206 99121السطر الأول هو طلب curl السابق، والثاني هو أول طلب من المتصفح: 16384 بايت فيها الترويسة والدليل الأول، كما تنص المواصفة، ثم طلبات المربعات الظاهرة. ونفس الخريطة بالإنجليزية من الرابط ?lang=en، دون أي تغيير في المربعات:

النصوص العربية على الخريطة: اختبار صادق
النص العربي على الخرائط في المتصفح كان مشكلة قديمة، لأن الحروف يجب أن تتصل وتعكس اتجاهها، ومكتبات الخرائط كانت ترسم كل حرف وحده. وفي سجل تغييرات MapLibre للإصدار 6.9.0 أن المكتبة أصبحت ترسم العربية والعبرية بشكل صحيح دون إضافة النصوص من اليمين إلى اليسار RTL Text Plugin، وأن الدالة setRTLTextPlugin أصبحت مهملة Deprecated. ولم نكتف بما في السجل، فشغلنا الصفحة نفسها بالإصدار الأقدم 5.24.0 مرة دون الإضافة ومرة مع mapbox-gl-rtl-text الإصدار 0.3.0، والصورة التالية تبين الفرق:

ومن هذا الاختبار ثلاث نتائج عملية:
- إذا كان تطبيقك على MapLibre من الإصدار 6.9.0 فما بعده فالعربية تعمل دون أي سطر إضافي، كما في صورة الرياض أعلاه بالإصدار 6.13.0.
- إذا كنت على إصدار أقدم ولا تستطيع الترقية الآن، فأضف
setRTLTextPluginقبل إنشاء الخريطة، وإلا فسوف يرى المستخدم العربي ما في يمين الصورة. - الخريطة تعرض
name:arإذا كان موجوداً في OpenStreetMap، وإلا فالاسم الأصليname، لذلك سوف ترى بعض الأماكن بالإنجليزية مثل As Sil Kbeer وAl Taief في صورة الرياض، وبعض المطاعم بأسمائها الإنجليزية في الصورة التالية، والحل ليس في الكود وإنما في البيانات: أضف الاسم العربي في OpenStreetMap نفسه فيصل إلى كل من يستخدمها في البناء التالي.

ولاحظ أن البناء من Protomaps يتوقف عند المستوى 15، وما بعده تكبير للمربعات نفسها Overzoom يرسمه المتصفح بحدة لأنها متجهة، ففي المستوى 16 احتاجت الشاشة 25 طلب نطاق بمجموع 974,729 بايت، وكلها أيضاً من 127.0.0.1:18701.
نكسر الإعداد: سيرفر لا يدعم Range
ماذا يحدث إذا وضعت الملفات على سيرفر لا يدعم طلبات النطاق؟ لنأخذ مثالاً سيرفر Python المدمج، فنقدم المجلد نفسه على منفذ آخر:
docker run -d --name lab-maps-norange -v "$PWD/web:/srv:ro" -w /srv -p 127.0.0.1:18702:8000 \
python:3.12-slim python -m http.server 8000
curl -s -r 0-126 -D - -o /dev/null http://127.0.0.1:18702/tiles/riyadh.pmtiles | head -5والمخرج سوف يكون كما يلي:
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.15
Date: Thu, 08 Oct 2026 20:05:05 GMT
Content-type: application/octet-stream
Content-Length: 16989977طلبنا 127 بايتاً فرد السيرفر بالحالة 200 وأرسل الملف كاملاً، وعند فتح http://127.0.0.1:18702 تبقى الخريطة فارغة، والخطأ الذي التقطناه من حدث error في MapLibre هو:
Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving.وهذه الرسالة تعني أن السيرفر أو شيئاً أمامه، مثل Reverse Proxy أو CDN أو إعداد ضغط خاطئ، يتجاهل الترويسة Range، فإذا رأيتها فابدأ بالأمر curl -r 0-126 -D - كما فعلنا، وابحث عن 206 وContent-Range في الرد. ثم نحذف هذا السيرفر بالاسم:
docker rm -f lab-maps-norangeالنسبة على خريطتك عملياً
- اترك عنصر النسبة Attribution Control ظاهراً في زاوية الخريطة، وMapLibre يعرضه تلقائياً من الحقل
attributionفي المصدر، كما في الزاوية السفلية في الصور أعلاه. - إرشادات النسبة تسمح بطيها Collapse بشروط، مثل أن تطوى عند تحريك الخريطة أو بعد خمس ثوان، بشرط أن يجد المستخدم معلومات الرخصة إذا بحث عنها، مثل زر (i) في زاوية الخريطة، وهو ما يفعله MapLibre في الشاشات الصغيرة.
- إذا نشرت لقطة شاشة من خريطتك في تقرير أو عرض، فهي أيضاً عمل ناتج من البيانات، فاترك النسبة ظاهرة فيها.
من المختبر إلى الإنتاج
- الدومين و HTTPS: ضع nginx خلف Reverse Proxy بشهادة TLS كما في تطبيقك خلف Reverse Proxy، وتأكد بعد ذلك بالأمر
curl -rأن ال Proxy يمرر طلبات النطاق ويرد بالحالة 206. - التخزين المؤقت Cache: ملف المربعات لا يتغير إلا عندما تستبدله، لذلك يمكن أن تضع له مدة تخزين طويلة في المتصفح أو في شبكة توصيل المحتوى CDN، وعند التحديث غير اسم الملف، مثل
riyadh-20261008.pmtiles، حتى لا يخلط المتصفح بين نسختين. - التخزين: يمكن أن تضع الملف في تخزين كائنات Object Storage متوافق مع S3 بدل القرص، كما شرحنا في أنواع التخزين، بشرط أن يدعم طلبات النطاق وأن تضبط CORS إذا كان على دومين آخر.
- التحديث: البيانات في المختبر من يوم القص، والبناء يومي، فحدد كم مرة تحتاج تحديث خريطتك، وأعد أمر
pmtiles extractبالبناء الأحدث، وثبت تاريخ البناء في الأمر كما فعلنا بدل أن تعتمد على رابط يتغير. - المنطقة: كلما اتسع المستطيل كبر الملف، فاقطع المنطقة التي يحتاجها تطبيقك فقط، ويمكن أن تقطع منطقة أوسع بمستويات أقل بالخيار
--maxzoom.
ماذا بعد الخريطة؟
الخريطة تعرض الأماكن، ولكن تطبيقك غالباً يحتاج أن يحول عنواناً يكتبه المستخدم إلى نقطة على الخريطة، أو نقطة من GPS الهاتف إلى عنوان، وهذا ما نشرحه في المقال التالي البحث عن العناوين Geocoding مع Nominatim على مستطيل الرياض نفسه. وحساب المسارات والمسافات، وتحليل البيانات الجغرافية في PostGIS، موضوعات مخططة في المرحلة نفسها، وإذا كانت بياناتك الجغرافية في PostgreSQL أصلاً فاقرأ قسم PostGIS في مقال PostgreSQL لكل شيء.
تنظيف المختبر
من داخل مجلد المختبر نحذف ال Container والشبكة بالاسم:
docker compose downوالمخرج سوف يكون كما يلي:
Container lab-maps-web Stopping
Container lab-maps-web Stopped
Container lab-maps-web Removing
Container lab-maps-web Removed
Network lab-maps_default Removing
Network lab-maps_default Removedثم نحذف Docker Image الخاصة بالأداة إذا لم تعد تحتاجها، ومجلد المختبر الذي فيه ملف المربعات:
docker image rm protomaps/go-pmtiles:v1.31.2
cd .. && rm -rf maps-labتدريب عملي
- اقطع مستطيل مدينتك بدل الرياض، وقارن حجم الملف وعدد المربعات بما رأيناه هنا، ثم اقطع المنطقة نفسها بالخيار
--maxzoom=12وقارن الحجم مرة أخرى. - غير
namedFlavor('light')إلىdarkوغير مسار الرموز إلىsprites/v4/dark، وأضف علامة Marker لموقع فرعك بالدالةnew maplibregl.Marker().
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- بيانات OpenStreetMap مفتوحة برخصة ODbL، والخريطة التي ترسمها منها عمل ناتج ترخصه كما تريد، بشرط نسبة «OpenStreetMap» ظاهرة على الخريطة، أما تعديل البيانات نفسها ونشرها فيبقى تحت ODbL.
- سيرفرات مربعات OpenStreetMap ليست لخدمة تطبيقك، فاستضف خريطتك بنفسك.
- المربعات المتجهة تنقل الرسم إلى المتصفح، فتعرض العربية والإنجليزية من الملف نفسه، وPMTiles تجعل المربعات ملفاً واحداً يقدمه أي سيرفر يدعم طلبات النطاق.
- في المختبر أصبحت الرياض من المستوى 0 إلى 15 ملفاً من 17 ميغابايت في 15 ثانية، والشاشة الأولى احتاجت 13 طلب نطاق بقرابة 652 كيلوبايت، وكل الطلبات إلى سيرفرنا.
- MapLibre من الإصدار 6.9.0 ترسم العربية صحيحة دون إضافة، والإصدارات الأقدم تحتاج
setRTLTextPlugin، والأسماء الناقصة بالعربية تصلح في بيانات OpenStreetMap لا في الكود. - إذا رأيت الخطأ HTTP Byte Serving فالسيرفر أو ما أمامه يتجاهل Range، وتأكد دائماً بالأمر
curl -r 0-126 -D -.
دليلالبحث عن العناوين Geocoding مع Nominatimكيف تحول اسم المكان إلى إحداثيات والإحداثيات إلى عنوان على سيرفرك مع Nominatim، وما الذي يجيده بالعربية وما الذي يفوته.
دليلPostgreSQL لكل شيء: متى يكفيك ومتى تحتاج غيره؟كم عملاً يستطيع PostgreSQL واحد أن يقوم به في شركتك الناشئة قبل أن تحتاج Redis أو Elasticsearch، وما الإشارة التي تقول لك إن الوقت قد حان.
دليلكيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxyإذا كان تطبيقك يرى عنوان ال Proxy بدلاً من الزائر أو يولد روابط http، فهذا هو السبب والإعداد الصحيح لإطار عملك.
دليلكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظهلماذا يبطؤ موقعك على الزائر البعيد، وكيف تقرب شبكة CDN ملفاتك منه، وكيف تمنعها من حفظ الصفحات الخاصة.
دليلأنواع التخزين وواجهة S3: أين تضع بيانات تطبيقك؟أين تضع قاعدة البيانات وملفات المستخدمين والنسخ الاحتياطية، ولماذا يناسب كلاً منها نوع مختلف من التخزين.
دليلما هي البرمجيات مفتوحة المصدر؟ القصة وأشهر الرخصكيف بدأت البرمجيات الحرة والمفتوحة، وما الفرق بين الحر والمجاني، وكيف تعرف ما تسمح به رخصة أي مشروع قبل أن تبني عليه.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ المختبر على بناء Protomaps بتاريخ 20261008 (الإصدار 4.15.2)، وgo-pmtiles 1.31.2، وMapLibre GL JS 6.13.0 و5.24.0 للمقارنة، وpmtiles 4.5.0، و@protomaps/basemaps 5.7.2، وnginx 1.29.8، وDocker Engine 29.8.0 وDocker Compose v5.5.1 على Windows 11 مع Docker Desktop.