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

223 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# سناریوی نهایی: 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`:
```sql
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)
```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)
```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 یا محدودیت سخت‌تر یا کش کردن نتایج پرتکرار باشد.
پس از تأیید این سناریو می‌توان مرحلهٔ ۱ تا ۸ را در پروژه پیاده‌سازی کرد.