🗺️ نقشه الوکادو

مستندات سرویس نقشه الوکادو

با یک خط اسکریپت، نقشه کامل ایران را داخل سایت خودتان می‌آورید: تایل‌های وکتور آفلاین، مارکر و پاپ‌آپ، مسیریابی جاده‌ای، محاسبه مسافت و زمان، و نمایش موقعیت زندهٔ کاربر. هیچ وابستگی بیرونی وجود ندارد؛ همهٔ فایل‌ها (Leaflet، MapLibre، فونت، تایل و گراف مسیر) از سرور https://map.bitjob.ir سرو می‌شوند.

رابط برنامه‌نویسی سمت مرورگر روی Leaflet 1.9 ساخته شده است، پس هر چیزی که در مستندات Leaflet بلدید اینجا هم کار می‌کند. موتور رندر تایل‌ها MapLibre GL است.

فهرست

  1. شروع سریع
  2. گرفتن کلید API و محدودکردن دامنه
  3. ساختن نقشه
  4. جابه‌جایی، زوم و کنترل‌ها
  5. مارکر، پاپ‌آپ و آیکون سفارشی
  6. خط، چندضلعی و دایره
  7. رویدادها و گرفتن مختصات
  8. مسیریابی جاده‌ای
  9. جستجو، تکمیل خودکار و ژئوکدینگ
  10. ماتریس فاصله و زمان
  11. تصویر ثابت نقشه
  12. تم شب
  13. محاسبه مسافت و زمان
  14. موقعیت زندهٔ کاربر
  15. نمایش داده GeoJSON
  16. دسترسی مستقیم به MapLibre
  17. مرجع REST API
  18. خطاها و کدهای وضعیت
  19. هزینه‌ها و سشن
  20. محدودیت‌ها و نکات مهم
  21. نمونهٔ کامل (نقشه + مسیریابی + مسافت)
  22. عیب‌یابی

۱) شروع سریع

سه قدم تا نقشهٔ زنده:

<!-- ۱. اسکریپت سرویس با کلید شما -->
<script src="https://map.bitjob.ir/map.js?key=YOUR_API_KEY"></script>

<!-- ۲. عنصری که نقشه داخلش ساخته می‌شود (ارتفاع الزامی است) -->
<div id="map" style="height:400px"></div>

<!-- ۳. ساخت نقشه -->
<script>
  AlooMap.ready(function () {
    var map = AlooMap.map('map', { center: [35.6892, 51.3890], zoom: 12 });
  });
</script>

کلید را می‌توانید به‌جای query string با data-key هم بدهید: <script src="https://map.bitjob.ir/map.js" data-key="YOUR_API_KEY">

نکات ضروری

۲) گرفتن کلید API و محدودکردن دامنه

  1. از ورود با الوکادو وارد شوید (ثبت‌نام جدا ندارد؛ کد تایید در پیام‌رسان الوکادو می‌آید).
  2. در داشبورد یک کلید جدید بسازید.
  3. از صفحهٔ شارژ حساب را شارژ کنید.

دو نوع کلید

کلید عمومی mk_…کلید سرّی sk_…
کجا استفاده می‌شود داخل HTML سایت شما (map.js) فقط روی سرور خودتان
چه کسی می‌بیندش همه — در سورس صفحه پیداست هیچ‌کس؛ هرگز نباید به مرورگر برسد
دامنهٔ مجاز اجباری لازم نیست (Origin ندارد)
مناسب برای نمایش نقشه، مارکر، موقعیت کاربر مسیریابی، ماتریس، جستجو، تصویر ثابت

محدودکردن کلید عمومی به دامنهٔ خودتان

هنگام ساخت کلید عمومی، فیلد «دامنه‌های مجاز» را پر کنید (با کاما جدا کنید، مثل example.com, shop.example.com). زیردامنه‌ها خودکار مجاز هستند (example.com شامل www.example.com هم می‌شود).

مهم: وقتی برای یک کلید عمومی دامنه تعیین کرده باشید، درخواستی که اصلاً هدر Origin نداشته باشد هم رد می‌شود (403) — نه فقط درخواستِ دامنهٔ اشتباه. دلیلش این است که کلید عمومی در سورس صفحهٔ شما پیداست و اگر درخواستِ بدون Origin پذیرفته می‌شد، هرکسی می‌توانست کلید را کپی کند و با یک اسکریپت ساده موجودی حساب شما را خرج کند.
برای فراخوانی سرور-به-سرور از کلید سرّی استفاده کنید، نه کلید عمومی.

اگر کلیدی جایی لو رفت، لازم نیست حذفش کنید: از داشبورد دکمهٔ چرخش را بزنید تا مقدارش عوض شود و نام و دامنه‌ها و آمارش سرِ جایش بماند. در داشبورد می‌توانید آستانهٔ هشدار موجودی کم را هم تنظیم کنید تا سرویس نقشهٔ سایتتان بی‌خبر قطع نشود.

۳) ساختن نقشه

روش ۱ — داخل ready (همزمان)

