routes + route_stops tables. Admin CRUD with stops submitted as an ordered array (drag-reorder is client-side; sequence is array-index). Cloning produces an inactive copy with -COPY-XXXX suffix. RouteCalculator recomputes total_distance_km via ST_Distance_Sphere across stop coordinates + dumpsite, and estimated_duration_minutes from dwell time + travel time at config-driven avg speed (default 25 km/h). default_team_id stays nullable bigint until Module 9 adds the FK. 139 feature tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.0 KiB
PHP
63 lines
2.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Route;
|
|
|
|
use App\Models\Route;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class RouteCalculator
|
|
{
|
|
public const DEFAULT_AVG_SPEED_KMH = 25.0;
|
|
public const DUMPSITE_DWELL_MINUTES = 20;
|
|
|
|
/**
|
|
* Recompute total_distance_km and estimated_duration_minutes for the
|
|
* given route based on its current ordered stops + default dumpsite.
|
|
*/
|
|
public function recompute(Route $route): void
|
|
{
|
|
$route->load(['stops.dropOffPoint', 'defaultDumpsite']);
|
|
|
|
$points = $route->stops
|
|
->map(fn ($s) => $s->dropOffPoint?->coordinates)
|
|
->filter()
|
|
->values();
|
|
|
|
if ($route->defaultDumpsite?->coordinates) {
|
|
$points->push($route->defaultDumpsite->coordinates);
|
|
}
|
|
|
|
$distanceKm = 0.0;
|
|
for ($i = 0; $i < $points->count() - 1; $i++) {
|
|
$distanceKm += $this->haversineKm(
|
|
$points[$i]->latitude,
|
|
$points[$i]->longitude,
|
|
$points[$i + 1]->latitude,
|
|
$points[$i + 1]->longitude,
|
|
);
|
|
}
|
|
|
|
$dwellAtStops = (int) $route->stops->sum('estimated_duration_at_stop_minutes');
|
|
$avgSpeed = (float) config('routes.avg_speed_kmh', self::DEFAULT_AVG_SPEED_KMH);
|
|
$travelMinutes = $avgSpeed > 0 ? (int) round(($distanceKm / $avgSpeed) * 60) : 0;
|
|
$dumpsiteDwell = $route->defaultDumpsite ? self::DUMPSITE_DWELL_MINUTES : 0;
|
|
|
|
$route->forceFill([
|
|
'total_distance_km' => round($distanceKm, 3),
|
|
'estimated_duration_minutes' => $dwellAtStops + $travelMinutes + $dumpsiteDwell,
|
|
])->save();
|
|
}
|
|
|
|
private function haversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float
|
|
{
|
|
// Use MySQL ST_Distance_Sphere directly for consistency with the
|
|
// database geofence math.
|
|
$row = DB::selectOne(
|
|
'SELECT ST_Distance_Sphere(ST_SRID(POINT(?, ?), 4326), ST_SRID(POINT(?, ?), 4326)) AS m',
|
|
[$lng1, $lat1, $lng2, $lat2],
|
|
);
|
|
|
|
return ((float) $row->m) / 1000.0;
|
|
}
|
|
}
|