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