پرش به مطلب اصلی

Regionalization

Regionalization در Doravel دو مسئولیت روشن دارد:

  • کار با ناحیه‌های جغرافیایی به شکل fluent و شبیه query builder
  • تبدیل مختصات بین Lat/Long و UTM

این API طوری طراحی شده که برای استفاده‌ی روزمره، ورودی اصلی شما Lat/Long باشد و فقط در زمان نیاز، بین دو سیستم مختصات تبدیل انجام دهید.

use Dornica\Foundation\Regionalization\Facade\Regionalizer;

شروع سریع

مثال ۱: دریافت شهرهای یک استان

use Dornica\Foundation\Regionalization\Enums\LocationType;

$cities = Regionalizer::forLocation(LocationType::CITY)
->whereParent($provinceId)
->get();

مثال ۲: دریافت شناسه‌ی ناحیه‌های کاربر فعلی

use Dornica\Foundation\Regionalization\Enums\LocationType;

$cityIds = Regionalizer::forUser()
->forLocation(LocationType::CITY)
->pluckIds();

مثال ۳: تبدیل مختصات Lat/Long به UTM

use Dornica\Foundation\Regionalization\DTOs\LatLongPoint;

$utmPoint = Regionalizer::toUtm(
new LatLongPoint(35.6892, 51.3890)
);

مثال ۴: تبدیل UTM به Lat/Long

use Dornica\Foundation\Regionalization\DTOs\UtmPoint;

$latLongPoint = Regionalizer::toLatLong(
new UtmPoint(535307.7558, 3949543.6647, 39)
);

مفاهیم اصلی

LocationType

برای انتخاب نوع ناحیه از enum زیر استفاده کنید:

use Dornica\Foundation\Regionalization\Enums\LocationType;
  • LocationType::COUNTRY
  • LocationType::PROVINCE
  • LocationType::CITY
  • LocationType::DISTRICT
  • LocationType::RURAL_DISTRICT
  • LocationType::VILLAGE
  • LocationType::NEIGHBORHOOD

DTOها

در این ماژول فقط دو DTO اصلی برای مختصات وجود دارد:

  • LatLongPoint
  • UtmPoint

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

Query Builder Style API

forLocation(LocationType $locationType)

نوع ناحیه‌ی هدف را مشخص می‌کند.

$query = Regionalizer::forLocation(LocationType::PROVINCE)->query();

whereParent(mixed $id = null)

نتیجه را بر اساس شناسه‌ی والد محدود می‌کند. مقدار ورودی می‌تواند:

  • یک شناسه باشد
  • آرایه‌ای از شناسه‌ها باشد
  • null باشد
Regionalizer::forLocation(LocationType::CITY)
->whereParent($provinceId)
->get();

Regionalizer::forLocation(LocationType::DISTRICT)
->whereParent([$cityId1, $cityId2])
->get();

forUser(object|int|null $user = null)

scope را روی کاربر فعلی یا یک کاربر مشخص قرار می‌دهد.

Regionalizer::forUser();
Regionalizer::forUser($userId);
Regionalizer::forUser($userModel);

query()

خود builder را برمی‌گرداند تا شرط‌های بیشتری به آن اضافه کنید.

use Dornica\Foundation\Regionalization\Enums\LocationType;

$query = Regionalizer::forUser()
->forLocation(LocationType::CITY)
->query();

$cities = $query?->orderBy('name')->get();

get()

خروجی را به صورت Collection برمی‌گرداند.

use Dornica\Foundation\Regionalization\Enums\LocationType;

$villages = Regionalizer::forLocation(LocationType::VILLAGE)
->whereParent($ruralDistrictId)
->get();

pluckIds()

فقط شناسه‌ها را برمی‌گرداند.

use Dornica\Foundation\Regionalization\Enums\LocationType;

$provinceIds = Regionalizer::forLocation(LocationType::PROVINCE)
->pluckIds();

