api/docs/SCENARIO_MATRIX_ROUTING_PGROUTING.md
2026-03-17 12:30:28 +00:00

14 KiB
Raw Permalink Blame History

سناریوی نهایی: API مسیریابی ماتریسی با pgRouting

این سند سناریوی کامل پیاده‌سازی Matrix Routing API بر اساس pgRouting و داده‌های OSM در دیتابیس gis را شرح می‌دهد. سرویس فقط برای نقاط واقع در محدوده ایران فعال است؛ در غیر این صورت خطا برگردانده می‌شود.


۱. وضعیت فعلی (پیش‌فرض)

  • دیتابیس: PostgreSQL با PostGIS، نام دیتابیس gis
  • جداول OSM: planet_osm_point, planet_osm_line, planet_osm_polygon (اسکیمای استاندارد osm2pgsql)
  • اتصال اپ: PDO با کاربر search_user به gis
  • مسیریابی: وجود ندارد؛ فقط جستجو، وکتور تایل و امکانات مشابه

۲. معماری کلی

کلاینت
   │
   │  POST /api/matrix  (origins[], destinations[])
   ▼
Symfony (MatrixController / MatrixService)
   │
   │  ۱) اعتبارسنجی و محدودیت N×M
   │  ۲) Snap نقاط به گراف (یافتن نزدیک‌ترین گره)
   │  ۳) فراخوانی pgRouting برای ماتریس
   ▼
PostgreSQL (gis)
   │  - extension: pgrouting
   │  - جداول گراف: ways_routing, ways_routing_vertices_pgr
   │  - توابع: pgr_dijkstraCostMatrix یا حلقه pgr_dijkstra
   ▼
پاسخ JSON: { distances: [[...]], durations: [[...]] }

۳. لایهٔ دادهٔ مسیریابی (گراف)

۳.۱ نیازمندی

  • جداول فعلی planet_osm_* برای مسیریابی به‌صورت مستقیم استفاده نمی‌شوند؛ pgRouting به یک گراف جهت‌دار نیاز دارد:
    • گره‌ها (vertices): نقاط اتصال جاده‌ها
    • یال‌ها (edges): قطعات جاده با فیلدهای source, target, cost (و ترجیحاً reverse_cost برای دوطرفه)

۳.۲ دو راه برای ساخت گراف

راه الف) استفاده از osm2pgrouting (توصیه برای شروع)

  • ورودی: همان فایل OSM (مثلاً iran-latest.osm.pbf) که برای به‌روزرسانی دیتابیس استفاده می‌کنید.
  • خروجی در همان دیتابیس gis: جداول با نام‌های مشابه ways, ways_vertices_pgr (بسته به mapconfig).
  • مزیت: استاندارد و مستند؛ فیلدهای length, cost, reverse_cost از قبل محاسبه می‌شوند.
  • نام schema/جدول را در سناریو به‌صورت routing در نظر می‌گیریم (مثلاً routing.ways, routing.ways_vertices_pgr) تا از جداول OSM اصلی جدا باشد.

راه ب) ساخت گراف از planet_osm_line

  • با اسکریپت/تابع SQL یا ابزار سفارشی، از خطوط با highway IS NOT NULL جدول یال‌ها و گره‌ها ساخته شود و در جداول جدا (مثلاً routing.ways, routing.ways_vertices_pgr) ذخیره شود.
  • مزیت: وابستگی به همان دیتایی که الان در PostGIS دارید؛ یک منبع حقیقت.
  • معایب: پیاده‌سازی و تست بیشتر؛ مدیریت one-way و سرعت بر اساس highway بر عهدهٔ شماست.

در سناریو نهایی فرض می‌کنیم جداول گراف با نام‌های زیر وجود دارند (در schemaی جدا یا در public):

  • routing_ways (یا ways):
    حداقل فیلدها: id, source, target, cost (متر یا ثانیه), reverse_cost, geom (geometry)
  • routing_ways_vertices_pgr (یا ways_vertices_pgr):
    حداقل فیلدها: id, the_geom (geometry)

و در صورت استفاده از osm2pgrouting، نام دقیق جداول طبق mapconfig (مثلاً ways, ways_vertices_pgr) در همان schema در نظر گرفته می‌شود.


۴. نصب و تنظیم pgRouting

  • نصب extension در دیتابیس gis:
    CREATE EXTENSION IF NOT EXISTS pgrouting;
    
  • اطمینان از وجود PostGIS (قبلاً دارید).
  • کاربر search_user (یا کاربری که اپ از آن استفاده می‌کند) باید دسترسی SELECT به جداول و توابع pgRouting و جداول گراف داشته باشد.

