163 lines
5.1 KiB
PHP
163 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace Modules\Tms\Services;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Tms\Models\RoutePlan;
|
|
use Modules\Tms\Models\RouteStop;
|
|
use Modules\Tms\Models\Shipment;
|
|
use Modules\Tms\Utils\TmsUtil;
|
|
|
|
class TmsRouteService
|
|
{
|
|
public function __construct(
|
|
protected TmsUtil $tmsUtil
|
|
) {}
|
|
|
|
public function generateRouteRefNo(int $business_id): string
|
|
{
|
|
$count = RoutePlan::where('business_id', $business_id)->count() + 1;
|
|
|
|
return 'RT-'.str_pad((string) $count, 5, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
public function createRoute(int $business_id, array $data): RoutePlan
|
|
{
|
|
return RoutePlan::create(array_merge($data, [
|
|
'business_id' => $business_id,
|
|
'ref_no' => $this->generateRouteRefNo($business_id),
|
|
'created_by' => auth()->id(),
|
|
]));
|
|
}
|
|
|
|
public function addShipmentsToRoute(RoutePlan $route, array $shipment_ids): void
|
|
{
|
|
$shipments = Shipment::where('business_id', $route->business_id)
|
|
->whereIn('id', $shipment_ids)
|
|
->get();
|
|
|
|
$maxOrder = (int) $route->stops()->max('stop_order');
|
|
|
|
foreach ($shipments as $shipment) {
|
|
if ($this->isShipmentBlockedForRoute($shipment)) {
|
|
continue;
|
|
}
|
|
|
|
$maxOrder++;
|
|
RouteStop::create([
|
|
'route_id' => $route->id,
|
|
'shipment_id' => $shipment->id,
|
|
'stop_order' => $maxOrder,
|
|
'latitude' => $shipment->destination_latitude,
|
|
'longitude' => $shipment->destination_longitude,
|
|
'address' => $shipment->destination_address,
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$shipment->update(['route_id' => $route->id]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Nearest-neighbor route optimization from origin.
|
|
*/
|
|
public function optimizeStops(RoutePlan $route): RoutePlan
|
|
{
|
|
$stops = $route->stops()->with('shipment')->get();
|
|
|
|
if ($stops->count() < 2) {
|
|
return $route;
|
|
}
|
|
|
|
$originLat = $route->origin_latitude;
|
|
$originLng = $route->origin_longitude;
|
|
|
|
if (! $originLat || ! $originLng) {
|
|
$first = $stops->first(fn ($s) => $s->latitude && $s->longitude);
|
|
if ($first) {
|
|
$originLat = $first->latitude;
|
|
$originLng = $first->longitude;
|
|
}
|
|
}
|
|
|
|
if (! $originLat || ! $originLng) {
|
|
return $route;
|
|
}
|
|
|
|
$remaining = $stops->filter(fn ($s) => $s->latitude && $s->longitude)->values();
|
|
$ordered = [];
|
|
$currentLat = (float) $originLat;
|
|
$currentLng = (float) $originLng;
|
|
$totalDistance = 0.0;
|
|
|
|
while ($remaining->isNotEmpty()) {
|
|
$nearestIdx = 0;
|
|
$nearestDist = PHP_FLOAT_MAX;
|
|
|
|
foreach ($remaining as $idx => $stop) {
|
|
$dist = $this->haversineKm($currentLat, $currentLng, (float) $stop->latitude, (float) $stop->longitude);
|
|
if ($dist < $nearestDist) {
|
|
$nearestDist = $dist;
|
|
$nearestIdx = $idx;
|
|
}
|
|
}
|
|
|
|
$next = $remaining->pull($nearestIdx);
|
|
$totalDistance += $nearestDist;
|
|
$currentLat = (float) $next->latitude;
|
|
$currentLng = (float) $next->longitude;
|
|
$ordered[] = $next;
|
|
}
|
|
|
|
$noCoords = $stops->filter(fn ($s) => ! $s->latitude || ! $s->longitude);
|
|
foreach ($noCoords as $stop) {
|
|
$ordered[] = $stop;
|
|
}
|
|
|
|
DB::transaction(function () use ($ordered, $route, $totalDistance) {
|
|
foreach ($ordered as $order => $stop) {
|
|
$stop->update(['stop_order' => $order + 1]);
|
|
}
|
|
$route->update([
|
|
'total_distance_km' => round($totalDistance, 2),
|
|
'status' => 'optimized',
|
|
]);
|
|
});
|
|
|
|
return $route->fresh(['stops.shipment']);
|
|
}
|
|
|
|
public function haversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float
|
|
{
|
|
$earthRadius = 6371;
|
|
$dLat = deg2rad($lat2 - $lat1);
|
|
$dLng = deg2rad($lng2 - $lng1);
|
|
$a = sin($dLat / 2) ** 2
|
|
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
|
|
|
|
return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
}
|
|
|
|
public function routeStatuses(): array
|
|
{
|
|
return [
|
|
'draft' => __('tms::lang.route_status_draft'),
|
|
'optimized' => __('tms::lang.route_status_optimized'),
|
|
'in_progress' => __('tms::lang.route_status_in_progress'),
|
|
'completed' => __('tms::lang.route_status_completed'),
|
|
'cancelled' => __('tms::lang.route_status_cancelled'),
|
|
];
|
|
}
|
|
|
|
protected function isShipmentBlockedForRoute(Shipment $shipment): bool
|
|
{
|
|
$settings = $this->tmsUtil->getTmsSettings($shipment->business_id);
|
|
|
|
if (empty($settings['exclude_uncleared_from_routes'])) {
|
|
return false;
|
|
}
|
|
|
|
return app(TmsCustomsShipmentSyncService::class)->customsBlocksTransit($shipment->loadMissing('customsClearance'));
|
|
}
|
|
}
|