اگر forUser() هم استفاده شود، شناسه‌ها از relationهای همان کاربر خوانده می‌شوند:

use Dornica\Foundation\Regionalization\Enums\LocationType;

$assignedNeighborhoodIds = Regionalizer::forUser($userId)
->forLocation(LocationType::NEIGHBORHOOD)
->pluckIds();

applyGeoScope(mixed $scope, Builder|null $query = null)

یک query دلخواه را بر اساس relationهای جغرافیایی scope ورودی محدود می‌کند.

use App\Models\Order;

$orders = Regionalizer::applyGeoScope($adminScope, Order::query())?->get();

این متد برای زمانی مفید است که scope یا model شما relationهایی مثل province، city، district، ruraldistrict، village یا neighborhood داشته باشد.

Coordinate Conversion API

در API عمومی Regionalization فقط دو متد تبدیل مختصات داریم:

  • toUtm()
  • toLatLong()

toUtm(LatLongPoint|array $point): UtmPoint

یک نقطه‌ی Lat/Long را به UTM تبدیل می‌کند.

use Dornica\Foundation\Regionalization\DTOs\LatLongPoint;

$utmPoint = Regionalizer::toUtm(
new LatLongPoint(
latitude: 35.6892,
longitude: 51.3890,
identifier: 1001
)
);

مثال با آرایه

$utmPoint = Regionalizer::toUtm([
'latitude' => 36.5559807,
'longitude' => 53.0512492,
'identifier' => 12,
]);

toLatLong(UtmPoint|array $point): LatLongPoint

یک نقطه‌ی UTM را به Lat/Long تبدیل می‌کند.

use Dornica\Foundation\Regionalization\DTOs\UtmPoint;

$latLongPoint = Regionalizer::toLatLong(
new UtmPoint(
x: 681964.6172,
y: 4046655.0000,
z: 39,
identifier: 2002
)
);

مثال با آرایه

$latLongPoint = Regionalizer::toLatLong([
'x' => 681964.6172,
'y' => 4046655.0000,
'zone' => 39,
'identifier' => 77,
]);

Geometry & Geospatial Calculations

علاوه بر تبدیل ساده‌ی نقطه، می‌توانید با اشیاء هندسی (Point، Polygon، MultiPolygon) کار کنید و فاصله، مساحت و مرکز هندسی را محاسبه کنید. همه از طریق همان facade در دسترس‌اند.

use Dornica\Foundation\Regionalization\Geometry\Point;
use Dornica\Foundation\Regionalization\Geometry\Polygon;
use Dornica\Foundation\Regionalization\Geometry\MultiPolygon;

ساخت اشیاء هندسی

// از LatLongPoint یا UtmPoint
$point = Regionalizer::geometryPoint(new LatLongPoint(35.6892, 51.3890));

// از آرایه‌ای از نقاط (Point | LatLongPoint | UtmPoint | array)
$polygon = Regionalizer::geometryPolygon([
new Point(35.6892, 51.3890),
new Point(35.6950, 51.3890),
new Point(35.6950, 51.3950),
new Point(35.6892, 51.3950),
]);

$multi = Regionalizer::geometryMultiPolygon([$polygon]);

هر شیء هندسی این خروجی‌ها را دارد:

$point->toWkt(); // POINT(51.389 35.6892)
$point->toGeoJson(); // ['type' => 'Point', 'coordinates' => [...]]
$point->toSqlExpression(); // SQL متناسب با driver فعال (مثلاً ST_GeomFromText(...))
$point->toArray();

برای ذخیره‌سازی در دیتابیس، به‌جای DB::raw($geometry->toSqlExpression()) از helper geo() یا Regionalizer::geo() استفاده کنید.

ذخیره‌سازی geometry

GeometryExpression یک Expression استاندارد لارavel است. SQL نهایی بر اساس driver فعال (mysql، pgsql، sqlsrv) ساخته می‌شود.

use Dornica\Foundation\Regionalization\Facade\Regionalizer;
use Dornica\Foundation\Regionalization\Models\Province;

