feat: add track:simulate Artisan command to simulate live truck movement and geofencing
This commit is contained in:
180
app/Console/Commands/SimulateTruckMovement.php
Normal file
180
app/Console/Commands/SimulateTruckMovement.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user