۵. به‌روزرسانی داده (هماهنگ با به‌روزرسانی OSM)

  • هر بار که داده OSM را در سرور به‌روز می‌کنید (مثلاً با osm2pgsql برای planet_osm_* یا با جایگزینی فایل OSM):
    1. اگر از osm2pgrouting استفاده می‌کنید: بعد از به‌روزرسانی، دوباره برای همان فایل OSM (یا اکسپورت فعلی ایران) اجرا کنید و خروجی را در جداول گراف (مثلاً در schema routing) بریزید؛ ترجیحاً با TRUNCATE + INSERT یا DROP + CREATE برای یکپارچگی.
    2. اگر گراف را از planet_osm_line می‌سازید: بعد از به‌روزرسانی جداول OSM، اسکریپت/تابع بازسازی گراف را اجرا کنید تا routing_ways و routing_ways_vertices_pgr به‌روز شوند.
  • این مرحله می‌تواند در همان اسکریپت/کرون به‌روزرسانی سرور قرار گیرد تا بعد از هر به‌روز OSM، گراف مسیریابی هم یکپارچه به‌روز شود.

۶. طراحی API

۶.۱ Endpoint

  • مسیر پیشنهادی: POST /api/matrix
  • متد: POST (به‌دلیل تعداد نقاط و طول URL در GET)
  • CORS: مانند سایر APIها (مثلاً همان listener فعلی) برای دامنه‌های مجاز.

۶.۲ ورودی (Request Body – JSON)

{
  "origins": [
    [51.42, 35.69],
    [51.38, 35.72]
  ],
  "destinations": [
    [51.43, 35.70],
    [51.39, 35.71],
    [51.40, 35.68]
  ],
  "options": {
    "metrics": ["distance", "duration"],
    "max_snap_distance_m": 500
  }
}
  • origins: آرایهٔ نقاط مبدأ؛ هر نقطه [longitude, latitude] (WGS84).
  • destinations: آرایهٔ نقاط مقصد؛ همان فرمت.
  • options (اختیاری):
    • metrics: ["distance"] و/یا ["duration"]؛ تعیین می‌کند در پاسخ فاصله (متر) و/یا زمان (ثانیه) برگردانده شود.
    • max_snap_distance_m: حداکثر فاصله (متر) برای snap نقطه به گراف؛ اگر نزدیک‌ترین گره دورتر باشد، آن نقطه نامعتبر تلقی شده و در پاسخ با مقدار null یا کد خطا مشخص می‌شود.

۶.۳ محدودیت‌های سایز

  • حداکثر تعداد origins و destinations تا حد معقول (مثلاً هر کدام حداکثر 25 یا 50) تا زمان پاسخ و بار دیتابیس قابل کنترل باشد.
  • حداکثر کل جفت‌ها (N×M) هم می‌تواند محدود شود (مثلاً 1000 یا 2500).
  • در صورت نقض محدودیت: پاسخ 400 با پیام واضح (مثلاً «حداکثر تعداد origins 25 است»).

۶.۴ خروجی (Response – JSON)

{
  "origins": [
    { "index": 0, "snapped": [51.4201, 35.6902], "valid": true },
    { "index": 1, "snapped": [51.3802, 35.7201], "valid": true }
  ],
  "destinations": [
    { "index": 0, "snapped": [51.4300, 35.7001], "valid": true },
    { "index": 1, "snapped": [51.3901, 35.7100], "valid": true },
    { "index": 2, "snapped": null, "valid": false }
  ],
  "distances": [
    [1200, 3500, null],
    [4100, 800, null]
  ],
  "durations": [
    [180, 420, null],
    [520, 95, null]
  ]
}
  • distances: ماتریس N×M؛ مقدار هر سلول فاصله به متر (یا null اگر مسیر یافت نشود یا نقطه نامعتبر باشد).
  • durations: ماتریس N×M؛ مقدار هر سلول زمان به ثانیه (یا null در همان شرایط).
  • origins/destinations: برای هر نقطه، ایندکس، مختصات snapped (در WGS84) و اینکه آیا در محدودهٔ snap قرار گرفته و در گراف استفاده شده است (valid: true/false).

در صورت خطای کلی (مثلاً پارامتر نامعتبر یا خطای دیتابیس): 500 با بدنهٔ JSON حاوی پیام خطا (و در حالت توسعه، جزئیات بیشتر).