$polygon = Regionalizer::geometryPolygon([
['latitude' => 35.6892, 'longitude' => 51.3890],
['latitude' => 35.6950, 'longitude' => 51.3890],
['latitude' => 35.6950, 'longitude' => 51.3950],
]);

// helper سراسری
$province->boundary_coordinates = geo($polygon);

// یا از facade
$province->boundary_coordinates = Regionalizer::geo($polygon);

$province->save();

Province::where('id', $provinceId)->update([
'boundary_coordinates' => geo($polygon),
]);
$province->boundary_coordinates = geo($polygon);

فاصله

use Dornica\Foundation\Regionalization\DTOs\LatLongPoint;
use Dornica\Foundation\Regionalization\DTOs\UtmPoint;

// طول و عرض جغرافیایی (Haversine) — متر
$d1 = Regionalizer::distanceBetweenLatLongPoints(
new LatLongPoint(35.6892, 51.3890),
new LatLongPoint(35.7000, 51.4000)
);

// UTM (اقلیدسی) — متر
$d2 = Regionalizer::distanceBetweenUtmPoints(
new UtmPoint(500000, 4000000, 39),
new UtmPoint(500300, 4000400, 39)
);

// بین دو شیء Point — متر
$d3 = Regionalizer::distanceBetweenPoints($pointA, $pointB);

مساحت

// از آرایه‌ای از UtmPoint (Shoelace) — مترمربع
$area = Regionalizer::polygonArea($utmPoints);

// مستقیم از شیء Polygon (تبدیل داخلی به UTM)
$area = Regionalizer::polygonAreaFromGeometry($polygon);

مرکز هندسی (Centroid)

// خروجی UtmPoint
$centroid = Regionalizer::polygonCentroid($utmPoints);

// خروجی Point (برای polygon هندسی)
$center = Regionalizer::polygonCentroidFromGeometry($polygon);

برای چندضلعی‌های تباه (مساحت صفر) متد centroid به میانگین حسابی نقاط برمی‌گردد.

کار مستقیم با DTOها

اگر نخواهید از facade استفاده کنید، خود DTOها هم متدهای تبدیل را دارند.

LatLongPoint::toUtmPoint()

use Dornica\Foundation\Regionalization\DTOs\LatLongPoint;

$latLongPoint = new LatLongPoint(35.6892, 51.3890);
$utmPoint = $latLongPoint->toUtmPoint();

UtmPoint::toLatLongPoint()

use Dornica\Foundation\Regionalization\DTOs\UtmPoint;

$utmPoint = new UtmPoint(681964.6172, 4046655.0000, 39);
$latLongPoint = $utmPoint->toLatLongPoint();

معرفی DTOها

LatLongPoint

Namespace: Dornica\Foundation\Regionalization\DTOs\LatLongPoint

فیلدهای اصلی:

  • latitude
  • longitude
  • identifier
  • attributes
  • sort

متدهای مهم:

  • fromArray(array $data)
  • fromArrayList(array $items)
  • getLatitude()
  • getLongitude()
  • toArray()
  • toUtmPoint()

UtmPoint

Namespace: Dornica\Foundation\Regionalization\DTOs\UtmPoint

فیلدهای اصلی:

  • x
  • y
  • z
  • zone
  • identifier
  • attributes
  • sort

متدهای مهم:

  • fromArray(array $data)
  • fromArrayList(array $items)
  • getX()
  • getY()
  • getZ()
  • getZone()
  • toArray()
  • toLatLongPoint()

Validation Rules

برای اعتبارسنجی ورودی‌های جغرافیایی (نقطه، چندضلعی، GeoJSON و محدوده) چند Rule آماده در ماژول وجود دارد.

use Dornica\Foundation\Regionalization\Rules\ValidPoint;
use Dornica\Foundation\Regionalization\Rules\ValidPolygon;
use Dornica\Foundation\Regionalization\Rules\ValidMultiPolygon;
use Dornica\Foundation\Regionalization\Rules\ValidUtmPoint;
use Dornica\Foundation\Regionalization\Rules\ValidGeoJson;
use Dornica\Foundation\Regionalization\Rules\PointWithinBoundary;