AlooMap.ready(function (session) {
  var map = AlooMap.map('map', {
    center: [35.6892, 51.3890],   // میدان آزادی تهران
    zoom: 12,
    maxZoom: 18,
    attribution: 'نقشه سایت من',  // متن پایین-چپ؛ '' یعنی بدون متن
    leaflet: { zoomControl: false, scrollWheelZoom: false } // هر گزینهٔ خام Leaflet
  });
  console.log('موجودی باقی‌مانده:', session.balance);
});

روش ۲ — createMap (Promise، بدون ready)

AlooMap.createMap('map', { center: [35.6892, 51.3890], zoom: 12 })
  .then(function (map) {
    // نقشه آماده است
  })
  .catch(function (err) {
    // خطا خودکار داخل خود عنصر نقشه هم نمایش داده می‌شود
    console.error(err.message);
  });

روش ۳ — افزودن لایهٔ نقشه به یک نقشهٔ Leaflet موجود

اگر خودتان از قبل Leaflet دارید و فقط تایل می‌خواهید:

AlooMap.ready(function () {
  var map = L.map('map').setView([35.69, 51.39], 12);
  AlooMap.tileLayer().addTo(map);
});

گزینه‌های AlooMap.map / AlooMap.createMap

گزینهپیش‌فرضتوضیح
center[32.4279, 53.6880]مرکز اولیه [lat, lng] (پیش‌فرض: مرکز ایران)
zoom6سطح زوم اولیه (۰ تا 18)
maxZoom18بیشترین زوم مجاز
attributionOpenStreetMap + نقشه الوکادومتن انتساب؛ رشتهٔ خالی یعنی حذف کامل
leaflet—آبجکت گزینه‌های خام Leaflet که مستقیم به L.map() پاس می‌شود

متدهای AlooMap

متدخروجیتوضیح
AlooMap.ready(cb)Promise<session>لود کتابخانه‌ها و ساخت سشن؛ callback اختیاری است
AlooMap.map(el, opts)L.Mapساخت همزمان نقشه — فقط داخل ready
AlooMap.createMap(el, opts)Promise<L.Map>ساخت نقشه بدون نیاز به ready؛ خطا را داخل عنصر نشان می‌دهد
AlooMap.tileLayer(opts)L.Layerفقط لایهٔ تایل، برای نقشهٔ Leaflet موجود
AlooMap.locate(map, opts)Promise<{stop()}>نمایش زندهٔ موقعیت کاربر
AlooMap.basestringآدرس سرور سرویس (برای صدازدن REST API)
AlooMap._sessionobjectسشن جاری: token، expiresAt، balance، styleUrl

el هم می‌تواند id عنصر (رشته) باشد و هم خود عنصر DOM.

۴) جابه‌جایی، زوم و کنترل‌ها

map.setView([36.2972, 59.6067], 13);      // پرش به مشهد
map.flyTo([29.5918, 52.5837], 13);        // انیمیشن نرم به شیراز
map.panTo([35.70, 51.40]);                // فقط جابه‌جایی
map.setZoom(15);  map.zoomIn();  map.zoomOut();

map.getCenter();   // {lat, lng}
map.getZoom();     // عدد
map.getBounds();   // کادر دید فعلی

// قاب‌کردن چند نقطه در دید
map.fitBounds([[35.68, 51.35], [35.75, 51.45]], { padding: [30, 30] });

// محدودکردن حرکت کاربر به محدودهٔ ایران
map.setMaxBounds([[24.0, 43.5], [40.5, 64.0]]);

// اگر نقشه داخل تب/آکاردئون مخفی ساخته شد، بعد از نمایان‌شدن حتماً:
map.invalidateSize();

کنترل‌ها

L.control.scale({ metric: true, imperial: false, position: 'bottomright' }).addTo(map);
map.zoomControl.setPosition('topleft');

// دکمهٔ تمام‌صفحه ساده
L.control.attribution({ prefix: false }).addTo(map);

۵) مارکر، پاپ‌آپ و آیکون سفارشی

مارکر ساده + پاپ‌آپ + تولتیپ

var marker = L.marker([35.6892, 51.3890]).addTo(map);

marker.bindPopup('<b>دفتر مرکزی</b><br>تهران، میدان آزادی').openPopup();
marker.bindTooltip('دفتر مرکزی');        // با هاور نشان داده می‌شود

marker.setLatLng([35.70, 51.40]);        // جابه‌جا کردن
map.removeLayer(marker);                 // حذف

مارکر با آیکون سفارشی

var myIcon = L.icon({
  iconUrl: 'https://example.com/pin.png',
  iconSize:   [32, 42],   // اندازهٔ تصویر
  iconAnchor: [16, 42],   // نقطه‌ای از تصویر که روی مختصات می‌نشیند (نوک پین)
  popupAnchor:[0, -40]    // محل بازشدن پاپ‌آپ نسبت به iconAnchor
});
L.marker([35.6892, 51.3890], { icon: myIcon }).addTo(map);

مارکر HTML/CSS (بدون تصویر)

var badge = L.divIcon({
  className: '',
  html: '<div style="background:#1a73e8;color:#fff;border-radius:14px;' +
        'padding:3px 9px;font:12px Tahoma;white-space:nowrap">۲۵۰,۰۰۰ تومان</div>',
  iconSize: null
});
L.marker([35.72, 51.42], { icon: badge }).addTo(map);

