firebase = $firebase; $this->mapsService = $mapsService; $this->config = config('restaurant-delivery.tracking'); } /* |-------------------------------------------------------------------------- | Rider Location Updates |-------------------------------------------------------------------------- */ /** * Update rider's live location */ public function updateRiderLocation( Rider $rider, float $latitude, float $longitude, ?float $speed = null, ?float $bearing = null, ?float $accuracy = null ): array { // Validate accuracy $accuracyThreshold = config('restaurant-delivery.firebase.location.accuracy_threshold'); if ($accuracy && $accuracy > $accuracyThreshold) { Log::warning('Rider location accuracy too low', [ 'rider_id' => $rider->id, 'accuracy' => $accuracy, 'threshold' => $accuracyThreshold, ]); } // Check minimum distance change $lastLocation = $this->getLastRiderLocation($rider->id); $minDistance = config('restaurant-delivery.firebase.location.min_distance_change'); if ($lastLocation) { $distance = $this->calculateDistance( $lastLocation['lat'], $lastLocation['lng'], $latitude, $longitude ); // Skip if distance is less than minimum threshold (reduces noise) if ($distance < ($minDistance / 1000)) { // Convert meters to km return [ 'updated' => false, 'reason' => 'Distance change below threshold', 'distance' => $distance * 1000, ]; } } // Update Firebase $this->firebase->updateRiderLocation( $rider->id, $latitude, $longitude, $speed, $bearing, $accuracy ); // Update local database $rider->update([ 'current_latitude' => $latitude, 'current_longitude' => $longitude, 'last_location_update' => now(), ]); // Log location (if enabled) if (config('restaurant-delivery.logging.log_location_updates')) { LocationLog::create([ 'rider_id' => $rider->id, 'latitude' => $latitude, 'longitude' => $longitude, 'speed' => $speed, 'bearing' => $bearing, 'accuracy' => $accuracy, ]); } // Update tracking for all active deliveries $activeDeliveries = $rider->activeDeliveries; foreach ($activeDeliveries as $delivery) { $this->updateDeliveryTracking($delivery, $latitude, $longitude, $speed, $bearing); } // Dispatch event Event::dispatch(new RiderLocationUpdated($rider, $latitude, $longitude, $speed, $bearing)); return [ 'updated' => true, 'location' => [ 'lat' => $latitude, 'lng' => $longitude, 'speed' => $speed, 'bearing' => $bearing, ], 'active_deliveries' => $activeDeliveries->count(), ]; } /** * Get last known rider location */ public function getLastRiderLocation(int|string $riderId): ?array { // Try cache first $cached = Cache::get("rider_location_{$riderId}"); if ($cached) { return $cached; } // Try Firebase return $this->firebase->getRiderLocation($riderId); } /** * Check if rider has valid location */ public function hasValidLocation(Rider $rider): bool { if (! $this->firebase->isRiderLocationStale($rider->id)) { return true; } // Fallback to database if ($rider->last_location_update) { $threshold = config('restaurant-delivery.firebase.location.stale_threshold'); return $rider->last_location_update->diffInSeconds(now()) < $threshold; } return false; } /* |-------------------------------------------------------------------------- | Delivery Tracking |-------------------------------------------------------------------------- */ /** * Initialize tracking for a new delivery */ public function initializeDeliveryTracking(Delivery $delivery): bool { if (! $this->config['restaurant-delivery.maps']['enabled']) { return false; } // Get route from maps service $route = null; if ($delivery->rider) { $route = $this->mapsService->getRoute( $delivery->pickup_latitude, $delivery->pickup_longitude, $delivery->drop_latitude, $delivery->drop_longitude ); } $deliveryData = [ 'status' => DeliveryStatus::PENDING, 'rider_id' => $delivery->rider_id, 'restaurant_id' => $delivery->restaurant_id, 'restaurant_name' => $delivery->restaurant?->name, 'pickup_latitude' => $delivery->pickup_latitude, 'pickup_longitude' => $delivery->pickup_longitude, 'pickup_address' => $delivery->pickup_address, 'drop_latitude' => $delivery->drop_latitude, 'drop_longitude' => $delivery->drop_longitude, 'drop_address' => $delivery->drop_address, 'route' => $route ? [ 'polyline' => $route['polyline'], 'distance' => $route['distance'], 'duration' => $route['duration'], ] : null, 'eta' => $route ? $route['duration'] : null, 'distance' => $route ? $route['distance'] : null, ]; return $this->firebase->initializeDeliveryTracking($delivery->id, $deliveryData); } /** * Update delivery tracking with new rider location */ public function updateDeliveryTracking( Delivery $delivery, float $latitude, float $longitude, ?float $speed = null, ?float $bearing = null ): void { // Determine destination based on delivery status $destLat = $delivery->status->isPickedUp() ? $delivery->drop_latitude : $delivery->pickup_latitude; $destLng = $delivery->status->isPickedUp() ? $delivery->drop_longitude : $delivery->pickup_longitude; // Calculate remaining distance $remainingDistance = $this->calculateDistance($latitude, $longitude, $destLat, $destLng); // Calculate ETA based on speed or average $eta = $this->calculateETA($remainingDistance, $speed); // Update Firebase $this->firebase->updateDeliveryRiderLocation( $delivery->id, $latitude, $longitude, $speed, $bearing, $eta, $remainingDistance ); // Check for geofence triggers (auto status updates) $this->checkGeofenceTriggers($delivery, $latitude, $longitude); // Update route if significantly off track if ($this->isOffRoute($delivery, $latitude, $longitude)) { $this->recalculateRoute($delivery, $latitude, $longitude); } } /** * Update delivery status in tracking */ public function updateDeliveryStatus(Delivery $delivery, ?array $metadata = null): void { $statusConfig = config("restaurant-delivery.delivery_flow.statuses.{$delivery->status->value}"); $this->firebase->updateDeliveryStatus( $delivery->id, $delivery->status->value, array_merge($metadata ?? [], [ 'eta' => $delivery->estimated_delivery_time?->diffForHumans(), 'rider' => $delivery->rider ? [ 'id' => $delivery->rider->id, 'name' => $delivery->rider->full_name, 'phone' => $delivery->rider->phone, 'photo' => $delivery->rider->photo_url, 'rating' => $delivery->rider->rating, 'vehicle' => $delivery->rider->vehicle_type, ] : null, ]) ); Event::dispatch(new DeliveryStatusChanged($delivery)); } /** * Get current tracking data for a delivery */ public function getDeliveryTracking(Delivery $delivery): ?array { $trackingData = $this->firebase->getDeliveryTracking($delivery->id); if (! $trackingData) { return null; } // Enhance with interpolated positions for smooth animation if ($this->config['animation']['enabled'] && isset($trackingData['rider_location'])) { $trackingData['animation'] = $this->generateAnimationData($trackingData['rider_location']); } return $trackingData; } /** * End tracking for completed/cancelled delivery */ public function endDeliveryTracking(Delivery $delivery): void { // Store final tracking data in history $this->storeTrackingHistory($delivery); // Remove from Firebase after a delay (allow final status update to be seen) dispatch(function () use ($delivery) { $this->firebase->removeDeliveryTracking($delivery->id); })->delay(now()->addMinutes(5)); // Remove rider assignment if ($delivery->rider_id) { $this->firebase->removeRiderAssignment($delivery->rider_id, $delivery->id); } } /* |-------------------------------------------------------------------------- | Geofencing |-------------------------------------------------------------------------- */ /** * Check if rider has entered geofence zones */ protected function checkGeofenceTriggers( Delivery $delivery, float $latitude, float $longitude ): void { $geofenceRadius = config('restaurant-delivery.delivery_flow.auto_status_updates.geofence_radius'); // Check proximity to restaurant (for pickup) if ($delivery->status->value === 'rider_assigned') { $distanceToRestaurant = $this->calculateDistance( $latitude, $longitude, $delivery->pickup_latitude, $delivery->pickup_longitude ); if ($distanceToRestaurant * 1000 <= $geofenceRadius) { $this->triggerGeofenceEvent($delivery, 'restaurant_arrival'); } } // Check proximity to customer (for delivery) if ($delivery->status->value === 'on_the_way') { $distanceToCustomer = $this->calculateDistance( $latitude, $longitude, $delivery->drop_latitude, $delivery->drop_longitude ); if ($distanceToCustomer * 1000 <= $geofenceRadius) { $this->triggerGeofenceEvent($delivery, 'customer_arrival'); } } } /** * Handle geofence trigger event */ protected function triggerGeofenceEvent(Delivery $delivery, string $event): void { $requireConfirmation = config('restaurant-delivery.delivery_flow.auto_status_updates.arrival_confirmation'); if (! $requireConfirmation) { switch ($event) { case 'restaurant_arrival': $delivery->updateStatus('rider_at_restaurant'); break; case 'customer_arrival': $delivery->updateStatus('arrived'); break; } } else { // Send notification to rider to confirm arrival $this->notifyRiderForConfirmation($delivery, $event); } } /** * Notify rider to confirm arrival */ protected function notifyRiderForConfirmation(Delivery $delivery, string $event): void { $message = $event === 'restaurant_arrival' ? 'You have arrived at the restaurant. Please confirm pickup.' : 'You have arrived at the customer location. Please confirm delivery.'; $this->firebase->sendPushNotification( $delivery->rider->fcm_token, 'Arrival Detected', $message, [ 'type' => 'arrival_confirmation', 'delivery_id' => (string) $delivery->id, 'event' => $event, ] ); } /* |-------------------------------------------------------------------------- | Route Management |-------------------------------------------------------------------------- */ /** * Check if rider is off the expected route */ protected function isOffRoute(Delivery $delivery, float $latitude, float $longitude): bool { if (! config('restaurant-delivery.edge_cases.route_deviation.enabled')) { return false; } $threshold = config('restaurant-delivery.edge_cases.route_deviation.threshold'); $cachedRoute = Cache::get("delivery_route_{$delivery->id}"); if (! $cachedRoute || empty($cachedRoute['points'])) { return false; } // Find minimum distance to any point on the route $minDistance = PHP_FLOAT_MAX; foreach ($cachedRoute['points'] as $point) { $distance = $this->calculateDistance( $latitude, $longitude, $point['lat'], $point['lng'] ) * 1000; // Convert to meters $minDistance = min($minDistance, $distance); } return $minDistance > $threshold; } /** * Recalculate route from current position */ protected function recalculateRoute( Delivery $delivery, float $currentLat, float $currentLng ): void { $destLat = $delivery->status->isPickedUp() ? $delivery->drop_latitude : $delivery->pickup_latitude; $destLng = $delivery->status->isPickedUp() ? $delivery->drop_longitude : $delivery->pickup_longitude; $route = $this->mapsService->getRoute($currentLat, $currentLng, $destLat, $destLng); if ($route) { Cache::put("delivery_route_{$delivery->id}", [ 'polyline' => $route['polyline'], 'points' => $route['points'], 'distance' => $route['distance'], 'duration' => $route['duration'], ], config('restaurant-delivery.cache.ttl.route_calculation')); $this->firebase->updateDeliveryRoute($delivery->id, $route); } } /* |-------------------------------------------------------------------------- | Animation Support |-------------------------------------------------------------------------- */ /** * Generate interpolation data for smooth animation */ protected function generateAnimationData(array $currentLocation): array { $lastLocation = Cache::get("last_animation_point_{$currentLocation['delivery_id']}"); if (! $lastLocation) { Cache::put( "last_animation_point_{$currentLocation['delivery_id']}", $currentLocation, 60 ); return ['interpolated' => false]; } $points = $this->config['animation']['interpolation_points']; $duration = $this->config['animation']['animation_duration']; $interpolatedPoints = []; for ($i = 1; $i <= $points; $i++) { $ratio = $i / $points; $interpolatedPoints[] = [ 'lat' => $lastLocation['lat'] + ($currentLocation['lat'] - $lastLocation['lat']) * $ratio, 'lng' => $lastLocation['lng'] + ($currentLocation['lng'] - $lastLocation['lng']) * $ratio, 'bearing' => $this->interpolateBearing( $lastLocation['bearing'] ?? 0, $currentLocation['bearing'] ?? 0, $ratio ), 'timestamp' => $lastLocation['timestamp'] + ($duration * $ratio), ]; } Cache::put( "last_animation_point_{$currentLocation['delivery_id']}", $currentLocation, 60 ); return [ 'interpolated' => true, 'points' => $interpolatedPoints, 'duration' => $duration, 'easing' => $this->config['animation']['easing'], ]; } /** * Interpolate bearing between two angles */ protected function interpolateBearing(float $from, float $to, float $ratio): float { $diff = $to - $from; // Handle wrap-around at 360 degrees if ($diff > 180) { $diff -= 360; } elseif ($diff < -180) { $diff += 360; } return fmod($from + $diff * $ratio + 360, 360); } /* |-------------------------------------------------------------------------- | Utility Methods |-------------------------------------------------------------------------- */ /** * Calculate distance between two points using Haversine formula */ protected function calculateDistance( float $lat1, float $lng1, float $lat2, float $lng2 ): float { $earthRadius = config('restaurant-delivery.distance.earth_radius_km'); $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) * sin($dLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return $earthRadius * $c; } /** * Calculate ETA based on distance and speed */ protected function calculateETA(float $distanceKm, ?float $speedKmh): int { // Use provided speed or assume average $speed = $speedKmh && $speedKmh > 0 ? $speedKmh : 25; // Default 25 km/h $hours = $distanceKm / $speed; return (int) ceil($hours * 60); // Return minutes } /** * Store tracking history for analytics */ protected function storeTrackingHistory(Delivery $delivery): void { // This would store the complete tracking history for analytics // Implementation depends on your data storage strategy } }