این Ruleها استاندارد لاراول (ValidationRule) هستند و نیازی به ثبت ندارند؛ مستقیم در rules() استفاده می‌شوند.

ValidPoint

Namespace: Dornica\Foundation\Regionalization\Rules\ValidPoint

یک نقطه‌ی جغرافیایی را اعتبارسنجی می‌کند. ورودی می‌تواند آرایه یا رشته باشد:

  • آرایه: ['latitude' => float, 'longitude' => float]
  • رشته: "lat,lng" مثل "35.6892,51.3890"

قواعد:

  • latitude بین -90 و 90
  • longitude بین -180 و 180
  • رشته‌ی ناقص (مثل "35.6" بدون طول جغرافیایی) نامعتبر است
public function rules(): array
{
return [
'location' => [new ValidPoint()],
'location.latitude' => ['required'],
'location.longitude' => ['required'],
];
}

ValidPolygon

Namespace: Dornica\Foundation\Regionalization\Rules\ValidPolygon

یک چندضلعی را اعتبارسنجی می‌کند. ورودی آرایه‌ای از نقاط است: [['latitude'=>…,'longitude'=>…], …]

قواعد:

  • حداقل minPoints نقطه (پیش‌فرض 3)
  • هر نقطه باید با ValidPoint معتبر باشد
  • حداقل minPoints نقطه‌ی یکتا (چندضلعی تباه/degenerate رد می‌شود)
use Dornica\Foundation\Regionalization\Rules\ValidPolygon;

public function rules(): array
{
return [
'boundary' => [new ValidPolygon()], // حداقل ۳ نقطه
'area' => [new ValidPolygon(minPoints: 4)],
];
}

ValidMultiPolygon

Namespace: Dornica\Foundation\Regionalization\Rules\ValidMultiPolygon

یک مجموعه از چندضلعی‌ها را اعتبارسنجی می‌کند (مناسب فیلد boundary_coordinates که از نوع MultiPolygon است). ورودی آرایه‌ای از polygonهاست که هر کدام آرایه‌ای از نقاط است.

قواعد:

  • حداقل یک polygon
  • هر polygon باید با ValidPolygon معتبر باشد
use Dornica\Foundation\Regionalization\Rules\ValidMultiPolygon;

public function rules(): array
{
return [
'boundary_coordinates' => [new ValidMultiPolygon()],
'zones' => [new ValidMultiPolygon(minPoints: 4)],
];
}

ValidUtmPoint

Namespace: Dornica\Foundation\Regionalization\Rules\ValidUtmPoint

یک نقطه‌ی UTM را اعتبارسنجی می‌کند. ورودی می‌تواند آرایه یا رشته باشد:

  • آرایه: ['x' => float, 'y' => float, 'z' => int] (کلید zone هم پذیرفته می‌شود)
  • رشته: "x,y" یا "x,y,z" مثل "500000,4000000,39"

قواعد:

  • x (easting) و y (northing) اجباری و عددی
  • zone در صورت وجود، عددی بین 1 و 60
use Dornica\Foundation\Regionalization\Rules\ValidUtmPoint;

public function rules(): array
{
return [
'point' => [new ValidUtmPoint()],
];
}

ValidGeoJson

Namespace: Dornica\Foundation\Regionalization\Rules\ValidGeoJson

ساختار یک شیء GeoJSON را اعتبارسنجی می‌کند. ورودی می‌تواند آرایه یا رشته‌ی JSON باشد و باید کلیدهای type و coordinates را داشته باشد.

نوع‌های پشتیبانی‌شده: Point، Polygon، MultiPolygon.

use Dornica\Foundation\Regionalization\Rules\ValidGeoJson;