مارکر قابل جابه‌جایی (انتخاب آدرس توسط کاربر)

var pin = L.marker([35.6892, 51.3890], { draggable: true }).addTo(map);
pin.on('dragend', function () {
  var p = pin.getLatLng();
  document.getElementById('lat').value = p.lat.toFixed(6);
  document.getElementById('lng').value = p.lng.toFixed(6);
});

گذاشتن مارکر با کلیک کاربر

var picked = null;
map.on('click', function (e) {
  if (picked) picked.setLatLng(e.latlng);
  else picked = L.marker(e.latlng).addTo(map);
  picked.bindPopup('موقعیت انتخابی: ' + e.latlng.lat.toFixed(5) + ', ' +
                   e.latlng.lng.toFixed(5)).openPopup();
});

مارکرهای زیاد + حذف یک‌جا

var group = L.layerGroup().addTo(map);

var places = [
  { name: 'شعبه ونک',  lat: 35.7575, lng: 51.4100 },
  { name: 'شعبه تجریش', lat: 35.8020, lng: 51.4340 },
  { name: 'شعبه آزادی', lat: 35.6997, lng: 51.3380 }
];

places.forEach(function (p) {
  L.marker([p.lat, p.lng]).bindPopup(p.name).addTo(group);
});

// قاب‌کردن همهٔ مارکرها در دید
map.fitBounds(L.featureGroup(group.getLayers()).getBounds(), { padding: [40, 40] });

group.clearLayers();   // حذف همهٔ مارکرها با هم

۶) خط، چندضلعی و دایره

// خط (مثلاً مسیر پیموده‌شده)
var line = L.polyline([[35.69, 51.39], [35.75, 51.45], [35.80, 51.43]], {
  color: '#1a73e8', weight: 5, opacity: 0.85
}).addTo(map);

// چندضلعی (محدودهٔ سرویس‌دهی)
L.polygon([[35.70, 51.33], [35.78, 51.36], [35.76, 51.48], [35.68, 51.45]], {
  color: '#15803d', fillOpacity: 0.15
}).addTo(map).bindPopup('محدودهٔ ارسال رایگان');

// دایره با شعاع متر (شعاع پوشش)
L.circle([35.6892, 51.3890], { radius: 3000, color: '#b91c1c', fillOpacity: 0.08 }).addTo(map);

// مستطیل
L.rectangle([[35.66, 51.32], [35.74, 51.46]], { weight: 1 }).addTo(map);

// ویرایش و حذف
line.setStyle({ color: 'red' });
line.setLatLngs([[35.69, 51.39], [35.72, 51.42]]);
map.removeLayer(line);

۷) رویدادها و گرفتن مختصات

map.on('click', function (e) {
  console.log(e.latlng.lat, e.latlng.lng);   // مختصات نقطهٔ کلیک‌شده
});

map.on('moveend', function () { console.log('مرکز جدید:', map.getCenter()); });
map.on('zoomend', function () { console.log('زوم:', map.getZoom()); });

marker.on('click', function () { /* کلیک روی مارکر */ });
map.on('contextmenu', function (e) { /* کلیک راست */ });

// بارگذاری داده بر اساس کادر دید فعلی
map.on('moveend', function () {
  var b = map.getBounds();
  fetch('/api/places?north=' + b.getNorth() + '&south=' + b.getSouth() +
        '&east=' + b.getEast() + '&west=' + b.getWest());
});

۸) مسیریابی جاده‌ای

مسیریابی روی گراف جاده‌های ایران (داده OpenStreetMap) و کاملاً آفلاین روی سرور ما انجام می‌شود. خروجی شامل مسافت جاده‌ای، زمان تقریبی، شکل مسیر و دستورهای گام‌به‌گام است.

ساده‌ترین شکل

AlooMap.route([35.6892, 51.3890], [36.2972, 59.6067]).then(function (r) {
  AlooMap.drawRoute(map, r);   // رسم مسیر و زوم روی آن
  console.log(AlooMap.formatDistance(r.meters), AlooMap.formatDuration(r.seconds));
  r.steps.forEach(function (s) { console.log(s.text); });
});

نقاط میانی و پروفایل

// مسیر با توقف‌های میانی — یک درخواست، نه چند تا
AlooMap.route([
  [35.70, 51.40],   // مبدأ
  [35.75, 51.45],   // توقف ۱
  [35.80, 51.50]    // مقصد
], { profile: 'car' });

پروفایل‌های ممکن: car (پیش‌فرض)، pedestrian، bicycle. پروفایل‌های در دسترس به گرافی که روی سرور ساخته شده بستگی دارد؛ فهرست دقیق در /readyz می‌آید و درخواستِ پروفایلِ ناموجود پاسخ 400 با فهرست پشتیبانی‌شده‌ها می‌گیرد. هزینه به‌ازای هر بخش مسیر حساب می‌شود، پس مسیر سه‌نقطه‌ای دو برابر مسیر ساده هزینه دارد.

درخواست خام

POST https://map.bitjob.ir/api/v1/route
Content-Type: application/json

{
  "key": "YOUR_API_KEY",
  "points": [[35.6892, 51.3890], [36.2972, 59.6067]],
  "profile": "car"
}

