14 KiB
سناریوی نهایی: 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):- اگر از osm2pgrouting استفاده میکنید: بعد از بهروزرسانی، دوباره برای همان فایل OSM (یا اکسپورت فعلی ایران) اجرا کنید و خروجی را در جداول گراف (مثلاً در schema
routing) بریزید؛ ترجیحاً با TRUNCATE + INSERT یا DROP + CREATE برای یکپارچگی. - اگر گراف را از
planet_osm_lineمیسازید: بعد از بهروزرسانی جداول OSM، اسکریپت/تابع بازسازی گراف را اجرا کنید تاrouting_waysوrouting_ways_vertices_pgrبهروز شوند.
- اگر از osm2pgrouting استفاده میکنید: بعد از بهروزرسانی، دوباره برای همان فایل OSM (یا اکسپورت فعلی ایران) اجرا کنید و خروجی را در جداول گراف (مثلاً در schema
- این مرحله میتواند در همان اسکریپت/کرون بهروزرسانی سرور قرار گیرد تا بعد از هر بهروز 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یا کد خطا مشخص میشود.
- metrics:
۶.۳ محدودیتهای سایز
- حداکثر تعداد 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)
-
دریافت و اعتبارسنجی ورودی
- پارس JSON؛ بررسی وجود
originsوdestinationsو آرایه بودن آنها. - اعمال سقف روی تعداد origins، destinations و N×M؛ در صورت نقض برگرداندن 400.
- پارس JSON؛ بررسی وجود
-
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).
- برای هر نقطه (هر مبدأ و هر مقصد): با یک کوئری PostGIS نزدیکترین رأس در
-
محاسبهٔ ماتریس با pgRouting
- روش ۱: اگر pgRouting شما
pgr_dijkstraCostMatrixپشتیبانی میکند و محدودیت تعداد رأس را رعایت میکنید، یک بار برای تمام گرههای مبدأ و مقصد (اتحاد آنها) ماتریس cost را بگیرید و سپس سطر/ستونهای مربوط به origins و destinations را استخراج کنید. - روش ۲: برای هر مبدأ، یک بار
pgr_dijkstraاز آن مبدأ به همهٔ مقصدها؛ جمعآوری cost (فاصله یا زمان) در یک ماتریس N×M. - اگر cost در جدول یال بهصورت فاصله (متر) ذخیره شده، خروجی همان distances است؛ برای durations یا باید cost را بر اساس طول و سرعت متوسط محاسبه کرده باشید، یا ستون جدا (مثلاً
travel_time_sec) در جدول یال داشته باشید و از آن استفاده کنید.
- روش ۱: اگر pgRouting شما
-
ساخت پاسخ
- پر کردن آرایههای
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 یا محدودیت سختتر یا کش کردن نتایج پرتکرار باشد.
پس از تأیید این سناریو میتوان مرحلهٔ ۱ تا ۸ را در پروژه پیادهسازی کرد.