@fleetbase/fleetops-engine 0.6.45 → 0.6.47
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/map-settings.hbs +26 -0
- package/addon/components/admin/map-settings.js +46 -0
- package/addon/components/admin/routing-settings.hbs +11 -0
- package/addon/components/cell/order-route-type.hbs +46 -0
- package/addon/components/cell/order-route-type.js +90 -0
- package/addon/components/cell/place-address.hbs +1 -0
- package/addon/components/display-place.hbs +45 -56
- package/addon/components/display-place.js +21 -0
- package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/maintenance-schedule/form.js +2 -1
- package/addon/components/map/drawer/driver-listing.js +5 -5
- package/addon/components/map/drawer/place-listing.js +5 -5
- package/addon/components/map/drawer/position-listing.js +102 -21
- package/addon/components/map/drawer/vehicle-listing.js +5 -5
- package/addon/components/map/google-live-map.hbs +9 -0
- package/addon/components/map/google-live-map.js +445 -0
- package/addon/components/map/leaflet-live-map.hbs +186 -142
- package/addon/components/map/leaflet-live-map.js +121 -41
- package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
- package/addon/components/map/toolbar/zones-panel.hbs +9 -9
- package/addon/components/map/toolbar.hbs +3 -3
- package/addon/components/map/toolbar.js +9 -1
- package/addon/components/modals/map-layer-form.hbs +132 -63
- package/addon/components/modals/orchestrator-import.hbs +4 -11
- package/addon/components/modals/orchestrator-import.js +17 -1
- package/addon/components/modals/update-order-activity.hbs +1 -1
- package/addon/components/orchestrator/order-pool.js +3 -2
- package/addon/components/orchestrator-workbench.js +29 -41
- package/addon/components/order/details/detail.js +2 -2
- package/addon/components/order/details/payload.hbs +5 -5
- package/addon/components/order/details/payload.js +18 -1
- package/addon/components/order/details/route.hbs +2 -2
- package/addon/components/order/details/route.js +76 -3
- package/addon/components/order/form/details.hbs +1 -2
- package/addon/components/order/form/details.js +6 -0
- package/addon/components/order/form/payload.hbs +33 -0
- package/addon/components/order/form/payload.js +1 -0
- package/addon/components/order/form/route.hbs +8 -8
- package/addon/components/order/form/route.js +86 -18
- package/addon/components/order/form.js +2 -2
- package/addon/components/order/route-editor.hbs +209 -147
- package/addon/components/order/route-editor.js +143 -102
- package/addon/components/place/address.hbs +1 -0
- package/addon/components/place/address.js +12 -0
- package/addon/components/route-list.hbs +43 -55
- package/addon/components/route-list.js +47 -0
- package/addon/components/route-optimization-wizard-panel.js +3 -4
- package/addon/components/vehicle/card.hbs +4 -4
- package/addon/controllers/maintenance/schedules/index.js +2 -1
- package/addon/controllers/operations/orders/index/details.js +110 -37
- package/addon/controllers/operations/orders/index/new.js +1 -1
- package/addon/controllers/operations/orders/index.js +8 -1
- package/addon/controllers/settings/map.js +97 -0
- package/addon/controllers/settings/payments/index.js +1 -1
- package/addon/controllers/settings/routing.js +47 -12
- package/addon/extension.js +11 -0
- package/addon/helpers/place-address-html.js +6 -0
- package/addon/instance-initializers/register-google-routes.js +17 -0
- package/addon/instance-initializers/register-osrm.js +5 -0
- package/addon/routes/application.js +6 -0
- package/addon/routes/operations/orchestrator.js +13 -0
- package/addon/routes/operations/orders/index/details.js +29 -22
- package/addon/routes/operations/orders/index/new.js +9 -0
- package/addon/routes/operations/orders/index.js +0 -1
- package/addon/routes/settings/map.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/entity-actions.js +1 -1
- package/addon/services/geofence-event-bus.js +2 -1
- package/addon/services/geofence.js +204 -168
- package/addon/services/google-routes.js +300 -0
- package/addon/services/leaflet-draw-restriction.js +2 -1
- package/addon/services/leaflet-map-manager.js +17 -0
- package/addon/services/map-adapter/google.js +2205 -0
- package/addon/services/map-adapter/leaflet.js +1042 -0
- package/addon/services/map-adapter-interface.js +551 -0
- package/addon/services/map-manager.js +768 -0
- package/addon/services/map-settings.js +75 -0
- package/addon/services/movement-tracker.js +83 -32
- package/addon/services/order-actions.js +11 -1
- package/addon/services/osrm.js +79 -6
- package/addon/services/position-playback.js +135 -62
- package/addon/services/route-engine.js +83 -0
- package/addon/services/service-areas.js +8 -1
- package/addon/styles/fleetops-engine.css +555 -0
- package/addon/templates/management/vehicles/index.hbs +0 -1
- package/addon/templates/operations/orders/index/details.hbs +0 -1
- package/addon/templates/settings/map.hbs +53 -0
- package/addon/templates/settings/routing.hbs +15 -4
- package/addon/utils/fleet-ops-options.js +1 -0
- package/addon/utils/has-valid-coordinates.js +6 -0
- package/addon/utils/map-provider-options.js +6 -0
- package/addon/utils/order-route-editing.js +86 -0
- package/addon/utils/order-route-summary.js +20 -0
- package/addon/utils/place-address-html.js +41 -0
- package/addon/utils/route-colors.js +43 -12
- package/addon/utils/route-visualization.js +93 -0
- package/addon/utils/vendor-integration.js +2 -1
- package/app/components/admin/map-settings.js +1 -0
- package/app/components/admin/routing-settings.js +1 -0
- package/app/components/cell/order-route-type.js +1 -0
- package/app/components/cell/place-address.js +1 -0
- package/app/components/map/google-live-map.js +1 -0
- package/app/components/place/address.js +1 -0
- package/app/controllers/settings/map.js +1 -0
- package/app/helpers/place-address-html.js +1 -0
- package/app/routes/settings/map.js +1 -0
- package/app/services/map-adapter/google.js +1 -0
- package/app/services/map-adapter/leaflet.js +1 -0
- package/app/services/map-adapter-interface.js +1 -0
- package/app/services/map-manager.js +1 -0
- package/app/services/map-settings.js +1 -0
- package/app/templates/settings/map.js +1 -0
- package/app/utils/has-valid-coordinates.js +1 -0
- package/app/utils/order-route-editing.js +2 -0
- package/app/utils/order-route-summary.js +1 -0
- package/app/utils/place-address-html.js +1 -0
- package/app/utils/route-colors.js +1 -1
- package/app/utils/route-visualization.js +1 -0
- package/assets/images/drawing-controls-spritesheet.svg +156 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/pnpm-workspace.yaml +7 -0
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +90 -34
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
- package/server/src/Http/Filter/OrderFilter.php +2 -1
- package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
- package/server/src/Http/Resources/v1/Order.php +20 -1
- package/server/src/Models/Order.php +42 -1
- package/server/src/Models/OrderConfig.php +15 -0
- package/server/src/Models/Payload.php +131 -27
- package/server/src/Models/Place.php +182 -48
- package/server/src/Models/ServiceRate.php +4 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
- package/server/src/Support/Utils.php +36 -2
- package/server/src/routes.php +4 -0
- package/translations/en-us.yaml +1937 -1916
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Service from '@ember/service';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SETTINGS = {
|
|
6
|
+
mapProvider: 'leaflet',
|
|
7
|
+
googleMapsApiKey: '',
|
|
8
|
+
googleMapsMapId: '',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default class MapSettingsService extends Service {
|
|
12
|
+
@service fetch;
|
|
13
|
+
@tracked settings = { ...DEFAULT_SETTINGS };
|
|
14
|
+
@tracked isLoaded = false;
|
|
15
|
+
@tracked loadPromise = null;
|
|
16
|
+
|
|
17
|
+
get mapProvider() {
|
|
18
|
+
return this.settings.mapProvider ?? 'leaflet';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get googleMapsApiKey() {
|
|
22
|
+
return this.settings.googleMapsApiKey ?? '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get googleMapsMapId() {
|
|
26
|
+
return this.settings.googleMapsMapId ?? '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get isGoogleMaps() {
|
|
30
|
+
return this.mapProvider === 'google';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async load({ force = false } = {}) {
|
|
34
|
+
if (!force && this.isLoaded) {
|
|
35
|
+
return this.settings;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!force && this.loadPromise) {
|
|
39
|
+
return this.loadPromise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.loadPromise = this.fetch
|
|
43
|
+
.get('fleet-ops/settings/map')
|
|
44
|
+
.then((settings) => this.applySettings(settings))
|
|
45
|
+
.catch(() => {
|
|
46
|
+
this.settings = { ...DEFAULT_SETTINGS };
|
|
47
|
+
this.isLoaded = true;
|
|
48
|
+
return this.settings;
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
this.loadPromise = null;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return this.loadPromise;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
applySettings(settings = {}) {
|
|
58
|
+
this.settings = {
|
|
59
|
+
...DEFAULT_SETTINGS,
|
|
60
|
+
...settings,
|
|
61
|
+
};
|
|
62
|
+
this.isLoaded = true;
|
|
63
|
+
return this.settings;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setMapProvider(mapProvider = 'leaflet') {
|
|
67
|
+
this.settings = {
|
|
68
|
+
...this.settings,
|
|
69
|
+
mapProvider,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.isLoaded = true;
|
|
73
|
+
return this.settings;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MovementTrackerService
|
|
3
|
+
*
|
|
4
|
+
* Handles real-time marker movement via SocketCluster events.
|
|
5
|
+
* Refactored to use the provider-agnostic MapManagerService instead of
|
|
6
|
+
* accessing Leaflet marker objects directly.
|
|
7
|
+
*
|
|
8
|
+
* The EventBuffer is preserved, but marker manipulation now goes through
|
|
9
|
+
* `mapManager.updateMarkerPosition()` and `mapManager.setMarkerRotation()`
|
|
10
|
+
* so it works with any map provider.
|
|
11
|
+
*
|
|
12
|
+
* Backward-compatible: the `model.leafletLayer` path is still tried as
|
|
13
|
+
* a last resort so that existing code continues to work during migration.
|
|
14
|
+
*/
|
|
1
15
|
import Service from '@ember/service';
|
|
2
16
|
import { tracked } from '@glimmer/tracking';
|
|
3
17
|
import { inject as service } from '@ember/service';
|
|
@@ -14,10 +28,41 @@ export class EventBuffer {
|
|
|
14
28
|
@tracked intervalId;
|
|
15
29
|
@tracked model;
|
|
16
30
|
|
|
17
|
-
|
|
31
|
+
/** @type {import('./map-manager').default|null} */
|
|
32
|
+
mapManager = null;
|
|
33
|
+
|
|
34
|
+
constructor(model, { callback = null, waitTime = 1000 * 3, mapManager = null }) {
|
|
18
35
|
this.model = model;
|
|
19
36
|
this.callback = callback;
|
|
20
37
|
this.waitTime = waitTime;
|
|
38
|
+
this.mapManager = mapManager;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the marker id for the tracked model.
|
|
43
|
+
* @returns {string|null}
|
|
44
|
+
*/
|
|
45
|
+
#getMarkerId() {
|
|
46
|
+
return this.model?.id ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Calculate distance in metres between two positions.
|
|
51
|
+
* Uses the adapter when available, falls back to Haversine.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ lat: number, lng: number }} prev
|
|
54
|
+
* @param {number[]} nextLatLng - [lat, lng]
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
#calcDistance(prev, nextLatLng) {
|
|
58
|
+
if (this.mapManager) {
|
|
59
|
+
return this.mapManager.distanceBetween(prev.lat, prev.lng, nextLatLng[0], nextLatLng[1]);
|
|
60
|
+
}
|
|
61
|
+
const R = 6371000;
|
|
62
|
+
const dLat = ((nextLatLng[0] - prev.lat) * Math.PI) / 180;
|
|
63
|
+
const dLng = ((nextLatLng[1] - prev.lng) * Math.PI) / 180;
|
|
64
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos((prev.lat * Math.PI) / 180) * Math.cos((nextLatLng[0] * Math.PI) / 180) * Math.sin(dLng / 2) ** 2;
|
|
65
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
21
66
|
}
|
|
22
67
|
|
|
23
68
|
start() {
|
|
@@ -52,70 +97,74 @@ export class EventBuffer {
|
|
|
52
97
|
@task *process() {
|
|
53
98
|
debug('Processing movement tracker event buffer.');
|
|
54
99
|
|
|
55
|
-
// Take a snapshot of events to process and clear buffer immediately
|
|
56
|
-
// This prevents losing events that arrive during processing
|
|
57
100
|
const eventsToProcess = [...this.events];
|
|
58
|
-
this.events = [];
|
|
101
|
+
this.events = [];
|
|
59
102
|
|
|
60
|
-
// Sort events by created_at
|
|
61
103
|
eventsToProcess.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
62
104
|
debug(`[MovementTracker EventBuffer processing ${eventsToProcess.length} events]`);
|
|
63
105
|
|
|
64
|
-
// Process sorted events
|
|
65
106
|
for (const output of eventsToProcess) {
|
|
66
107
|
const { event, data } = output;
|
|
67
108
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
109
|
+
// Resolve marker via adapter (provider-agnostic) or Leaflet fallback
|
|
110
|
+
const markerId = this.#getMarkerId();
|
|
111
|
+
const hasAdapterMarker = markerId && this.mapManager?.hasMarker(markerId);
|
|
112
|
+
const leafletMarker = !hasAdapterMarker ? this.model?.leafletLayer || this.model?._layer || this.model?._marker : null;
|
|
113
|
+
|
|
114
|
+
if (!hasAdapterMarker && (!leafletMarker || !leafletMarker._map)) {
|
|
71
115
|
debug('No marker or marker not on map yet');
|
|
72
116
|
continue;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
// log incoming event
|
|
76
119
|
debug(`${event} - ${data.id} ${data.additionalData?.index ? '#' + data.additionalData?.index : ''} (${output.created_at}) [ ${data.location.coordinates.join(' ')} ]`);
|
|
77
120
|
|
|
78
|
-
// GeoJSON ->
|
|
121
|
+
// GeoJSON -> [lat, lng]
|
|
79
122
|
const [lng, lat] = data.location.coordinates;
|
|
80
123
|
const nextLatLng = [lat, lng];
|
|
81
124
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
// Calculate distance for animation duration
|
|
126
|
+
let meters = 0;
|
|
127
|
+
if (hasAdapterMarker) {
|
|
128
|
+
const adapterCenter = this.mapManager.getCenter?.();
|
|
129
|
+
if (adapterCenter) meters = this.#calcDistance(adapterCenter, nextLatLng);
|
|
130
|
+
} else if (leafletMarker) {
|
|
131
|
+
const map = leafletMarker._map;
|
|
132
|
+
const prev = leafletMarker.getLatLng();
|
|
133
|
+
meters = map ? map.distance(prev, nextLatLng) : prev.distanceTo(nextLatLng);
|
|
134
|
+
}
|
|
86
135
|
|
|
87
|
-
// Assume payload speed is m/s; if it's km/h, convert: mps = kmh / 3.6
|
|
88
136
|
let mps = Number.isFinite(data.speed) && data.speed > 0 ? data.speed : null;
|
|
89
|
-
|
|
90
|
-
// Reduce animation duration and clamp between 100ms and 500ms
|
|
91
|
-
// This makes animations faster and prevents long delays
|
|
92
137
|
const durationMs = mps ? Math.max(100, Math.min((meters / mps) * 1000, 500)) : 500;
|
|
93
138
|
|
|
94
139
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
if (hasAdapterMarker) {
|
|
141
|
+
// ── Provider-agnostic path ─────────────────────────────
|
|
142
|
+
if (Number.isFinite(data.heading) && data.heading !== -1) {
|
|
143
|
+
this.mapManager.setMarkerRotation(markerId, data.heading);
|
|
144
|
+
}
|
|
145
|
+
this.mapManager.updateMarkerPosition(markerId, lat, lng, true, durationMs);
|
|
146
|
+
} else if (leafletMarker) {
|
|
147
|
+
// ── Leaflet backward-compat path ───────────────────────
|
|
148
|
+
if (typeof leafletMarker.setRotationAngle === 'function' && Number.isFinite(data.heading) && data.heading !== -1) {
|
|
149
|
+
leafletMarker.setRotationAngle(data.heading);
|
|
150
|
+
}
|
|
151
|
+
if (typeof leafletMarker.slideTo === 'function') {
|
|
152
|
+
leafletMarker.slideTo(nextLatLng, { duration: durationMs });
|
|
153
|
+
} else {
|
|
154
|
+
leafletMarker.setLatLng(nextLatLng);
|
|
155
|
+
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
158
|
if (typeof this.callback === 'function') {
|
|
108
159
|
this.callback(output, { nextLatLng, duration: durationMs, mps });
|
|
109
160
|
}
|
|
110
161
|
|
|
111
|
-
// Wait for animation to complete
|
|
112
162
|
yield timeout(durationMs + 50);
|
|
113
163
|
} catch (err) {
|
|
114
164
|
debug('MovementTracker EventBuffer error: ' + err.message);
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
167
|
|
|
118
|
-
// Don't clear here - we already cleared at the start
|
|
119
168
|
debug(`[MovementTracker EventBuffer finished processing ${eventsToProcess.length} events]`);
|
|
120
169
|
}
|
|
121
170
|
}
|
|
@@ -123,6 +172,8 @@ export class EventBuffer {
|
|
|
123
172
|
export default class MovementTrackerService extends Service {
|
|
124
173
|
@service socket;
|
|
125
174
|
@service universe;
|
|
175
|
+
@service mapManager;
|
|
176
|
+
|
|
126
177
|
@tracked channels = [];
|
|
127
178
|
@tracked buffers = new Map();
|
|
128
179
|
|
|
@@ -138,7 +189,7 @@ export default class MovementTrackerService extends Service {
|
|
|
138
189
|
#getBuffer(key, model, opts = {}) {
|
|
139
190
|
let buf = this.buffers.get(key);
|
|
140
191
|
if (!buf) {
|
|
141
|
-
buf = new EventBuffer(model, opts);
|
|
192
|
+
buf = new EventBuffer(model, { ...opts, mapManager: this.mapManager });
|
|
142
193
|
buf.start();
|
|
143
194
|
this.buffers.set(key, buf);
|
|
144
195
|
}
|
|
@@ -13,11 +13,16 @@ export default class OrderActionsService extends ResourceActionService {
|
|
|
13
13
|
this.initialize('order', {
|
|
14
14
|
defaultAttributes: {
|
|
15
15
|
meta: {},
|
|
16
|
-
payload: this.store.createRecord('payload'),
|
|
17
16
|
},
|
|
18
17
|
});
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
createNewInstance(attributes = {}) {
|
|
21
|
+
const payload = attributes.payload ?? this.store.createRecord('payload');
|
|
22
|
+
|
|
23
|
+
return super.createNewInstance({ ...attributes, payload });
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
transition = {
|
|
22
27
|
view: (order) => this.transitionTo('operations.orders.index.details', order),
|
|
23
28
|
edit: (order) => this.transitionTo('operations.orders.index.edit', order),
|
|
@@ -116,6 +121,10 @@ export default class OrderActionsService extends ResourceActionService {
|
|
|
116
121
|
this.resourceContextPanel.close(saveOptions.overlay.id);
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
if (saveOptions?.refresh === true) {
|
|
125
|
+
yield this.hostRouter.refresh();
|
|
126
|
+
}
|
|
127
|
+
|
|
119
128
|
return updatedOrder;
|
|
120
129
|
} catch (error) {
|
|
121
130
|
this.notifications.serverError(error);
|
|
@@ -287,6 +296,7 @@ export default class OrderActionsService extends ResourceActionService {
|
|
|
287
296
|
saveDisabled: false,
|
|
288
297
|
saveOptions: {
|
|
289
298
|
closePanel: true,
|
|
299
|
+
refresh: true,
|
|
290
300
|
},
|
|
291
301
|
});
|
|
292
302
|
}
|
package/addon/services/osrm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import RouteOptimizationInterfaceService from './route-optimization-interface';
|
|
2
|
+
import { isArray } from '@ember/array';
|
|
2
3
|
import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
|
|
3
4
|
import polyline from '@fleetbase/ember-core/utils/polyline';
|
|
4
5
|
import { debug } from '@ember/debug';
|
|
@@ -6,21 +7,62 @@ import { debug } from '@ember/debug';
|
|
|
6
7
|
export default class OsrmService extends RouteOptimizationInterfaceService {
|
|
7
8
|
name = 'OSRM';
|
|
8
9
|
|
|
9
|
-
async
|
|
10
|
+
async computeRoute(waypoints = [], options = {}) {
|
|
11
|
+
const normalizedWaypoints = (isArray(waypoints) ? waypoints : [])
|
|
12
|
+
.map(([lat, lng]) => [Number(lat), Number(lng)])
|
|
13
|
+
.filter(([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng));
|
|
14
|
+
|
|
15
|
+
if (normalizedWaypoints.length < 2) {
|
|
16
|
+
throw new Error('At least 2 waypoints are required to compute a route.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const routingHost = getRoutingHost(options.payload, options.waypoints);
|
|
20
|
+
const coordStr = normalizedWaypoints.map(([lat, lng]) => `${lng},${lat}`).join(';');
|
|
21
|
+
const response = await fetch(`${routingHost}/route/v1/driving/${coordStr}?overview=full&geometries=polyline&steps=true`);
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`OSRM route request failed with status ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await response.json();
|
|
28
|
+
const route = result?.routes?.[0];
|
|
29
|
+
if (!route?.geometry) {
|
|
30
|
+
throw new Error('OSRM route request returned no geometry.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const coordinates = polyline.decode(route.geometry);
|
|
34
|
+
const bounds = this.#boundsFromCoordinates(coordinates);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
engine: 'osrm',
|
|
38
|
+
waypoints: normalizedWaypoints,
|
|
39
|
+
coordinates,
|
|
40
|
+
bounds,
|
|
41
|
+
summary: {
|
|
42
|
+
totalDistance: route.distance ?? 0,
|
|
43
|
+
totalTime: route.duration ?? 0,
|
|
44
|
+
},
|
|
45
|
+
legs: route.legs ?? [],
|
|
46
|
+
raw: result,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async optimize({ order, payload, waypoints, coordinates: originalCoords, preserveEndpoints = false }, options = {}) {
|
|
10
51
|
const driverAssigned = order.driver_assigned;
|
|
11
52
|
const driverPosition = driverAssigned?.location?.coordinates; // [lon,lat] | undefined
|
|
12
|
-
const
|
|
13
|
-
const
|
|
53
|
+
const hasDriverStart = this.#isValidDriverPosition(driverPosition);
|
|
54
|
+
const coordinates = hasDriverStart ? [driverPosition, ...originalCoords] : [...originalCoords];
|
|
14
55
|
const source = 'first';
|
|
15
|
-
const destination = 'any';
|
|
56
|
+
const destination = preserveEndpoints ? 'last' : 'any';
|
|
16
57
|
const roundtrip = false; // don’t loop back
|
|
17
58
|
const routingHost = getRoutingHost(payload, waypoints);
|
|
18
59
|
|
|
19
60
|
try {
|
|
20
|
-
const result = await this.fetch.routing(coordinates, { source, destination, roundtrip
|
|
61
|
+
const result = await this.fetch.routing(coordinates, { source, destination, roundtrip }, { host: routingHost, ...options });
|
|
21
62
|
|
|
22
63
|
// Pair each OSRM waypoint with its Waypoint model
|
|
23
|
-
const
|
|
64
|
+
const endpointModels = preserveEndpoints ? [null, ...waypoints, null] : waypoints;
|
|
65
|
+
const modelsByInputIndex = hasDriverStart ? [null, ...endpointModels] : endpointModels;
|
|
24
66
|
const pairs = result.waypoints.map((wp, idx) => ({
|
|
25
67
|
model: modelsByInputIndex[idx], // Ember model or null (driver)
|
|
26
68
|
wp,
|
|
@@ -43,4 +85,35 @@ export default class OsrmService extends RouteOptimizationInterfaceService {
|
|
|
43
85
|
throw err;
|
|
44
86
|
}
|
|
45
87
|
}
|
|
88
|
+
|
|
89
|
+
#boundsFromCoordinates(coordinates = []) {
|
|
90
|
+
if (!coordinates.length) {
|
|
91
|
+
return [
|
|
92
|
+
[0, 0],
|
|
93
|
+
[0, 0],
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lats = coordinates.map(([lat]) => lat);
|
|
98
|
+
const lngs = coordinates.map(([, lng]) => lng);
|
|
99
|
+
|
|
100
|
+
return [
|
|
101
|
+
[Math.min(...lats), Math.min(...lngs)],
|
|
102
|
+
[Math.max(...lats), Math.max(...lngs)],
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#isValidDriverPosition(driverPosition) {
|
|
107
|
+
if (!isArray(driverPosition) || driverPosition.length < 2) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const [lng, lat] = driverPosition.map(Number);
|
|
112
|
+
|
|
113
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lat !== 0 && lng !== 0;
|
|
118
|
+
}
|
|
46
119
|
}
|