شکل قدیمی fromLat/fromLng/toLat/toLng هم همچنان کار می‌کند.

پاسخ موفق (۲۰۰)

{
  "ok": true,
  "meters": 892340,                 // مسافت جاده‌ای بر حسب متر
  "seconds": 32150,                 // زمان تقریبی بر حسب ثانیه
  "latlng": [[35.6892,51.3890], ... ],  // شکل مسیر، آمادهٔ L.polyline
  "steps": [                        // دستورهای گام‌به‌گام
    {
      "meters": 1200,
      "seconds": 95,
      "name": "خیابان ولیعصر",
      "maneuver": "right",          // straight | left | right | sharp-left | sharp-right | arrive
      "text": "۱٫۲ کیلومتر در خیابان ولیعصر، سپس به راست بپیچید به بزرگراه همت",
      "latlng": [35.7100, 51.4020]  // نقطهٔ پیچ
    }
  ],
  "balance": 148900                 // موجودی باقی‌ماندهٔ حساب (تومان)
}

رسم مسیر روی نقشه

function alooRoute(from, to) {
  return fetch(AlooMap.base + '/api/v1/route', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      key: 'YOUR_API_KEY',
      fromLat: from[0], fromLng: from[1],
      toLat:   to[0],   toLng:   to[1]
    })
  }).then(function (res) {
    return res.json().then(function (data) {
      if (!res.ok) throw new Error(data.error || 'خطای مسیریابی');
      return data;
    });
  });
}

var routeLine = null;

alooRoute([35.6892, 51.3890], [36.2972, 59.6067]).then(function (r) {
  if (routeLine) map.removeLayer(routeLine);
  routeLine = L.polyline(r.latlng, { color: '#1a73e8', weight: 5, opacity: 0.9 }).addTo(map);
  map.fitBounds(routeLine.getBounds(), { padding: [40, 40] });

  L.marker(r.latlng[0]).addTo(map).bindPopup('مبدأ');
  L.marker(r.latlng[r.latlng.length - 1]).addTo(map).bindPopup('مقصد');

  console.log((r.meters / 1000).toFixed(1) + ' کیلومتر — ' +
              Math.round(r.seconds / 60) + ' دقیقه');
});

مسیر چندنقطه‌ای (توقف‌های میانی)

هر درخواست فقط مبدأ و مقصد می‌گیرد؛ برای مسیر با توقف، بخش‌ها را پشت‌سرهم بگیرید و جمع کنید. هر بخش یک درخواست مسیریابی جداگانه حساب می‌شود.

async function multiRoute(points) {
  var total = { meters: 0, seconds: 0, latlng: [] };
  for (var i = 0; i < points.length - 1; i++) {
    var seg = await alooRoute(points[i], points[i + 1]);
    total.meters  += seg.meters;
    total.seconds += seg.seconds;
    total.latlng = total.latlng.concat(seg.latlng);
  }
  return total;
}

multiRoute([[35.69, 51.39], [35.82, 50.95], [36.27, 50.00]]).then(function (r) {
  L.polyline(r.latlng, { color: '#1a73e8', weight: 5 }).addTo(map);
});

صدازدن از سرور (پیشنهادی برای کلیدهای حساس)

curl -X POST https://map.bitjob.ir/api/v1/route \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","fromLat":35.6892,"fromLng":51.3890,"toLat":36.2972,"toLng":59.6067}'
// C#
var payload = new { key = "YOUR_API_KEY", fromLat = 35.6892, fromLng = 51.3890,
                    toLat = 36.2972, toLng = 59.6067 };
var res = await http.PostAsJsonAsync("https://map.bitjob.ir/api/v1/route", payload);
var route = await res.Content.ReadFromJsonAsync<RouteResult>();

درخواست سرور-به-سرور هدر Origin ندارد، پس محدودیت دامنهٔ کلید روی آن اعمال نمی‌شود؛ بهترین کار این است که کلید را در سرور خودتان نگه دارید و مسیریابی را از آنجا صدا بزنید.

۹) محاسبه مسافت و زمان

الف) مسافت جاده‌ای (دقیق، از سرویس مسیریابی)

عددی که در فاکتور ارسال یا کرایه لازم دارید همین است — طول واقعی مسیر روی جاده:

alooRoute(origin, destination).then(function (r) {
  var km      = r.meters / 1000;         // کیلومتر
  var minutes = r.seconds / 60;          // دقیقه
  var fare    = Math.round(km * 4500);   // مثال: کرایه هر کیلومتر ۴٬۵۰۰ تومان
  document.getElementById('fare').textContent = fare.toLocaleString('fa-IR') + ' تومان';
});

ب) فاصلهٔ مستقیم / هوایی (رایگان، بدون درخواست شبکه)

برای فیلترکردن «نزدیک‌ترین شعبه» یا بررسی «داخل شعاع سرویس‌دهی» همین کافی است:

// هر دو خروجی بر حسب متر است
var meters = map.distance([35.6892, 51.3890], [35.7575, 51.4100]);
var same   = L.latLng(35.6892, 51.3890).distanceTo([35.7575, 51.4100]);

