@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,445 @@
1
+ /* global google */
2
+ import Component from '@glimmer/component';
3
+ import { tracked } from '@glimmer/tracking';
4
+ import { inject as service } from '@ember/service';
5
+ import { action } from '@ember/object';
6
+ import { isArray } from '@ember/array';
7
+ import { debug } from '@ember/debug';
8
+ import { guidFor } from '@ember/object/internals';
9
+
10
+ function buildDriverInfoWindowContent(driver) {
11
+ return `
12
+ <div class="fleetops-google-popover fleetops-google-popover--compact">
13
+ <div class="fleetops-google-popover__row">
14
+ <img src="${driver.photoUrl ?? ''}" alt="${driver.name ?? ''}" class="fleetops-google-popover__avatar" />
15
+ <div class="fleetops-google-popover__body">
16
+ <div class="fleetops-google-popover__title">${driver.name ?? '-'}</div>
17
+ <div class="fleetops-google-popover__meta">Phone: ${driver.phone ?? '-'}</div>
18
+ <div class="fleetops-google-popover__meta">Vehicle: ${driver.vehicle_name ?? '-'}</div>
19
+ <div class="fleetops-google-popover__meta">Status:
20
+ <span class="${driver.online ? 'text-green-500' : 'text-red-400'}">${driver.online ? 'Online' : 'Offline'}</span>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>`;
25
+ }
26
+
27
+ function buildDriverTooltipContent(driver) {
28
+ return `
29
+ <div class="fleetops-google-hover-tooltip__title">${driver.name ?? '-'}</div>
30
+ <div class="fleetops-google-hover-tooltip__meta ${driver.online ? 'text-green-400' : 'text-red-400'}">${driver.online ? 'Online' : 'Offline'}</div>
31
+ `;
32
+ }
33
+
34
+ function buildVehicleInfoWindowContent(vehicle) {
35
+ return `
36
+ <div class="fleetops-google-popover">
37
+ <div class="fleetops-google-popover__row">
38
+ <img src="${vehicle.photo_url ?? ''}" alt="${vehicle.display_name ?? ''}" class="fleetops-google-popover__vehicle" />
39
+ <div class="fleetops-google-popover__body">
40
+ <div class="fleetops-google-popover__title">${vehicle.displayName ?? '-'}</div>
41
+ <div class="fleetops-google-popover__meta">ID: ${vehicle.public_id ?? '-'}</div>
42
+ <div class="fleetops-google-popover__meta">Serial: ${vehicle.serial_number ?? vehicle.vin ?? '-'}</div>
43
+ <div class="fleetops-google-popover__meta">Driver: ${vehicle.driver_name ?? '-'}</div>
44
+ <div class="fleetops-google-popover__meta">Status:
45
+ <span class="${vehicle.online ? 'text-green-500' : 'text-red-400'}">${vehicle.online ? 'Online' : 'Offline'}</span>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>`;
50
+ }
51
+
52
+ function buildVehicleTooltipContent(vehicle) {
53
+ return `
54
+ <div class="fleetops-google-hover-tooltip__title">${vehicle.displayName ?? '-'}</div>
55
+ <div class="fleetops-google-hover-tooltip__meta">ID: ${vehicle.public_id ?? '-'}</div>
56
+ <div class="fleetops-google-hover-tooltip__meta ${vehicle.online ? 'text-green-400' : 'text-red-400'}">${vehicle.online ? 'Online' : 'Offline'}</div>
57
+ `;
58
+ }
59
+
60
+ function buildPlaceInfoWindowContent(place) {
61
+ return `
62
+ <div class="fleetops-google-popover fleetops-google-popover--compact">
63
+ <div class="fleetops-google-popover__body">
64
+ <div class="fleetops-google-popover__title">${place.name ?? place.address ?? '-'}</div>
65
+ <div class="fleetops-google-popover__meta">${place.address ?? ''}</div>
66
+ </div>
67
+ </div>`;
68
+ }
69
+
70
+ function buildPlaceTooltipContent(place) {
71
+ return `
72
+ <div class="fleetops-google-hover-tooltip__title">${place.name ?? place.address ?? '-'}</div>
73
+ <div class="fleetops-google-hover-tooltip__meta">${place.address ?? ''}</div>
74
+ `;
75
+ }
76
+
77
+ export default class MapGoogleLiveMapComponent extends Component {
78
+ @service mapManager;
79
+ id = guidFor(this);
80
+ @tracked map = null;
81
+ _infoWindows = new Map();
82
+
83
+ willDestroy() {
84
+ super.willDestroy(...arguments);
85
+ this._closeAllInfoWindows();
86
+ this._infoWindows.clear();
87
+ if (this.mapManager.providerName === 'google') {
88
+ this.mapManager.destroyMap();
89
+ }
90
+ }
91
+
92
+ @action async setupMap(element) {
93
+ const map = await this.mapManager.initializeMap(element, {
94
+ lat: this.args.latitude ?? 1.369,
95
+ lng: this.args.longitude ?? 103.886,
96
+ zoom: this.args.zoom ?? 12,
97
+ disableDefaultUI: true,
98
+ gestureHandling: 'greedy',
99
+ });
100
+
101
+ this.map = map;
102
+ this.mapManager.on('moveend', () => this.args.onViewportChanged?.());
103
+ this.mapManager.on('rightclick', (event) => this._showMapContextMenu(event));
104
+ this.args.onLoad?.({ target: map });
105
+ this.syncResources();
106
+ debug('[GoogleLiveMap] Map initialised');
107
+ }
108
+
109
+ @action syncResources() {
110
+ if (!this.map || !this.mapManager.isGoogleMaps) return;
111
+ this.#syncDrivers();
112
+ this.#syncVehicles();
113
+ this.#syncPlaces();
114
+ this.#syncServiceAreas();
115
+ }
116
+
117
+ async #syncDrivers() {
118
+ const drivers = this.args.drivers ?? [];
119
+
120
+ for (const driver of drivers) {
121
+ const coords = this._getCoords(driver.location);
122
+ if (!coords) continue;
123
+
124
+ const marker = this.mapManager.getMarker(driver.id);
125
+ if (!marker) {
126
+ const createdMarker = await this.mapManager.addMarker(driver.id, coords.lat, coords.lng, {
127
+ iconUrl: driver.vehicle_avatar ?? '/engines-dist/images/driver-marker.png',
128
+ iconSize: [24, 24],
129
+ title: driver.name,
130
+ tooltip: buildDriverTooltipContent(driver),
131
+ tooltipOptions: { html: true },
132
+ rotationAngle: driver.heading,
133
+ onClick: () => this.#openInfoWindow(`driver:${driver.id}`, buildDriverInfoWindowContent(driver), driver.id, () => this.args.onDriverClicked?.(driver)),
134
+ onRightClick: (event) => this._showDriverContextMenu(driver, event),
135
+ });
136
+
137
+ this.args.onDriverAdded?.(driver, { target: createdMarker });
138
+ } else {
139
+ this.mapManager.updateMarkerPosition(driver.id, coords.lat, coords.lng, false, 0);
140
+ if (Number.isFinite(driver.heading) && driver.heading !== -1) {
141
+ this.mapManager.setMarkerRotation(driver.id, driver.heading);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ async #syncVehicles() {
148
+ const vehicles = this.args.vehicles ?? [];
149
+
150
+ for (const vehicle of vehicles) {
151
+ const coords = this._getCoords(vehicle.location);
152
+ if (!coords) continue;
153
+
154
+ const marker = this.mapManager.getMarker(vehicle.id);
155
+ if (!marker) {
156
+ const createdMarker = await this.mapManager.addMarker(vehicle.id, coords.lat, coords.lng, {
157
+ iconUrl: vehicle.avatar_url ?? '/engines-dist/images/vehicle-marker.png',
158
+ iconSize: [24, 24],
159
+ title: vehicle.displayName,
160
+ tooltip: buildVehicleTooltipContent(vehicle),
161
+ tooltipOptions: { html: true },
162
+ rotationAngle: vehicle.heading,
163
+ onClick: () => this.#openInfoWindow(`vehicle:${vehicle.id}`, buildVehicleInfoWindowContent(vehicle), vehicle.id, () => this.args.onVehicleClicked?.(vehicle)),
164
+ onRightClick: (event) => this._showVehicleContextMenu(vehicle, event),
165
+ });
166
+
167
+ this.args.onVehicleAdded?.(vehicle, { target: createdMarker });
168
+ } else {
169
+ this.mapManager.updateMarkerPosition(vehicle.id, coords.lat, coords.lng, false, 0);
170
+ if (Number.isFinite(vehicle.heading) && vehicle.heading !== -1) {
171
+ this.mapManager.setMarkerRotation(vehicle.id, vehicle.heading);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ async #syncPlaces() {
178
+ const places = this.args.places ?? [];
179
+
180
+ for (const place of places) {
181
+ const coords = this._getCoords(place.location);
182
+ if (!coords) continue;
183
+
184
+ const marker = this.mapManager.getMarker(place.id);
185
+ if (!marker) {
186
+ const createdMarker = await this.mapManager.addMarker(place.id, coords.lat, coords.lng, {
187
+ iconUrl: place.avatar_url ?? '/engines-dist/images/building-marker.png',
188
+ iconSize: [16, 16],
189
+ title: place.address,
190
+ tooltip: buildPlaceTooltipContent(place),
191
+ tooltipOptions: { html: true },
192
+ onClick: () => this.#openInfoWindow(`place:${place.id}`, buildPlaceInfoWindowContent(place), place.id, () => this.args.onPlaceClicked?.(place)),
193
+ });
194
+
195
+ this.args.onPlaceAdded?.(place, { target: createdMarker });
196
+ } else {
197
+ this.mapManager.updateMarkerPosition(place.id, coords.lat, coords.lng, false, 0);
198
+ }
199
+ }
200
+
201
+ const activeMarkerIds = new Set([
202
+ ...(this.args.drivers ?? []).map((driver) => driver.id),
203
+ ...(this.args.vehicles ?? []).map((vehicle) => vehicle.id),
204
+ ...places.map((place) => place.id),
205
+ ]);
206
+
207
+ this.#removeMarkersNotIn(activeMarkerIds);
208
+ }
209
+
210
+ #syncServiceAreas() {
211
+ const serviceAreas = this.args.serviceAreas ?? [];
212
+ const overlayIds = new Set();
213
+
214
+ for (const serviceArea of serviceAreas) {
215
+ const paths =
216
+ this._coordinatesToGooglePaths(serviceArea?.border?.coordinates, { source: 'geojson' }) ??
217
+ this._coordinatesToGooglePaths(serviceArea.leafletCoordinates, { source: 'leaflet' });
218
+ if (!paths) continue;
219
+
220
+ overlayIds.add(serviceArea.id);
221
+ const polygon = this.mapManager.getOverlay(serviceArea.id);
222
+ if (!polygon) {
223
+ const createdPolygon = this.mapManager.addPolygon(serviceArea.id, paths, {
224
+ color: serviceArea.stroke_color ?? serviceArea.color ?? '#3388ff',
225
+ fillColor: serviceArea.color ?? '#3388ff',
226
+ fillOpacity: 0.2,
227
+ tooltip: serviceArea.name,
228
+ onRightClick: (event) => this._showServiceAreaContextMenu(serviceArea, event),
229
+ });
230
+ this.mapManager.hideLayer(createdPolygon);
231
+ this.args.onServiceAreaLayerAdded?.(serviceArea, { target: createdPolygon });
232
+ } else {
233
+ polygon.setPaths(paths);
234
+ polygon.__labelText = serviceArea.name;
235
+ polygon.__labelPaths = paths;
236
+ }
237
+
238
+ for (const zone of serviceArea.zones ?? []) {
239
+ const zonePaths =
240
+ this._coordinatesToGooglePaths(zone?.border?.coordinates, { source: 'geojson' }) ?? this._coordinatesToGooglePaths(zone.leafletCoordinates, { source: 'leaflet' });
241
+ if (!zonePaths) continue;
242
+
243
+ overlayIds.add(zone.id);
244
+ const zonePolygon = this.mapManager.getOverlay(zone.id);
245
+ if (!zonePolygon) {
246
+ const createdZonePolygon = this.mapManager.addPolygon(zone.id, zonePaths, {
247
+ color: zone.stroke_color ?? zone.color ?? '#3388ff',
248
+ fillColor: zone.color ?? '#3388ff',
249
+ fillOpacity: 0.2,
250
+ tooltip: zone.name,
251
+ onRightClick: (event) => this._showZoneContextMenu(zone, event),
252
+ });
253
+ this.mapManager.hideLayer(createdZonePolygon);
254
+ this.args.onZoneLayerAdd?.(zone, { target: createdZonePolygon });
255
+ } else {
256
+ zonePolygon.setPaths(zonePaths);
257
+ zonePolygon.__labelText = zone.name;
258
+ zonePolygon.__labelPaths = zonePaths;
259
+ }
260
+ }
261
+ }
262
+
263
+ this.#removeStaleOverlays(overlayIds);
264
+ }
265
+
266
+ #removeMarkersNotIn(activeIds) {
267
+ for (const [markerId] of this.mapManager.adapter?._markers ?? []) {
268
+ if (this.#isTransientMarker(markerId)) {
269
+ continue;
270
+ }
271
+
272
+ if (!activeIds.has(markerId)) {
273
+ this._infoWindows.get(`driver:${markerId}`)?.close();
274
+ this._infoWindows.delete(`driver:${markerId}`);
275
+ this._infoWindows.get(`vehicle:${markerId}`)?.close();
276
+ this._infoWindows.delete(`vehicle:${markerId}`);
277
+ this._infoWindows.get(`place:${markerId}`)?.close();
278
+ this._infoWindows.delete(`place:${markerId}`);
279
+ this.mapManager.removeMarker(markerId);
280
+ }
281
+ }
282
+ }
283
+
284
+ #isTransientMarker(markerId) {
285
+ return typeof markerId === 'string' && (markerId.startsWith('position-playback:') || markerId.startsWith('position-history:') || markerId.startsWith('route:'));
286
+ }
287
+
288
+ #removeStaleOverlays(activeIds) {
289
+ for (const [overlayId, overlay] of this.mapManager.adapter?._overlays ?? []) {
290
+ if (overlay?.__pinnedByFocus) {
291
+ continue;
292
+ }
293
+
294
+ if (typeof overlayId === 'string' && overlayId.startsWith('route:')) {
295
+ continue;
296
+ }
297
+
298
+ if (!activeIds.has(overlayId)) {
299
+ this.mapManager.removeOverlay(overlayId);
300
+ }
301
+ }
302
+ }
303
+
304
+ #openInfoWindow(infoId, content, markerId, callback) {
305
+ this._closeAllInfoWindows();
306
+
307
+ let infoWindow = this._infoWindows.get(infoId);
308
+ if (!infoWindow) {
309
+ infoWindow = new google.maps.InfoWindow({ content });
310
+ this._infoWindows.set(infoId, infoWindow);
311
+ } else {
312
+ infoWindow.setContent(content);
313
+ }
314
+
315
+ const marker = this.mapManager.getMarker(markerId);
316
+ if (marker) {
317
+ infoWindow.open({ map: this.map, anchor: marker });
318
+ }
319
+
320
+ callback?.();
321
+ }
322
+
323
+ _showMapContextMenu(event) {
324
+ const items = this.mapManager.getComposedContextMenuItems('map');
325
+ if (!items.length) return;
326
+ this.mapManager.showContextMenu(event, items);
327
+ }
328
+
329
+ _showDriverContextMenu(driver, event) {
330
+ const items = this.mapManager.getComposedContextMenuItems(`driver:${driver.public_id}`);
331
+ this.mapManager.showContextMenu(event, items);
332
+ }
333
+
334
+ _showVehicleContextMenu(vehicle, event) {
335
+ const items = this.mapManager.getComposedContextMenuItems(`vehicle:${vehicle.public_id}`);
336
+ this.mapManager.showContextMenu(event, items);
337
+ }
338
+
339
+ _showServiceAreaContextMenu(serviceArea, event) {
340
+ const items = this.mapManager.getComposedContextMenuItems(`service-area:${serviceArea.public_id}`);
341
+ this.mapManager.showContextMenu(event, items);
342
+ }
343
+
344
+ _showZoneContextMenu(zone, event) {
345
+ const items = this.mapManager.getComposedContextMenuItems(`zone:${zone.public_id}`);
346
+ this.mapManager.showContextMenu(event, items);
347
+ }
348
+
349
+ _closeAllInfoWindows() {
350
+ this._infoWindows.forEach((iw) => iw.close());
351
+ this.mapManager.closeContextMenu();
352
+ }
353
+
354
+ _getCoords(location) {
355
+ if (!location) return null;
356
+ if (location?.coordinates && isArray(location.coordinates)) {
357
+ const [lng, lat] = location.coordinates;
358
+ if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
359
+ }
360
+
361
+ if (isArray(location) && location.length >= 2) {
362
+ const [lat, lng] = location;
363
+ if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
364
+ }
365
+
366
+ if (Number.isFinite(location?.latitude) && Number.isFinite(location?.longitude)) {
367
+ return { lat: location.latitude, lng: location.longitude };
368
+ }
369
+
370
+ return null;
371
+ }
372
+
373
+ _coordinatesToGooglePaths(coordinates, options = {}) {
374
+ if (!coordinates) return null;
375
+
376
+ const source = options.source ?? 'leaflet';
377
+
378
+ try {
379
+ const rings = this._normalizeCoordinateRings(coordinates, source);
380
+ const normalizedRings = rings.map((ring) => ring.map((point) => this._pointToGoogleLatLng(point, source)).filter(Boolean)).filter((ring) => ring.length >= 3);
381
+
382
+ if (normalizedRings.length === 0) {
383
+ return null;
384
+ }
385
+
386
+ return normalizedRings.length === 1 ? normalizedRings[0] : normalizedRings;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+
392
+ _normalizeCoordinateRings(coordinates, source = 'leaflet') {
393
+ if (!isArray(coordinates) || coordinates.length === 0) {
394
+ return [];
395
+ }
396
+
397
+ if (source === 'geojson') {
398
+ const [firstPolygon] = coordinates;
399
+
400
+ // Polygon coordinates: [ring[point]]
401
+ if (isArray(firstPolygon) && typeof firstPolygon?.[0]?.[0] === 'number') {
402
+ return [firstPolygon];
403
+ }
404
+
405
+ // MultiPolygon coordinates: [polygon[ring[point]]]
406
+ if (isArray(firstPolygon) && isArray(firstPolygon?.[0])) {
407
+ return coordinates.map((polygon) => (isArray(polygon) ? polygon[0] : null)).filter((ring) => isArray(ring));
408
+ }
409
+
410
+ return [];
411
+ }
412
+
413
+ // Flat leaflet-style ring: [[lat, lng], ...]
414
+ if (typeof coordinates?.[0]?.[0] === 'number') {
415
+ return [coordinates];
416
+ }
417
+
418
+ // Nested leaflet-style rings: [[[lat, lng], ...], ...]
419
+ if (isArray(coordinates?.[0])) {
420
+ return coordinates.filter((ring) => isArray(ring));
421
+ }
422
+
423
+ return [];
424
+ }
425
+
426
+ _pointToGoogleLatLng(point, source = 'leaflet') {
427
+ if (typeof point?.lat === 'number' && typeof point?.lng === 'number') {
428
+ return { lat: point.lat, lng: point.lng };
429
+ }
430
+
431
+ if (!isArray(point) || point.length < 2) {
432
+ return null;
433
+ }
434
+
435
+ if (source === 'geojson') {
436
+ const lng = Number(point[0]);
437
+ const lat = Number(point[1]);
438
+ return Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null;
439
+ }
440
+
441
+ const lat = Number(point[0]);
442
+ const lng = Number(point[1]);
443
+ return Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null;
444
+ }
445
+ }