@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.
Files changed (148) hide show
  1. package/addon/components/admin/map-settings.hbs +26 -0
  2. package/addon/components/admin/map-settings.js +46 -0
  3. package/addon/components/admin/routing-settings.hbs +11 -0
  4. package/addon/components/cell/order-route-type.hbs +46 -0
  5. package/addon/components/cell/order-route-type.js +90 -0
  6. package/addon/components/cell/place-address.hbs +1 -0
  7. package/addon/components/display-place.hbs +45 -56
  8. package/addon/components/display-place.js +21 -0
  9. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  10. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  11. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  12. package/addon/components/maintenance-schedule/form.js +2 -1
  13. package/addon/components/map/drawer/driver-listing.js +5 -5
  14. package/addon/components/map/drawer/place-listing.js +5 -5
  15. package/addon/components/map/drawer/position-listing.js +102 -21
  16. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  17. package/addon/components/map/google-live-map.hbs +9 -0
  18. package/addon/components/map/google-live-map.js +445 -0
  19. package/addon/components/map/leaflet-live-map.hbs +186 -142
  20. package/addon/components/map/leaflet-live-map.js +121 -41
  21. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  22. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  23. package/addon/components/map/toolbar.hbs +3 -3
  24. package/addon/components/map/toolbar.js +9 -1
  25. package/addon/components/modals/map-layer-form.hbs +132 -63
  26. package/addon/components/modals/orchestrator-import.hbs +4 -11
  27. package/addon/components/modals/orchestrator-import.js +17 -1
  28. package/addon/components/modals/update-order-activity.hbs +1 -1
  29. package/addon/components/orchestrator/order-pool.js +3 -2
  30. package/addon/components/orchestrator-workbench.js +29 -41
  31. package/addon/components/order/details/detail.js +2 -2
  32. package/addon/components/order/details/payload.hbs +5 -5
  33. package/addon/components/order/details/payload.js +18 -1
  34. package/addon/components/order/details/route.hbs +2 -2
  35. package/addon/components/order/details/route.js +76 -3
  36. package/addon/components/order/form/details.hbs +1 -2
  37. package/addon/components/order/form/details.js +6 -0
  38. package/addon/components/order/form/payload.hbs +33 -0
  39. package/addon/components/order/form/payload.js +1 -0
  40. package/addon/components/order/form/route.hbs +8 -8
  41. package/addon/components/order/form/route.js +86 -18
  42. package/addon/components/order/form.js +2 -2
  43. package/addon/components/order/route-editor.hbs +209 -147
  44. package/addon/components/order/route-editor.js +143 -102
  45. package/addon/components/place/address.hbs +1 -0
  46. package/addon/components/place/address.js +12 -0
  47. package/addon/components/route-list.hbs +43 -55
  48. package/addon/components/route-list.js +47 -0
  49. package/addon/components/route-optimization-wizard-panel.js +3 -4
  50. package/addon/components/vehicle/card.hbs +4 -4
  51. package/addon/controllers/maintenance/schedules/index.js +2 -1
  52. package/addon/controllers/operations/orders/index/details.js +110 -37
  53. package/addon/controllers/operations/orders/index/new.js +1 -1
  54. package/addon/controllers/operations/orders/index.js +8 -1
  55. package/addon/controllers/settings/map.js +97 -0
  56. package/addon/controllers/settings/payments/index.js +1 -1
  57. package/addon/controllers/settings/routing.js +47 -12
  58. package/addon/extension.js +11 -0
  59. package/addon/helpers/place-address-html.js +6 -0
  60. package/addon/instance-initializers/register-google-routes.js +17 -0
  61. package/addon/instance-initializers/register-osrm.js +5 -0
  62. package/addon/routes/application.js +6 -0
  63. package/addon/routes/operations/orchestrator.js +13 -0
  64. package/addon/routes/operations/orders/index/details.js +29 -22
  65. package/addon/routes/operations/orders/index/new.js +9 -0
  66. package/addon/routes/operations/orders/index.js +0 -1
  67. package/addon/routes/settings/map.js +3 -0
  68. package/addon/routes.js +1 -0
  69. package/addon/services/entity-actions.js +1 -1
  70. package/addon/services/geofence-event-bus.js +2 -1
  71. package/addon/services/geofence.js +204 -168
  72. package/addon/services/google-routes.js +300 -0
  73. package/addon/services/leaflet-draw-restriction.js +2 -1
  74. package/addon/services/leaflet-map-manager.js +17 -0
  75. package/addon/services/map-adapter/google.js +2205 -0
  76. package/addon/services/map-adapter/leaflet.js +1042 -0
  77. package/addon/services/map-adapter-interface.js +551 -0
  78. package/addon/services/map-manager.js +768 -0
  79. package/addon/services/map-settings.js +75 -0
  80. package/addon/services/movement-tracker.js +83 -32
  81. package/addon/services/order-actions.js +11 -1
  82. package/addon/services/osrm.js +79 -6
  83. package/addon/services/position-playback.js +135 -62
  84. package/addon/services/route-engine.js +83 -0
  85. package/addon/services/service-areas.js +8 -1
  86. package/addon/styles/fleetops-engine.css +555 -0
  87. package/addon/templates/management/vehicles/index.hbs +0 -1
  88. package/addon/templates/operations/orders/index/details.hbs +0 -1
  89. package/addon/templates/settings/map.hbs +53 -0
  90. package/addon/templates/settings/routing.hbs +15 -4
  91. package/addon/utils/fleet-ops-options.js +1 -0
  92. package/addon/utils/has-valid-coordinates.js +6 -0
  93. package/addon/utils/map-provider-options.js +6 -0
  94. package/addon/utils/order-route-editing.js +86 -0
  95. package/addon/utils/order-route-summary.js +20 -0
  96. package/addon/utils/place-address-html.js +41 -0
  97. package/addon/utils/route-colors.js +43 -12
  98. package/addon/utils/route-visualization.js +93 -0
  99. package/addon/utils/vendor-integration.js +2 -1
  100. package/app/components/admin/map-settings.js +1 -0
  101. package/app/components/admin/routing-settings.js +1 -0
  102. package/app/components/cell/order-route-type.js +1 -0
  103. package/app/components/cell/place-address.js +1 -0
  104. package/app/components/map/google-live-map.js +1 -0
  105. package/app/components/place/address.js +1 -0
  106. package/app/controllers/settings/map.js +1 -0
  107. package/app/helpers/place-address-html.js +1 -0
  108. package/app/routes/settings/map.js +1 -0
  109. package/app/services/map-adapter/google.js +1 -0
  110. package/app/services/map-adapter/leaflet.js +1 -0
  111. package/app/services/map-adapter-interface.js +1 -0
  112. package/app/services/map-manager.js +1 -0
  113. package/app/services/map-settings.js +1 -0
  114. package/app/templates/settings/map.js +1 -0
  115. package/app/utils/has-valid-coordinates.js +1 -0
  116. package/app/utils/order-route-editing.js +2 -0
  117. package/app/utils/order-route-summary.js +1 -0
  118. package/app/utils/place-address-html.js +1 -0
  119. package/app/utils/route-colors.js +1 -1
  120. package/app/utils/route-visualization.js +1 -0
  121. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  122. package/composer.json +1 -1
  123. package/extension.json +1 -1
  124. package/package.json +4 -4
  125. package/pnpm-workspace.yaml +7 -0
  126. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  127. package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
  128. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  129. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  130. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
  131. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  132. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  133. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  134. package/server/src/Http/Controllers/Internal/v1/OrderController.php +90 -34
  135. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  136. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  137. package/server/src/Http/Filter/OrderFilter.php +2 -1
  138. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  139. package/server/src/Http/Resources/v1/Order.php +20 -1
  140. package/server/src/Models/Order.php +42 -1
  141. package/server/src/Models/OrderConfig.php +15 -0
  142. package/server/src/Models/Payload.php +131 -27
  143. package/server/src/Models/Place.php +182 -48
  144. package/server/src/Models/ServiceRate.php +4 -3
  145. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  146. package/server/src/Support/Utils.php +36 -2
  147. package/server/src/routes.php +4 -0
  148. 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