public function rules(): array
{
return [
// هر نوع پشتیبانی‌شده
'geometry' => [new ValidGeoJson()],

// محدود به یک نوع خاص
'boundary' => [new ValidGeoJson(expectedType: 'MultiPolygon')],
];
}

PointWithinBoundary

Namespace: Dornica\Foundation\Regionalization\Rules\PointWithinBoundary

بررسی می‌کند که یک نقطه داخل یک محدوده (چندضلعی) باشد. از الگوریتم ray-casting استفاده می‌کند.

ورودی نقطه مثل ValidPoint (آرایه یا "lat,lng"). محدوده آرایه‌ای از نقاط [latitude, longitude] است؛ اگر داده نشود، از مرز پیکربندی‌شده‌ی کامپوننت Map (dornica-panel-kit.components.map.boundary) استفاده می‌کند.

use Dornica\Foundation\Regionalization\Rules\PointWithinBoundary;

public function rules(): array
{
return [
// محدوده‌ی صریح
'location' => [new PointWithinBoundary([
[35.70, 51.30],
[35.70, 51.50],
[35.60, 51.50],
[35.60, 51.30],
])],

// یا اتکا به مرز پیش‌فرض config
'point' => [new PointWithinBoundary()],
];
}

اگر هیچ محدوده‌ای (نه ورودی و نه config) موجود نباشد، این Rule محدودیتی اعمال نمی‌کند.

مثال‌های واقعی‌تر

مثال ۱: فرم ثبت شعبه

فرض کنید کاربر مختصات جغرافیایی را از فرم فرستاده و شما می‌خواهید آن را به UTM تبدیل و ذخیره کنید:

use Dornica\Foundation\Regionalization\DTOs\LatLongPoint;

$point = new LatLongPoint(
latitude: (float) request('latitude'),
longitude: (float) request('longitude')
);

$utmPoint = Regionalizer::toUtm($point);

Branch::query()->create([
'x' => $utmPoint->getX(),
'y' => $utmPoint->getY(),
'zone' => $utmPoint->getZone(),
]);

مثال ۲: نمایش مختصات ذخیره‌شده روی UI

اگر مختصات در دیتابیس به شکل UTM ذخیره شده باشند:

use Dornica\Foundation\Regionalization\DTOs\UtmPoint;

$utmPoint = new UtmPoint(
x: $branch->x,
y: $branch->y,
z: $branch->zone
);

$latLongPoint = Regionalizer::toLatLong($utmPoint);

مثال ۳: دریافت شهرهای منتسب به کاربر

use Dornica\Foundation\Regionalization\Enums\LocationType;

$cities = Regionalizer::forUser(auth()->id())
->forLocation(LocationType::CITY)
->get();

مثال ۴: دریافت روستاهای چند بخش

use Dornica\Foundation\Regionalization\Enums\LocationType;

$villages = Regionalizer::forLocation(LocationType::VILLAGE)
->whereParent([$ruralDistrictA, $ruralDistrictB])
->get();

نکات مهم

  • ترتیب dornica-app.regionalization.divisions باید یک زیرمجموعه‌ی پیوسته از این زنجیره باشد: country > province > city > district > rural_district > village > neighborhood
  • برای تبدیل UTM به Lat/Long باید zone مشخص باشد.
  • در ورودی‌های آرایه‌ای UtmPoint می‌توانید از کلید z یا zone استفاده کنید.
  • API جدید fluent است، ولی aliasهای قدیمی مثل user(), locations(), parent(), ids() و filterOnGeo() برای سازگاری هنوز قابل استفاده هستند.
  • Validation Ruleها (ValidPoint, ValidPolygon, ValidMultiPolygon, ValidUtmPoint, ValidGeoJson, PointWithinBoundary) استاندارد لاراول‌اند و مستقیم در rules() قابل استفاده‌اند.
  • برای ستون‌های spatial از geo() یا Regionalizer::geo() استفاده کنید؛ دیگر نیازی به DB::raw($geometry->toSqlExpression()) نیست.