// آیا داخل شعاع ۵ کیلومتری هستیم؟
if (map.distance(userLatLng, shopLatLng) <= 5000) { /* داخل محدوده */ }

// نزدیک‌ترین شعبه به کاربر
var nearest = places
  .map(function (p) { return { p: p, d: map.distance(userLatLng, [p.lat, p.lng]) }; })
  .sort(function (a, b) { return a.d - b.d; })[0];

فاصلهٔ مستقیم همیشه از مسافت جاده‌ای کمتر است. برای قیمت‌گذاری از حالت (الف) استفاده کنید و از (ب) فقط برای مرتب‌سازی و فیلتر اولیه.

ج) طول یک خط (مسیر رسم‌شده یا مسیر پیموده‌شده)

function polylineLength(latlngs) {
  var sum = 0;
  for (var i = 1; i < latlngs.length; i++)
    sum += L.latLng(latlngs[i - 1]).distanceTo(latlngs[i]);
  return sum;   // متر
}

polylineLength(routeLine.getLatLngs());

د) اندازه‌گیری با کلیک کاربر (خط‌کش)

var pts = [], ruler = null;
map.on('click', function (e) {
  pts.push(e.latlng);
  if (ruler) map.removeLayer(ruler);
  ruler = L.polyline(pts, { color: '#b91c1c', dashArray: '6,6' }).addTo(map);
  if (pts.length > 1)
    ruler.bindTooltip(fmtDistance(polylineLength(pts)), { permanent: true }).openTooltip();
});

ه) قالب‌بندی فارسی مسافت و زمان

function fmtDistance(m) {
  return m < 1000
    ? Math.round(m) + ' متر'
    : (m / 1000).toFixed(m < 10000 ? 1 : 0) + ' کیلومتر';
}

function fmtDuration(s) {
  var h = Math.floor(s / 3600), m = Math.round((s % 3600) / 60);
  if (h && m) return h + ' ساعت و ' + m + ' دقیقه';
  if (h)      return h + ' ساعت';
  return Math.max(1, m) + ' دقیقه';
}

fmtDistance(892340);  // "892 کیلومتر"
fmtDuration(32150);   // "8 ساعت و 56 دقیقه"

seconds زمان آزادراهی بر اساس سرعت مجاز جاده‌هاست و ترافیک لحظه‌ای را در نظر نمی‌گیرد؛ برای شهرهای شلوغ معمولاً ضریبی (مثلاً ۱٫۳) روی آن اعمال می‌کنند.

۱۰) موقعیت زندهٔ کاربر

مارکر آبی + دایرهٔ دقت را روی نقشه می‌آورد و با هر به‌روزرسانی GPS جابه‌جا می‌کند (enableHighAccuracy فعال است).

AlooMap.locate(map, {
  follow: true,      // نقشه همراه کاربر حرکت کند
  zoom: 16,          // زوم اولین fix
  timeout: 15000,    // میلی‌ثانیه
  onUpdate: function (pos) {
    console.log(pos.coords.latitude, pos.coords.longitude,
                'دقت:', pos.coords.accuracy + ' متر');
  }
}).then(function (tracker) {
  // پایان‌دادن به ردیابی و حذف مارکر
  document.getElementById('stopBtn').onclick = function () { tracker.stop(); };
}).catch(function (err) {
  alert('دسترسی به موقعیت ممکن نشد: ' + err.message);
});

مرورگرها موقعیت‌یابی را فقط روی HTTPS اجازه می‌دهند و کاربر باید اجازه بدهد. فاصلهٔ کاربر تا مقصد را با map.distance() یا مسیریابی حساب کنید.

۱۱) نمایش داده GeoJSON

fetch('/api/my-branches.geojson')
  .then(function (r) { return r.json(); })
  .then(function (geo) {
    var layer = L.geoJSON(geo, {
      style: { color: '#1a73e8', weight: 3 },
      pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, { radius: 6, fillColor: '#1a73e8', fillOpacity: 1, weight: 2, color: '#fff' });
      },
      onEachFeature: function (feature, lyr) {
        lyr.bindPopup(feature.properties.name);
      }
    }).addTo(map);

    map.fitBounds(layer.getBounds());
  });

توجه: در GeoJSON ترتیب مختصات [lng, lat] است (برعکس Leaflet)؛ L.geoJSON خودش این تبدیل را انجام می‌دهد.

۱۲) دسترسی مستقیم به MapLibre (پیشرفته)

لایهٔ تایل یک نقشهٔ MapLibre GL است. اگر به قابلیت‌های سطح‌پایین (تغییر استایل لایه‌ها، چرخش، شیب سه‌بعدی) نیاز دارید:

AlooMap.ready(function () {
  var map = AlooMap.map('map', { center: [35.69, 51.39], zoom: 12 });
  var gl  = AlooMap.tileLayer();          // یا لایه‌ای که createMap ساخته
  gl.addTo(map);

  var mlMap = gl.getMaplibreMap();        // شیء maplibregl.Map
  mlMap.on('load', function () {
    mlMap.setPaintProperty('water', 'fill-color', '#a8d8ff');
  });
});

استفاده از استایل بدون Leaflet