- constructor(model, { callback = null, waitTime = 1000 * 3 }) {
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 = []; // Clear immediately to accept new 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
- // get movingObject marker
69
- const marker = this.model.leafletLayer || this.model._layer || this.model._marker;
70
- if (!marker || !marker._map) {
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 -> Leaflet [lat, lng]
121
+ // GeoJSON -> [lat, lng]
79
122
  const [lng, lat] = data.location.coordinates;
80
123
  const nextLatLng = [lat, lng];
81
124
 
82
- // Calc speed
83
- const map = marker._map;
84
- const prev = marker.getLatLng();
85
- const meters = map ? map.distance(prev, nextLatLng) : prev.distanceTo(nextLatLng);
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
- // Apply rotation if heading is valid
96
- if (typeof marker.setRotationAngle === 'function' && Number.isFinite(data.heading) && data.heading !== -1) {
97
- marker.setRotationAngle(data.heading);
98
- }
99
-
100
- // Move marker with animation
101
- if (typeof marker.slideTo === 'function') {
102
- marker.slideTo(nextLatLng, { duration: durationMs });
103
- } else {
104
- marker.setLatLng(nextLatLng);
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
  }
@@ -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 optimize({ order, payload, waypoints, coordinates: originalCoords }, options = {}) {
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 coordinates = driverPosition ? [driverPosition, ...originalCoords] : [...originalCoords];
13
- const hasDriverStart = Boolean(driverPosition);
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, annotations: true }, { host: routingHost, ...options });
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 modelsByInputIndex = hasDriverStart ? [null, ...waypoints] : waypoints;
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
  }