Files
Verde-Web/app/Console/Commands/SimulateTruckMovement.php

189 lines
6.6 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Trip;
use App\Models\TripStop;
use App\Models\Truck;
use App\Models\User;
use App\Services\LiveTracking\TruckTracker;
use App\Services\Trip\TripExecutor;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SimulateTruckMovement extends Command
{
protected $signature = 'track:simulate {--truck=VRD-SP-001 : Plate number of the truck} {--speed=2 : Sleep duration in seconds between steps}';
protected $description = 'Simulate real-time GPS telemetry and stop transitions for an active collection trip';
public function handle(TruckTracker $tracker, TripExecutor $executor): int
{
$plate = $this->option('truck');
$delay = (int) $this->option('speed');
$truck = Truck::where('plate_number', $plate)->first();
if (! $truck) {
$this->error("Truck with plate number {$plate} not found.");
return self::FAILURE;
}
// Find the active or scheduled trip for this truck
$trip = Trip::where('truck_id', $truck->id)
->whereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_SCHEDULED, Trip::STATUS_AT_DUMPSITE])
->first();
if (! $trip) {
$this->error("No active or scheduled trip found for truck {$plate}.");
return self::FAILURE;
}
$driver = User::find($trip->team->driver_id);
if (! $driver) {
$this->error('No driver assigned to the team for this trip.');
return self::FAILURE;
}
$this->info("Starting simulation on Trip: {$trip->trip_number} with Truck: {$truck->plate_number}");
$this->info("Driver: {$driver->first_name} {$driver->last_name}");
// Reset trip to clean state for replay/simulation
DB::transaction(function () use ($trip) {
$trip->forceFill([
'status' => Trip::STATUS_SCHEDULED,
'actual_start_time' => null,
])->save();
$trip->timelineEvents()->delete();
foreach ($trip->stops as $stop) {
$stop->forceFill([
'status' => TripStop::STATUS_PENDING,
'actual_arrival' => null,
'actual_departure' => null,
'coordinates_at_arrival' => null,
])->save();
}
});
$this->info('Trip state reset to Scheduled.');
// Start the trip
$startLat = 13.7900;
$startLng = 121.0200;
$this->info("Starting trip at: Lat {$startLat}, Lng {$startLng}");
$trip = $executor->start($trip, $driver, $startLat, $startLng);
$this->info('Trip status: IN_PROGRESS');
$currentLat = $startLat;
$currentLng = $startLng;
$stops = $trip->stops()->orderBy('sequence')->get();
foreach ($stops as $index => $stop) {
$dop = $stop->dropOffPoint;
if (! $dop || ! $dop->coordinates) {
continue;
}
$destLat = $dop->coordinates->latitude;
$destLng = $dop->coordinates->longitude;
$this->comment("\nMoving towards Stop #{$stop->sequence}: {$dop->name}...");
// Interpolate 5 steps to reach the stop
$stepsCount = 5;
for ($step = 1; $step <= $stepsCount; $step++) {
$ratio = $step / $stepsCount;
$lat = $currentLat + ($destLat - $currentLat) * $ratio;
$lng = $currentLng + ($destLng - $currentLng) * $ratio;
// Calculate heading
$heading = 45; // arbitrary default
if ($destLng != $currentLng) {
$rad = atan2($destLat - $currentLat, $destLng - $currentLng);
$heading = (int) round(90 - rad2deg($rad));
if ($heading < 0) {
$heading += 360;
}
}
$tracker->record(
truck: $truck,
lat: $lat,
lng: $lng,
heading: $heading,
speedKmh: 35.0,
trip: $trip
);
$this->line(" Step {$step}/{$stepsCount} -> Lat: ".round($lat, 6).', Lng: '.round($lng, 6).", Heading: {$heading}°");
sleep($delay);
}
$currentLat = $destLat;
$currentLng = $destLng;
// Arrive at stop
$this->info("Arriving at Stop #{$stop->sequence}: {$dop->name}");
$stop = $executor->arriveAtStop($stop, $driver, $currentLat, $currentLng);
sleep($delay * 2);
// Depart stop
$this->info("Departing from Stop #{$stop->sequence}: {$dop->name}");
$executor->departStop($stop, $driver, $currentLat, $currentLng);
sleep($delay);
}
// Move to the Dumpsite
$dumpsite = $trip->dumpsite;
if ($dumpsite && $dumpsite->coordinates) {
$destLat = $dumpsite->coordinates->latitude;
$destLng = $dumpsite->coordinates->longitude;
$this->comment("\nMoving towards Dumpsite: {$dumpsite->name}...");
$stepsCount = 5;
for ($step = 1; $step <= $stepsCount; $step++) {
$ratio = $step / $stepsCount;
$lat = $currentLat + ($destLat - $currentLat) * $ratio;
$lng = $currentLng + ($destLng - $currentLng) * $ratio;
// Calculate heading
$heading = 45;
if ($destLng != $currentLng) {
$rad = atan2($destLat - $currentLat, $destLng - $currentLng);
$heading = (int) round(90 - rad2deg($rad));
if ($heading < 0) {
$heading += 360;
}
}
// The last step will fall inside the dumpsite geofence and trigger it!
$result = $tracker->record(
truck: $truck,
lat: $lat,
lng: $lng,
heading: $heading,
speedKmh: 45.0,
trip: $trip
);
$this->line(" Step {$step}/{$stepsCount} -> Lat: ".round($lat, 6).', Lng: '.round($lng, 6));
if ($result->geofenceTriggered) {
$this->warn(' [GEOFENCE] Entered Dumpsite boundary! Trip status transitioned automatically.');
}
sleep($delay);
}
}
$this->info("\nSimulation complete! Truck reached the dumpsite.");
return self::SUCCESS;
}
}