AlooMap.ready(function (session) {
  var m = new maplibregl.Map({
    container: 'map',
    style: session.styleUrl,      // شامل توکن سشن است
    center: [51.3890, 35.6892],   // دقت کنید: MapLibre به شکل [lng, lat]
    zoom: 11
  });
});

ماتریس فاصله و زمان

زمان سفر بین چند مبدأ و چند مقصد را یک‌جا حساب می‌کند — چیزی که سامانه‌های پخش، تخصیص راننده و انتخاب نزدیک‌ترین شعبه لازم دارند. یک درخواست ماتریس به‌مراتب ارزان‌تر و سریع‌تر از صدا زدن جداگانهٔ مسیریابی برای هر جفت است.

AlooMap.matrix(
  [[35.70, 51.40], [35.75, 51.45]],           // مبدأها (رانندگان)
  [[35.72, 51.38], [35.68, 51.42], [35.80, 51.50]]  // مقصدها (سفارش‌ها)
).then(function (r) {
  // r.seconds[i][j] = زمان از مبدأ i تا مقصد j (ثانیه) — null یعنی مسیری نیست
  r.seconds.forEach(function (row, i) {
    var best = row.indexOf(Math.min.apply(null, row.filter(function (v) { return v !== null; })));
    console.log('راننده', i, '→ نزدیک‌ترین سفارش', best);
  });
});

ماتریس مسافت اختیاری است و با { includeDistance: true } فعال می‌شود؛ چون گراف برای متریکِ زمان پیش‌محاسبه شده، محاسبهٔ مسافت مسیر کندتری دارد و برای ماتریس‌های بزرگ توصیه نمی‌شود. هزینه بر اساس تعداد خانه‌ها (مبدأها × مقصدها) حساب می‌شود.

تصویر ثابت نقشه

یک PNG از نقشه، رندرشده روی سرور — برای ایمیل، فاکتور، PDF و هر جایی که جاوااسکریپت اجرا نمی‌شود.

<img src="https://map.bitjob.ir/api/v1/staticmap?key=YOUR_KEY&center=35.6892,51.3890&zoom=13&size=600x400&markers=35.6892,51.3890,red" />

یا از داخل جاوااسکریپت:

var url = AlooMap.staticMapUrl({
  center: [35.6892, 51.3890],
  zoom: 13,
  size: [600, 400],
  theme: 'dark',
  markers: [[35.6892, 51.3890, 'red'], [35.7448, 51.3750, 'blue']],
  path: routeResult.latlng   // خروجی AlooMap.route را مستقیم بدهید
});
پارامترتوضیح
centerlat,lng — اجباری
zoom۰ تا ۱۸ (پیش‌فرض ۱۲)
sizeعرضxارتفاع بر حسب پیکسل (پیش‌فرض 600x400)
themedark برای تم شب
markerslat,lng[,رنگ] جدا شده با |
pathlat,lng جدا شده با | — خط مسیر

روی تصویر ثابت عمداً برچسب متنی رسم نمی‌شود: شکل‌دهی راست‌به‌چپِ فارسی روی رستر سمت سرور نتیجهٔ بدی می‌دهد و نقشهٔ بی‌برچسبِ تمیز بهتر از برچسب‌های شکسته است.

تم شب

ساده‌ترین راه، پارامتر theme روی خود آدرس اسکریپت است:

<!-- همیشه شب -->
<script src="https://map.bitjob.ir/map.js?key=YOUR_KEY&theme=dark"></script>

<!-- پیروی از تنظیم سیستم کاربر -->
<script src="https://map.bitjob.ir/map.js?key=YOUR_KEY&theme=auto"></script>

یا هنگام ساخت نقشه، و جابه‌جایی در زمان اجرا:

AlooMap.ready(function () {
  var layer = AlooMap.tileLayer({ theme: 'dark' });
  var map = L.map('map').setView([35.6892, 51.3890], 12);
  layer.addTo(map);

  // بعداً، مثلاً با کلید تغییر تم سایت:
  AlooMap.setTheme(layer, 'light');
});

۱۳) مرجع REST API

آدرس پایه: https://map.bitjob.ir — همهٔ endpointها CORS باز دارند.