۷. مراحل پردازش در سمت سرور (Symfony)

  1. دریافت و اعتبارسنجی ورودی

    • پارس JSON؛ بررسی وجود origins و destinations و آرایه بودن آن‌ها.
    • اعمال سقف روی تعداد origins، destinations و N×M؛ در صورت نقض برگرداندن 400.
  2. Snap به گراف

    • برای هر نقطه (هر مبدأ و هر مقصد): با یک کوئری PostGIS نزدیک‌ترین رأس در routing_ways_vertices_pgr را پیدا کنید (مثلاً با ST_DWithin + ORDER BY ST_Distance(the_geom, ST_Transform(ST_SetSRID(ST_Point(lon,lat), 4326), SRID_گراف)) LIMIT 1).
    • اگر فاصله بیشتر از max_snap_distance_m بود، نقطه را نامعتبر علامت بزنید و در ماتریس برای آن سطر/ستون null بگذارید.
    • خروجی این مرحله: دو لیست از شناسهٔ گره (vertex id) برای مبدأها و مقصدها (به‌همراه پرچم valid).
  3. محاسبهٔ ماتریس با pgRouting

    • روش ۱: اگر pgRouting شما pgr_dijkstraCostMatrix پشتیبانی می‌کند و محدودیت تعداد رأس را رعایت می‌کنید، یک بار برای تمام گره‌های مبدأ و مقصد (اتحاد آن‌ها) ماتریس cost را بگیرید و سپس سطر/ستون‌های مربوط به origins و destinations را استخراج کنید.
    • روش ۲: برای هر مبدأ، یک بار pgr_dijkstra از آن مبدأ به همهٔ مقصدها؛ جمع‌آوری cost (فاصله یا زمان) در یک ماتریس N×M.
    • اگر cost در جدول یال به‌صورت فاصله (متر) ذخیره شده، خروجی همان distances است؛ برای durations یا باید cost را بر اساس طول و سرعت متوسط محاسبه کرده باشید، یا ستون جدا (مثلاً travel_time_sec) در جدول یال داشته باشید و از آن استفاده کنید.
  4. ساخت پاسخ

    • پر کردن آرایه‌های distances و durations (در صورت درخواست)؛ برای نقاط نامعتبر یا مسیر ناموجود مقدار null.
    • برگرداندن JSON با ساختار بالا و هدرهای CORS مناسب.

۸. هزینهٔ یال (cost) و مدت زمان (duration)

  • در جدول یال‌ها حداقل یک cost لازم است (معمولاً طول به متر یا زمان به ثانیه).
  • برای duration: یا در زمان ساخت گراف یک فیلد travel_time_sec (یا مشابه) بر اساس نوع جاده و سرعت پیش‌فرض محاسبه و ذخیره شود، یا در همان cost از «زمان» استفاده شود و فاصله را در لایهٔ اپ با ضریب سرعت تقریبی به زمان تبدیل کنید (کمتر توصیه می‌شود ولی امکان‌پذیر است).
  • در سناریو نهایی فرض می‌کنیم جدول گراف هم cost_length (متر) و هم cost_time (ثانیه) دارد، یا فقط length داریم و duration در اپ با سرعت متوسط (مثلاً 50 km/h برای جاده‌های درون‌شهری) تقریب زده می‌شود تا در پاسخ durations قابل ارائه باشد.

۹. امنیت و دسترسی

  • کاربر دیتابیس (search_user) فقط به SELECT روی جداول گراف و توابع pgRouting نیاز دارد؛ نیازی به INSERT/UPDATE روی جداول مسیریابی در زمان سرویس‌دهی نیست.
  • API می‌تواند در مرحلهٔ اول بدون احراز هویت باشد (مشابه وکتور تایل و جستجو)؛ در صورت نیاز بعداً می‌توان API key یا rate limit اضافه کرد.

۱۰. خلاصهٔ چک‌لیست پیاده‌سازی

مرحله توضیح
۱ نصب extension pgRouting در دیتابیس gis
۲ ساخت/وارد کردن گراف (osm2pgrouting یا اسکریپت از planet_osm_line) در جداول routing_ways و routing_ways_vertices_pgr (یا نام‌های نهایی انتخاب‌شده)
۳ تعریف cost (و در صورت امکان duration) برای یال‌ها و اطمینان از SRID یکسان برای هندسهٔ گراف
۴ اسکریپت/کرون به‌روزرسانی گراف پس از هر به‌روزرسانی OSM
۵ در Symfony: سرویس/ریپازیتوری برای اتصال به gis و توابع Snap + Matrix (در صورت نیاز جدا از OsmSearchRepository یا استفاده از همان اتصال)
۶ کنترلر POST /api/matrix با اعتبارسنجی، محدودیت سایز، فراخوانی سرویس ماتریس و برگرداندن JSON
۷ تست با چند مبدأ/مقصد واقعی و مقایسه با نقشه
۸ مستندسازی endpoint در سایت/صفحهٔ API (مثلاً در راهنمای استفاده یا enterprise)

۱۱. نکات نهایی

  • SRID: گراف معمولاً در یک SRID متریک (مثلاً 3857 یا 32639 برای UTM ایران) نگهداری می‌شود؛ در Snap ورودی WGS84 را به SRID گراف تبدیل کنید.
  • یک‌طرفه/دوطرفه: جدول یال باید reverse_cost داشته باشد تا جاده‌های یک‌طرفه درست محاسبه شوند؛ در غیر این صورت همهٔ یال‌ها دوطرفه در نظر گرفته می‌شوند.
  • عملکرد: برای N,M کوچک (مثلاً تا 25) پاسخ در حد قابل قبول است؛ برای سایزهای بزرگتر ممکن است نیاز به timeout یا محدودیت سخت‌تر یا کش کردن نتایج پرتکرار باشد.

پس از تأیید این سناریو می‌توان مرحلهٔ ۱ تا ۸ را در پروژه پیاده‌سازی کرد.