189 lines
6.6 KiB
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;
|
|
}
|
|
}
|