متد و مسیرورودیخروجی
POST /api/v1/sessions { key, page } { token, expiresAt, cost, balance, maxZoom, styleUrl, darkStyleUrl } — ساخت سشن لود نقشه (هزینه‌دار)
GET /api/v1/sessions/status ?key=…&t=TOKEN { ok } — بررسی رایگان سشن ذخیره‌شده؛ 402 یعنی اعتبار تمام شده، 401 یعنی سشن تازه لازم است
POST /api/v1/route { key, fromLat, fromLng, toLat, toLng } یا { key, points: [[lat,lng], …], profile } { ok, meters, seconds, latlng, steps, balance } — محاسبه مسیر (هزینه‌دار؛ هزینه به‌ازای هر بخش)
POST /api/v1/matrix { key, sources, targets, profile, includeDistance } { ok, seconds, meters, balance } — ماتریس فاصله (هزینه به‌ازای هر خانه)
GET /api/v1/autocomplete key, session, q, limit, lat, lng { ok, results, charged, balance } — صورتحساب سشنی: یک بار به‌ازای هر سشن
GET /api/v1/search key, q, limit, lat, lng { ok, results: [{ id, name, nameEn, kind, category, context, lat, lng, distanceMeters }], balance }
GET /api/v1/reverse key, lat, lng { ok, result, balance } — نزدیک‌ترین مکان شناخته‌شده
GET /api/v1/staticmap key, center, zoom, size, theme, markers, path تصویر image/png (هزینه‌دار)
GET /api/map/style.json?t=TOKEN توکن سشن، و theme=dark برای تم شب استایل MapLibre با آدرس مطلق تایل و گلیف
GET /api/map/{z}/{x}/{y}.pbf?t=TOKEN توکن سشن تایل وکتور MVT؛ 204 یعنی تایل خالی (بدون داده)
GET /api/map/info — وضعیت فایل نقشه: available, minZoom, maxZoom, bounds, ...
GET /healthz — { ok: true } — فقط زنده بودن پروسه
GET /readyz — وضعیت واقعی سرویس: فایل نقشه، گراف مسیریابی و دیتابیس

همهٔ endpointها سقف نرخ درخواست دارند؛ عبور از آن پاسخ 429 با هدر Retry-After می‌دهد.

اسکریپت map.js خودش سشن می‌سازد و توکن را مدیریت می‌کند؛ به‌ندرت لازم است /api/v1/sessions را دستی صدا بزنید (مثلاً وقتی می‌خواهید مستقیم با MapLibre کار کنید).

۱۴) خطاها و کدهای وضعیت

بدنهٔ همهٔ خطاها به شکل { "error": "توضیح فارسی" } است.

کدمعنیراه‌حل
400کلید API ارسال نشده، یا مختصات نامعتبر استپارامتر key و بازهٔ مختصات را بررسی کنید
401کلید نامعتبر یا غیرفعال استکلید را از داشبورد دوباره بگیرید / فعال کنید
402اعتبار سرویس نقشه تمام شده استاز صفحهٔ شارژ حساب را شارژ کنید
403 دامنهٔ درخواست جزو دامنه‌های مجاز کلید نیست، یا کلید عمومیِ دامنه‌دار بدون هدر Origin صدا زده شده است دامنه را در داشبورد به کلید اضافه کنید؛ برای فراخوانی سرور-به-سرور کلید سرّی بسازید
422مسیر پیدا نشد، یا مبدأ/مقصد به هیچ جاده‌ای نزدیک نیستنقطه را روی جاده‌ای معتبر و داخل ایران انتخاب کنید
429تعداد درخواست از سقف نرخ گذشته استهدر Retry-After را ببینید و درخواست‌ها را پخش کنید
503گراف مسیریابی یا نمایهٔ جستجو روی سرور در دسترس نیستموقتی است؛ بعداً دوباره تلاش کنید

مدیریت خطا در کد

alooRoute(a, b)
  .then(drawRoute)
  .catch(function (err) {
    // پیام‌های سرویس فارسی و قابل نمایش مستقیم به کاربرند
    document.getElementById('routeError').textContent = err.message;
  });

۱۵) هزینه‌ها و سشن

عملیاتهزینهتوضیح
لود نقشه (ساخت سشن جدید)23 تومانیک بار در هر مرورگر، تا انقضای سشن
هر بخش مسیریابی39 تومانمسیر ساده = ۱ بخش؛ مسیر با ۲ توقف = ۳ بخش
هر خانهٔ ماتریس20 تومانمبدأها × مقصدها (حداکثر 2,500 خانه)
هر جستجو یا ژئوکدینگ معکوس39 تومان/api/v1/search و /api/v1/reverse
هر سشن تکمیل خودکار 39 تومان یک بار برای کل تایپ کاربر، نه به‌ازای هر ضربهٔ کلید
هر تصویر ثابت نقشه23 تومان/api/v1/staticmap
تایل، استایل، فونترایگانداخل سشن معتبر، بدون سقف

برای برآورد هزینهٔ ماهانه از ماشین‌حساب تعرفه‌ها استفاده کنید.

سشن 6 ساعت معتبر است و در localStorage مرورگر کاربر ذخیره می‌شود؛ بنابراین رفرش‌کردن صفحه، جابه‌جایی بین صفحات سایت و باز کردن تب جدید هزینهٔ تازه ندارند. فقط انقضای سشن یعنی یک لود جدید. با این حال، هر لود صفحه سشن ذخیره‌شده را (رایگان) با سرور می‌سنجد: اگر موجودی به هزینهٔ یک لود نرسد یا کلید غیرفعال شود، نقشه به‌جای نمایش، پیام شارژ حساب را نشان می‌دهد.

موجودی و ریز تراکنش‌ها را در داشبورد می‌بینید — به همراه نمودار مصرف ۳۰ روز اخیر به تفکیک کلید و نوع درخواست. مقدار balance در پاسخ هر endpoint هزینه‌دار هم موجودی لحظه‌ای را برمی‌گرداند. برای اینکه سرویس بی‌خبر قطع نشود، در داشبورد آستانهٔ هشدار موجودی کم را تنظیم کنید.

۱۶) محدودیت‌ها و نکات مهم

