diff --git a/app/Console/Commands/SimulateTruckMovement.php b/app/Console/Commands/SimulateTruckMovement.php new file mode 100644 index 0000000..db886fd --- /dev/null +++ b/app/Console/Commands/SimulateTruckMovement.php @@ -0,0 +1,180 @@ +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; + } +}