migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\RestaurantDelivery\Services\Tracking;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\RestaurantDelivery\Enums\DeliveryStatus;
|
||||
use Modules\RestaurantDelivery\Events\DeliveryStatusChanged;
|
||||
use Modules\RestaurantDelivery\Events\RiderLocationUpdated;
|
||||
use Modules\RestaurantDelivery\Models\Delivery;
|
||||
use Modules\RestaurantDelivery\Models\LocationLog;
|
||||
use Modules\RestaurantDelivery\Models\Rider;
|
||||
use Modules\RestaurantDelivery\Services\Firebase\FirebaseService;
|
||||
use Modules\RestaurantDelivery\Services\Maps\MapsService;
|
||||
|
||||
class LiveTrackingService
|
||||
{
|
||||
protected FirebaseService $firebase;
|
||||
|
||||
protected MapsService $mapsService;
|
||||
|
||||
protected array $config;
|
||||
|
||||
public function __construct(
|
||||
FirebaseService $firebase,
|
||||
MapsService $mapsService
|
||||
) {
|
||||
$this->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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user