۱۷) نمونهٔ کامل — نقشه + انتخاب مبدأ/مقصد + مسیر و مسافت

این فایل را کامل کپی کنید، فقط YOUR_API_KEY را عوض کنید:

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>مسیریاب من</title>
  <style>
    body { font-family: Tahoma, sans-serif; margin: 0; padding: 16px; }
    #map { height: 70vh; border-radius: 12px; }
    .bar { margin-bottom: 10px; font-size: 14px; }
    .bar b { color: #1a73e8; }
  </style>
</head>
<body>
  <div class="bar">
    روی نقشه کلیک کنید: اولین کلیک مبدأ، دومین کلیک مقصد.
    <span id="out"></span>
    <button id="reset">شروع دوباره</button>
  </div>
  <div id="map"></div>

  <script src="https://map.bitjob.ir/map.js?key=YOUR_API_KEY"></script>
  <script>
  var KEY = 'YOUR_API_KEY';

  function fmtDistance(m) {
    return m < 1000 ? Math.round(m) + ' متر' : (m / 1000).toFixed(1) + ' کیلومتر';
  }
  function fmtDuration(s) {
    var h = Math.floor(s / 3600), m = Math.round((s % 3600) / 60);
    return (h ? h + ' ساعت و ' : '') + Math.max(1, m) + ' دقیقه';
  }

  AlooMap.createMap('map', { center: [35.6892, 51.3890], zoom: 11 }).then(function (map) {
    var from = null, to = null, fromPin = null, toPin = null, line = null;
    var out = document.getElementById('out');

    function reset() {
      [fromPin, toPin, line].forEach(function (l) { if (l) map.removeLayer(l); });
      from = to = fromPin = toPin = line = null;
      out.textContent = '';
    }
    document.getElementById('reset').onclick = reset;

    map.on('click', function (e) {
      if (from && to) reset();

      if (!from) {
        from = e.latlng;
        fromPin = L.marker(from, { draggable: true }).addTo(map).bindPopup('مبدأ').openPopup();
        out.textContent = 'حالا مقصد را انتخاب کنید…';
        return;
      }

      to = e.latlng;
      toPin = L.marker(to, { draggable: true }).addTo(map).bindPopup('مقصد');
      out.textContent = 'در حال محاسبه مسیر…';

      fetch(AlooMap.base + '/api/v1/route', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          key: KEY,
          fromLat: from.lat, fromLng: from.lng,
          toLat: to.lat,     toLng: to.lng
        })
      })
      .then(function (res) {
        return res.json().then(function (d) {
          if (!res.ok) throw new Error(d.error || 'خطای مسیریابی');
          return d;
        });
      })
      .then(function (r) {
        line = L.polyline(r.latlng, { color: '#1a73e8', weight: 5 }).addTo(map);
        map.fitBounds(line.getBounds(), { padding: [40, 40] });
        out.innerHTML = 'مسافت: <b>' + fmtDistance(r.meters) + '</b> — ' +
                        'زمان تقریبی: <b>' + fmtDuration(r.seconds) + '</b> — ' +
                        'فاصله مستقیم: ' + fmtDistance(map.distance(from, to));
      })
      .catch(function (err) { out.textContent = err.message; });
    });
  });
  </script>
</body>
</html>

۱۸) عیب‌یابی

مشکلعلت و راه‌حل
نقشه دیده نمی‌شود / کادر خالی است عنصر نقشه ارتفاع ندارد. برایش height بگذارید.
نقشه نصفه یا خاکستری رندر شده نقشه در حالت مخفی (تب/مودال/آکاردئون) ساخته شده؛ بعد از نمایان‌شدن map.invalidateSize() را صدا بزنید.
خطای «AlooMap هنوز آماده نیست» AlooMap.map() را بیرون از ready صدا زده‌اید؛ یا داخل ready ببرید یا از createMap استفاده کنید.
پیام «این دامنه برای کلید API مجاز نیست» دامنهٔ فعلی در فهرست دامنه‌های مجاز کلید نیست (localhost هم باید اضافه شود یا فهرست خالی بماند).
«اعتبار سرویس نقشه تمام شده است» موجودی کمتر از هزینهٔ یک لود است؛ حساب را شارژ کنید.
مسیریابی 422 می‌دهد نقطه بیرون از ایران است یا هیچ جاده‌ای تا شعاع ۵ کیلومتری‌اش نیست؛ نقطه را نزدیک‌تر به جاده انتخاب کنید.
مارکر پیش‌فرض بدون تصویر است استایل Leaflet را دستکاری کرده‌اید یا CSS سایت آن را override می‌کند؛ از L.icon با تصویر خودتان استفاده کنید.
حروف فارسی روی نقشه به‌هم‌ریخته است پلاگین RTL خودکار لود می‌شود؛ اگر MapLibre را مستقیم می‌سازید، خودتان setRTLTextPlugin را صدا بزنید.
موقعیت کاربر گرفته نمی‌شود سایت باید روی HTTPS باشد و کاربر اجازه بدهد؛ داخل iframe نیاز به allow="geolocation" دارد.

سؤالی باقی مانده؟ از داشبورد با نمونهٔ کد و پیام خطا تماس بگیرید.