@fleetbase/fleetops-engine 0.6.47 → 0.6.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/admin/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +49 -5
- package/addon/components/orchestrator/phase-builder.js +34 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +4 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form.hbs +2 -2
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- package/addon/controllers/settings/routing.js +124 -1
- package/addon/extension.js +172 -3
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +806 -46
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +82 -4
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/fleetops.php +32 -11
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
- package/server/src/Http/Resources/v1/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/OrderConfig.php +14 -11
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Notifications/LateDeparture.php +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -234
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +27 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/MetricsRegistryTest.php +88 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +250 -0
- package/translations/en-us.yaml +41 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
10
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
11
|
+
|
|
12
|
+
class CalculatedTrackingProvider implements TrackingProviderInterface
|
|
13
|
+
{
|
|
14
|
+
public function key(): string
|
|
15
|
+
{
|
|
16
|
+
return 'calculated';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function capabilities(): TrackingProviderCapabilities
|
|
20
|
+
{
|
|
21
|
+
return new TrackingProviderCapabilities();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function canTrack(TrackingContext $context): bool
|
|
25
|
+
{
|
|
26
|
+
return $context->canRoute();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
|
|
30
|
+
{
|
|
31
|
+
$points = $context->routePoints();
|
|
32
|
+
$distance = 0;
|
|
33
|
+
$legs = [];
|
|
34
|
+
|
|
35
|
+
for ($i = 0; $i < count($points) - 1; $i++) {
|
|
36
|
+
$legDistance = Utils::vincentyGreatCircleDistance($points[$i], $points[$i + 1]);
|
|
37
|
+
$legDuration = $this->durationFromDistance($legDistance, $options);
|
|
38
|
+
$distance += $legDistance;
|
|
39
|
+
$legs[] = [
|
|
40
|
+
'index' => $i,
|
|
41
|
+
'distance_m' => $legDistance,
|
|
42
|
+
'duration_s' => $legDuration,
|
|
43
|
+
'duration_in_traffic_s' => null,
|
|
44
|
+
'provider' => $this->key(),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new TrackingProviderResult(
|
|
49
|
+
provider: $this->key(),
|
|
50
|
+
distanceMeters: $distance,
|
|
51
|
+
durationSeconds: $this->durationFromDistance($distance, $options),
|
|
52
|
+
durationInTrafficSeconds: null,
|
|
53
|
+
legs: $legs,
|
|
54
|
+
warnings: ['calculated_route_used'],
|
|
55
|
+
confidence: 'low'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected function durationFromDistance(float $distanceMeters, TrackingOptions $options): float
|
|
60
|
+
{
|
|
61
|
+
$metersPerSecond = max($options->defaultVehicleSpeedKph, 1) * 1000 / 3600;
|
|
62
|
+
|
|
63
|
+
return round($distanceMeters / $metersPerSecond);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
10
|
+
use Illuminate\Support\Facades\Http;
|
|
11
|
+
use Illuminate\Support\Str;
|
|
12
|
+
|
|
13
|
+
class GoogleRoutesTrackingProvider implements TrackingProviderInterface
|
|
14
|
+
{
|
|
15
|
+
public function key(): string
|
|
16
|
+
{
|
|
17
|
+
return 'google_routes';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public function capabilities(): TrackingProviderCapabilities
|
|
21
|
+
{
|
|
22
|
+
return new TrackingProviderCapabilities(traffic: true, perLegEta: true, routeGeometry: true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function canTrack(TrackingContext $context): bool
|
|
26
|
+
{
|
|
27
|
+
return $context->canRoute() && filled($this->apiKey());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
|
|
31
|
+
{
|
|
32
|
+
$points = $context->routePoints();
|
|
33
|
+
$origin = array_shift($points);
|
|
34
|
+
$destination = array_pop($points);
|
|
35
|
+
$body = [
|
|
36
|
+
'origin' => ['location' => ['latLng' => $this->latLng($origin)]],
|
|
37
|
+
'destination' => ['location' => ['latLng' => $this->latLng($destination)]],
|
|
38
|
+
'travelMode' => 'DRIVE',
|
|
39
|
+
'routingPreference' => $options->trafficEnabled ? 'TRAFFIC_AWARE_OPTIMAL' : 'TRAFFIC_UNAWARE',
|
|
40
|
+
'computeAlternativeRoutes' => false,
|
|
41
|
+
'languageCode' => 'en-US',
|
|
42
|
+
'units' => 'METRIC',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (!empty($points)) {
|
|
46
|
+
$body['intermediates'] = array_map(fn ($point) => ['location' => ['latLng' => $this->latLng($point)]], $points);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$response = Http::timeout(5)
|
|
50
|
+
->withHeaders([
|
|
51
|
+
'X-Goog-Api-Key' => $this->apiKey(),
|
|
52
|
+
'X-Goog-FieldMask' => 'routes.distanceMeters,routes.duration,routes.staticDuration,routes.polyline.encodedPolyline,routes.legs.distanceMeters,routes.legs.duration,routes.legs.staticDuration',
|
|
53
|
+
])
|
|
54
|
+
->post('https://routes.googleapis.com/directions/v2:computeRoutes', $body);
|
|
55
|
+
|
|
56
|
+
if (!$response->successful()) {
|
|
57
|
+
throw new \RuntimeException('Google Routes request failed with status ' . $response->status());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$route = data_get($response->json(), 'routes.0');
|
|
61
|
+
if (!$route) {
|
|
62
|
+
throw new \RuntimeException('Google Routes returned no route.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$duration = $this->durationToSeconds(data_get($route, 'staticDuration')) ?? $this->durationToSeconds(data_get($route, 'duration'));
|
|
66
|
+
$trafficDuration = $this->durationToSeconds(data_get($route, 'duration'));
|
|
67
|
+
|
|
68
|
+
return new TrackingProviderResult(
|
|
69
|
+
provider: $this->key(),
|
|
70
|
+
distanceMeters: data_get($route, 'distanceMeters'),
|
|
71
|
+
durationSeconds: $duration,
|
|
72
|
+
durationInTrafficSeconds: $options->trafficEnabled ? $trafficDuration : null,
|
|
73
|
+
polyline: data_get($route, 'polyline.encodedPolyline'),
|
|
74
|
+
legs: $this->legs(data_get($route, 'legs', []), $options),
|
|
75
|
+
warnings: [],
|
|
76
|
+
confidence: $options->trafficEnabled ? 'high' : 'medium',
|
|
77
|
+
raw: $route
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected function legs(array $legs, TrackingOptions $options): array
|
|
82
|
+
{
|
|
83
|
+
return collect($legs)->map(function ($leg, $index) use ($options) {
|
|
84
|
+
$duration = $this->durationToSeconds(data_get($leg, 'staticDuration')) ?? $this->durationToSeconds(data_get($leg, 'duration'));
|
|
85
|
+
$trafficDuration = $this->durationToSeconds(data_get($leg, 'duration'));
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
'index' => $index,
|
|
89
|
+
'distance_m' => data_get($leg, 'distanceMeters'),
|
|
90
|
+
'duration_s' => $duration,
|
|
91
|
+
'duration_in_traffic_s' => $options->trafficEnabled ? $trafficDuration : null,
|
|
92
|
+
'provider' => $this->key(),
|
|
93
|
+
];
|
|
94
|
+
})->all();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected function latLng($point): array
|
|
98
|
+
{
|
|
99
|
+
return [
|
|
100
|
+
'latitude' => $point->getLat(),
|
|
101
|
+
'longitude' => $point->getLng(),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected function durationToSeconds(?string $duration): ?float
|
|
106
|
+
{
|
|
107
|
+
if (!$duration) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (float) Str::replaceLast('s', '', $duration);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected function apiKey(): ?string
|
|
115
|
+
{
|
|
116
|
+
return config('services.google_maps.api_key') ?: env('GOOGLE_MAPS_API_KEY');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\OSRM;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
10
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
11
|
+
use Illuminate\Support\Arr;
|
|
12
|
+
|
|
13
|
+
class OsrmTrackingProvider implements TrackingProviderInterface
|
|
14
|
+
{
|
|
15
|
+
public function key(): string
|
|
16
|
+
{
|
|
17
|
+
return 'osrm';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public function capabilities(): TrackingProviderCapabilities
|
|
21
|
+
{
|
|
22
|
+
return new TrackingProviderCapabilities(perLegEta: true, routeGeometry: true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function canTrack(TrackingContext $context): bool
|
|
26
|
+
{
|
|
27
|
+
return $context->canRoute();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
|
|
31
|
+
{
|
|
32
|
+
$response = OSRM::getRouteFromPoints($context->routePoints(), [
|
|
33
|
+
'overview' => 'full',
|
|
34
|
+
'geometries' => 'polyline',
|
|
35
|
+
'steps' => 'false',
|
|
36
|
+
'annotations' => 'false',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
if (data_get($response, 'code') !== 'Ok') {
|
|
40
|
+
throw new \RuntimeException('OSRM did not return a routable response.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$route = Arr::first(data_get($response, 'routes', []));
|
|
44
|
+
if (!$route) {
|
|
45
|
+
throw new \RuntimeException('OSRM returned no route.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$legs = collect(data_get($route, 'legs', []))->map(function ($leg, $index) {
|
|
49
|
+
return [
|
|
50
|
+
'index' => $index,
|
|
51
|
+
'distance_m' => data_get($leg, 'distance'),
|
|
52
|
+
'duration_s' => data_get($leg, 'duration'),
|
|
53
|
+
'duration_in_traffic_s' => null,
|
|
54
|
+
'provider' => $this->key(),
|
|
55
|
+
];
|
|
56
|
+
})->all();
|
|
57
|
+
|
|
58
|
+
return new TrackingProviderResult(
|
|
59
|
+
provider: $this->key(),
|
|
60
|
+
distanceMeters: data_get($route, 'distance'),
|
|
61
|
+
durationSeconds: data_get($route, 'duration'),
|
|
62
|
+
durationInTrafficSeconds: null,
|
|
63
|
+
polyline: data_get($route, 'geometry'),
|
|
64
|
+
coordinates: data_get($route, 'waypoints', []),
|
|
65
|
+
legs: $legs,
|
|
66
|
+
warnings: ['no_live_traffic'],
|
|
67
|
+
confidence: 'medium',
|
|
68
|
+
raw: $route
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Order Tracking Providers
|
|
2
|
+
|
|
3
|
+
FleetOps tracking providers adapt third-party routing or tracking systems into the canonical tracking intelligence response.
|
|
4
|
+
|
|
5
|
+
Providers must implement `Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface`:
|
|
6
|
+
|
|
7
|
+
```php
|
|
8
|
+
public function key(): string;
|
|
9
|
+
public function capabilities(): TrackingProviderCapabilities;
|
|
10
|
+
public function canTrack(TrackingContext $context): bool;
|
|
11
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Register providers from a service provider:
|
|
15
|
+
|
|
16
|
+
```php
|
|
17
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
|
|
18
|
+
|
|
19
|
+
public function boot(): void
|
|
20
|
+
{
|
|
21
|
+
app(TrackingProviderRegistry::class)->register(new TomTomTrackingProvider());
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Provider adapters should never expose vendor response shapes directly. Map provider responses into `TrackingProviderResult` and declare capabilities such as traffic-aware ETA, per-leg ETA, route geometry, or map matching.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\Providers\CalculatedTrackingProvider;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
10
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
11
|
+
|
|
12
|
+
class FakeTrackingProvider implements TrackingProviderInterface
|
|
13
|
+
{
|
|
14
|
+
public function __construct(protected string $providerKey = 'fake')
|
|
15
|
+
{
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public function key(): string
|
|
19
|
+
{
|
|
20
|
+
return $this->providerKey;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public function capabilities(): TrackingProviderCapabilities
|
|
24
|
+
{
|
|
25
|
+
return new TrackingProviderCapabilities(traffic: true, perLegEta: true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public function canTrack(TrackingContext $context): bool
|
|
29
|
+
{
|
|
30
|
+
return $context->canRoute();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
|
|
34
|
+
{
|
|
35
|
+
$result = (new CalculatedTrackingProvider())->track($context, $options);
|
|
36
|
+
$result->provider = $this->providerKey;
|
|
37
|
+
$result->confidence = 'high';
|
|
38
|
+
$result->warnings = [];
|
|
39
|
+
|
|
40
|
+
return $result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Payload;
|
|
8
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
9
|
+
use Illuminate\Support\Collection;
|
|
10
|
+
|
|
11
|
+
class TrackingContext
|
|
12
|
+
{
|
|
13
|
+
public function __construct(
|
|
14
|
+
public Order $order,
|
|
15
|
+
public ?Payload $payload,
|
|
16
|
+
public ?Driver $driver,
|
|
17
|
+
public ?Point $origin,
|
|
18
|
+
public ?Point $driverLocation,
|
|
19
|
+
public Collection $stops,
|
|
20
|
+
public Collection $completedStops,
|
|
21
|
+
public Collection $remainingStops,
|
|
22
|
+
public ?TrackingStop $activeStop,
|
|
23
|
+
public ?TrackingStop $nextStop,
|
|
24
|
+
public ?int $driverLocationAgeSeconds,
|
|
25
|
+
public array $warnings = [],
|
|
26
|
+
) {
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function routePoints(): array
|
|
30
|
+
{
|
|
31
|
+
$points = [];
|
|
32
|
+
if ($this->origin) {
|
|
33
|
+
$points[] = $this->origin;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
foreach ($this->remainingStops as $stop) {
|
|
37
|
+
$point = $stop instanceof TrackingStop ? $stop->point() : null;
|
|
38
|
+
if ($point) {
|
|
39
|
+
$points[] = $point;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return $points;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public function canRoute(): bool
|
|
47
|
+
{
|
|
48
|
+
return count($this->routePoints()) >= 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public function stateSignature(): string
|
|
52
|
+
{
|
|
53
|
+
return md5(json_encode($this->stops->map(function (TrackingStop $stop) {
|
|
54
|
+
return [
|
|
55
|
+
'uuid' => $stop->uuid,
|
|
56
|
+
'type' => $stop->type,
|
|
57
|
+
'status' => $stop->status,
|
|
58
|
+
'completed' => $stop->completed,
|
|
59
|
+
'sequence' => $stop->sequence,
|
|
60
|
+
];
|
|
61
|
+
})->values()->all()));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Waypoint;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
9
|
+
use Illuminate\Support\Collection;
|
|
10
|
+
use Illuminate\Support\Str;
|
|
11
|
+
|
|
12
|
+
class TrackingContextBuilder
|
|
13
|
+
{
|
|
14
|
+
public function build(Order $order, TrackingOptions $options): TrackingContext
|
|
15
|
+
{
|
|
16
|
+
$order->loadAssignedDriver();
|
|
17
|
+
$order->loadMissing([
|
|
18
|
+
'payload',
|
|
19
|
+
'payload.pickup',
|
|
20
|
+
'payload.dropoff',
|
|
21
|
+
'payload.waypoints',
|
|
22
|
+
'payload.waypointMarkers.place',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
$payload = $order->payload;
|
|
26
|
+
$driver = $order->driverAssigned;
|
|
27
|
+
$warnings = [];
|
|
28
|
+
$driverLocationAge = $driver?->updated_at ? now()->diffInSeconds($driver->updated_at) : null;
|
|
29
|
+
|
|
30
|
+
$origin = null;
|
|
31
|
+
$driverLocation = null;
|
|
32
|
+
if ($driver && $driver->location) {
|
|
33
|
+
try {
|
|
34
|
+
$driverLocation = Utils::getPointFromMixed($driver);
|
|
35
|
+
if (!$this->isValidPoint($driverLocation)) {
|
|
36
|
+
$driverLocation = null;
|
|
37
|
+
}
|
|
38
|
+
} catch (\Throwable) {
|
|
39
|
+
$driverLocation = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ($driverLocation) {
|
|
44
|
+
$origin = $driverLocation;
|
|
45
|
+
} else {
|
|
46
|
+
$warnings[] = 'missing_driver_location';
|
|
47
|
+
$origin = $this->fallbackOrigin($payload);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if ($driverLocationAge !== null && $driverLocationAge > $options->staleLocationThresholdSeconds) {
|
|
51
|
+
$warnings[] = 'stale_driver_location';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$stops = $this->stops($payload, $order);
|
|
55
|
+
$completedStops = $stops->filter(fn (TrackingStop $stop) => $stop->completed)->values();
|
|
56
|
+
$remainingStops = $stops->reject(fn (TrackingStop $stop) => $stop->completed)->values();
|
|
57
|
+
$activeStop = $this->activeStop($payload, $remainingStops);
|
|
58
|
+
$nextStop = $remainingStops->first(fn (TrackingStop $stop) => !$activeStop || $stop->uuid !== $activeStop->uuid);
|
|
59
|
+
|
|
60
|
+
if (!$activeStop && $remainingStops->isNotEmpty()) {
|
|
61
|
+
$activeStop = $remainingStops->first();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$missingStopLocations = $remainingStops->filter(fn (TrackingStop $stop) => !$stop->point())->count();
|
|
65
|
+
if ($missingStopLocations > 0) {
|
|
66
|
+
$warnings[] = 'missing_stop_location';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new TrackingContext(
|
|
70
|
+
order: $order,
|
|
71
|
+
payload: $payload,
|
|
72
|
+
driver: $driver,
|
|
73
|
+
origin: $origin,
|
|
74
|
+
driverLocation: $driverLocation,
|
|
75
|
+
stops: $stops,
|
|
76
|
+
completedStops: $completedStops,
|
|
77
|
+
remainingStops: $remainingStops,
|
|
78
|
+
activeStop: $activeStop,
|
|
79
|
+
nextStop: $nextStop,
|
|
80
|
+
driverLocationAgeSeconds: $driverLocationAge,
|
|
81
|
+
warnings: array_values(array_unique($warnings))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected function stops($payload, Order $order): Collection
|
|
86
|
+
{
|
|
87
|
+
if (!$payload) {
|
|
88
|
+
return collect();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$stops = collect();
|
|
92
|
+
$sequence = 0;
|
|
93
|
+
|
|
94
|
+
if ($payload->pickup instanceof Place) {
|
|
95
|
+
$stops->push(new TrackingStop(
|
|
96
|
+
uuid: $payload->pickup->uuid,
|
|
97
|
+
publicId: $payload->pickup->public_id,
|
|
98
|
+
type: 'pickup',
|
|
99
|
+
status: null,
|
|
100
|
+
place: $payload->pickup,
|
|
101
|
+
completed: !in_array($order->status, ['created', 'dispatched', 'pending']),
|
|
102
|
+
sequence: ++$sequence
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$markers = $payload->waypointMarkers instanceof Collection ? $payload->waypointMarkers : collect();
|
|
107
|
+
if ($markers->isNotEmpty()) {
|
|
108
|
+
foreach ($markers->sortBy('order')->values() as $waypoint) {
|
|
109
|
+
$stops->push($this->stopFromWaypoint($waypoint, ++$sequence));
|
|
110
|
+
}
|
|
111
|
+
} elseif ($payload->waypoints instanceof Collection) {
|
|
112
|
+
foreach ($payload->waypoints as $place) {
|
|
113
|
+
if ($place instanceof Place) {
|
|
114
|
+
$stops->push(new TrackingStop(
|
|
115
|
+
uuid: $place->uuid,
|
|
116
|
+
publicId: $place->public_id,
|
|
117
|
+
type: 'waypoint',
|
|
118
|
+
status: null,
|
|
119
|
+
place: $place,
|
|
120
|
+
completed: false,
|
|
121
|
+
sequence: ++$sequence
|
|
122
|
+
));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if ($payload->dropoff instanceof Place) {
|
|
128
|
+
$stops->push(new TrackingStop(
|
|
129
|
+
uuid: $payload->dropoff->uuid,
|
|
130
|
+
publicId: $payload->dropoff->public_id,
|
|
131
|
+
type: 'dropoff',
|
|
132
|
+
status: null,
|
|
133
|
+
place: $payload->dropoff,
|
|
134
|
+
completed: in_array($order->status, ['completed', 'canceled']),
|
|
135
|
+
sequence: ++$sequence
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return $stops->values();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected function stopFromWaypoint(Waypoint $waypoint, int $sequence): TrackingStop
|
|
143
|
+
{
|
|
144
|
+
$waypoint->loadMissing('place');
|
|
145
|
+
$status = strtolower((string) $waypoint->status_code);
|
|
146
|
+
|
|
147
|
+
return new TrackingStop(
|
|
148
|
+
uuid: $waypoint->place_uuid,
|
|
149
|
+
publicId: data_get($waypoint, 'place.public_id'),
|
|
150
|
+
type: 'waypoint',
|
|
151
|
+
status: $status ?: null,
|
|
152
|
+
place: $waypoint->place,
|
|
153
|
+
waypoint: $waypoint,
|
|
154
|
+
completed: in_array($status, ['completed', 'canceled']),
|
|
155
|
+
sequence: $sequence
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
protected function activeStop($payload, Collection $remainingStops): ?TrackingStop
|
|
160
|
+
{
|
|
161
|
+
if (!$payload || !$payload->current_waypoint_uuid || !Str::isUuid($payload->current_waypoint_uuid)) {
|
|
162
|
+
return $remainingStops->first();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return $remainingStops->first(function (TrackingStop $stop) use ($payload) {
|
|
166
|
+
return $stop->uuid === $payload->current_waypoint_uuid
|
|
167
|
+
|| data_get($stop->waypoint, 'uuid') === $payload->current_waypoint_uuid
|
|
168
|
+
|| data_get($stop->waypoint, 'place_uuid') === $payload->current_waypoint_uuid;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected function fallbackOrigin($payload)
|
|
173
|
+
{
|
|
174
|
+
if (!$payload) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
$point = Utils::getPointFromMixed($payload->getPickupOrCurrentWaypoint());
|
|
180
|
+
|
|
181
|
+
return $this->isValidPoint($point) ? $point : null;
|
|
182
|
+
} catch (\Throwable) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected function isValidPoint($point): bool
|
|
188
|
+
{
|
|
189
|
+
if (!$point || !method_exists($point, 'getLat') || !method_exists($point, 'getLng')) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
$lat = (float) $point->getLat();
|
|
194
|
+
$lng = (float) $point->getLng();
|
|
195
|
+
|
|
196
|
+
return is_finite($lat)
|
|
197
|
+
&& is_finite($lng)
|
|
198
|
+
&& $lat >= -90
|
|
199
|
+
&& $lat <= 90
|
|
200
|
+
&& $lng >= -180
|
|
201
|
+
&& $lng <= 180
|
|
202
|
+
&& !(abs($lat) < 0.000001 && abs($lng) < 0.000001);
|
|
203
|
+
}
|
|
204
|
+
}
|