Files
Verde-Web/app/Services/Route/RouteCalculator.php
admin de77503cdd feat(backend): complete Module 8 (routes)
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>
2026-04-30 02:25:04 +08:00

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;
}
}