@fleetbase/fleetops-engine 0.2.7 → 0.2.9

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 (47) hide show
  1. package/README.md +3 -3
  2. package/addon/components/context-panel.hbs +5 -0
  3. package/addon/components/context-panel.js +6 -0
  4. package/addon/components/driver-form-panel.hbs +22 -22
  5. package/addon/components/driver-form-panel.js +28 -3
  6. package/addon/components/driver-panel.hbs +8 -8
  7. package/addon/components/driver-panel.js +23 -6
  8. package/addon/components/fleet-vehicle-listing.hbs +1 -1
  9. package/addon/components/live-map.hbs +20 -9
  10. package/addon/components/live-map.js +1045 -354
  11. package/addon/components/settings-window.hbs +1 -1
  12. package/addon/components/vehicle-form-panel.hbs +24 -24
  13. package/addon/components/vehicle-form-panel.js +26 -3
  14. package/addon/components/vehicle-panel.hbs +8 -9
  15. package/addon/components/vehicle-panel.js +21 -4
  16. package/addon/controllers/operations/orders/index/new.js +3 -5
  17. package/addon/controllers/operations/orders/index/view.js +5 -7
  18. package/addon/controllers/operations/orders/index.js +15 -2
  19. package/addon/engine.js +6 -0
  20. package/addon/routes/operations/orders/index/view.js +4 -2
  21. package/addon/routes/operations/orders/index.js +5 -1
  22. package/addon/services/context-panel.js +106 -0
  23. package/addon/services/leaflet-contextmenu-manager.js +311 -0
  24. package/addon/services/leaflet-map-manager.js +61 -0
  25. package/addon/services/service-areas.js +242 -32
  26. package/addon/templates/application.hbs +2 -1
  27. package/addon/utils/find-leaflet-layer.js +13 -0
  28. package/addon/utils/fly-to-leaflet-layer.js +25 -0
  29. package/addon/utils/get-leaflet-layer-by-id.js +12 -0
  30. package/addon/utils/layer-can-bind-contextmenu.js +3 -0
  31. package/app/components/context-panel.js +1 -0
  32. package/app/services/context-panel.js +1 -0
  33. package/app/services/leaflet-contextmenu-manager.js +1 -0
  34. package/app/services/leaflet-map-manager.js +1 -0
  35. package/app/utils/find-leaflet-layer.js +1 -0
  36. package/app/utils/fly-to-leaflet-layer.js +1 -0
  37. package/app/utils/get-leaflet-layer-by-id.js +1 -0
  38. package/app/utils/layer-can-bind-contextmenu.js +1 -0
  39. package/composer.json +12 -2
  40. package/extension.json +1 -1
  41. package/package.json +2 -2
  42. package/server/src/Http/Controllers/Internal/v1/LiveController.php +7 -4
  43. package/server/src/Http/Filter/VehicleFilter.php +7 -0
  44. package/server/src/Http/Resources/v1/Vehicle.php +51 -39
  45. package/.php-cs-fixer.cache +0 -1
  46. package/addon/services/map-manager.js +0 -3
  47. package/app/services/map-manager.js +0 -1
@@ -1,67 +1,227 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
- import { action, computed, set } from '@ember/object';
4
+ import { action, set } from '@ember/object';
5
5
  import { isArray } from '@ember/array';
6
- import { isBlank } from '@ember/utils';
7
- import { dasherize } from '@ember/string';
6
+ import { dasherize, camelize } from '@ember/string';
7
+ import { singularize } from 'ember-inflector';
8
8
  import { alias } from '@ember/object/computed';
9
- import { guidFor } from '@ember/object/internals';
10
9
  import { later } from '@ember/runloop';
11
10
  import { allSettled } from 'rsvp';
11
+ import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
12
12
 
13
13
  const DEFAULT_LATITUDE = 1.369;
14
14
  const DEFAULT_LONGITUDE = 103.8864;
15
15
 
16
+ /**
17
+ * Component which displays live activity.
18
+ *
19
+ * @class
20
+ */
16
21
  export default class LiveMapComponent extends Component {
22
+ /**
23
+ * Inject the `store` service.
24
+ *
25
+ * @memberof LiveMapComponent
26
+ */
17
27
  @service store;
28
+
29
+ /**
30
+ * Inject the `fetch` service.
31
+ *
32
+ * @memberof LiveMapComponent
33
+ */
18
34
  @service fetch;
35
+
36
+ /**
37
+ * Inject the `socket` service.
38
+ *
39
+ * @memberof LiveMapComponent
40
+ */
19
41
  @service socket;
42
+
43
+ /**
44
+ * Inject the `currentUser` service.
45
+ *
46
+ * @memberof LiveMapComponent
47
+ */
20
48
  @service currentUser;
49
+
50
+ /**
51
+ * Inject the `notifications` service.
52
+ *
53
+ * @memberof LiveMapComponent
54
+ */
21
55
  @service notifications;
56
+
57
+ /**
58
+ * Inject the `serviceAreas` service.
59
+ *
60
+ * @memberof LiveMapComponent
61
+ */
22
62
  @service serviceAreas;
63
+
64
+ /**
65
+ * Inject the `appCache` service.
66
+ *
67
+ * @memberof LiveMapComponent
68
+ */
23
69
  @service appCache;
70
+
71
+ /**
72
+ * Inject the `universe` service.
73
+ *
74
+ * @memberof LiveMapComponent
75
+ */
24
76
  @service universe;
25
77
 
78
+ /**
79
+ * Inject the `crud` service.
80
+ *
81
+ * @memberof LiveMapComponent
82
+ */
83
+ @service crud;
84
+
85
+ /**
86
+ * Inject the `contextPanel` service.
87
+ *
88
+ * @memberof LiveMapComponent
89
+ */
90
+ @service contextPanel;
91
+
92
+ /**
93
+ * Inject the `leafletMapManager` service.
94
+ *
95
+ * @memberof LiveMapComponent
96
+ */
97
+ @service leafletMapManager;
98
+
99
+ /**
100
+ * Inject the `leafletContextmenuManager` service.
101
+ *
102
+ * @memberof LiveMapComponent
103
+ */
104
+ @service leafletContextmenuManager;
105
+
106
+ /**
107
+ * An array of routes.
108
+ * @type {Array}
109
+ */
26
110
  @tracked routes = [];
111
+
112
+ /**
113
+ * An array of drivers.
114
+ * @type {Array}
115
+ */
27
116
  @tracked drivers = [];
117
+
118
+ /**
119
+ * An array of vehicles.
120
+ * @type {Array}
121
+ */
122
+ @tracked vehicles = [];
123
+
124
+ /**
125
+ * An array of places.
126
+ * @type {Array}
127
+ */
28
128
  @tracked places = [];
129
+
130
+ /**
131
+ * An array of channels.
132
+ * @type {Array}
133
+ */
29
134
  @tracked channels = [];
135
+
136
+ /**
137
+ * Indicates if data is loading.
138
+ * @type {boolean}
139
+ */
30
140
  @tracked isLoading = true;
141
+
142
+ /**
143
+ * Indicates if the component is ready.
144
+ * @type {boolean}
145
+ */
31
146
  @tracked isReady = false;
32
- @tracked isDriversVisible = true;
33
- @tracked isPlacesVisible = true;
34
- @tracked isRoutesVisible = true;
35
- @tracked isDrawControlsVisible = false;
36
- @tracked isCreatingServiceArea = false;
37
- @tracked isCreatingZone = false;
38
- @tracked currentContextMenuItems = [];
147
+
148
+ /**
149
+ * Controls for visibility.
150
+ * @type {Object}
151
+ */
152
+ @tracked visibilityControls = {};
153
+
154
+ /**
155
+ * An array of active service areas.
156
+ * @type {Array}
157
+ */
39
158
  @tracked activeServiceAreas = [];
159
+
160
+ /**
161
+ * An array of editable map layers.
162
+ * @type {Array}
163
+ */
40
164
  @tracked editableLayers = [];
165
+
166
+ /**
167
+ * The Leaflet map instance.
168
+ * @type {Object}
169
+ */
41
170
  @tracked leafletMap;
42
- @tracked activeFeatureGroup;
171
+
172
+ /**
173
+ * The map's zoom level.
174
+ * @type {number}
175
+ */
176
+ @tracked zoom = 12;
177
+
178
+ /**
179
+ * The feature group for drawing on the map.
180
+ * @type {Object}
181
+ */
43
182
  @tracked drawFeatureGroup;
183
+
184
+ /**
185
+ * The draw control for the map.
186
+ * @type {Object}
187
+ */
44
188
  @tracked drawControl;
189
+
190
+ /**
191
+ * The URL for the map's tile source.
192
+ * @type {string}
193
+ */
194
+ @tracked tileSourceUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
195
+
196
+ /**
197
+ * The latitude for the map view.
198
+ * @type {number}
199
+ */
45
200
  @tracked latitude = DEFAULT_LATITUDE;
46
- @tracked longitude = DEFAULT_LONGITUDE;
47
- @tracked skipSetCoordinates = false;
48
- @tracked mapId = guidFor(this);
49
- @alias('currentUser.latitude') userLatitude;
50
- @alias('currentUser.longitude') userLongitude;
51
201
 
52
- @computed('args.zoom') get zoom() {
53
- return this.args.zoom || 12;
54
- }
202
+ /**
203
+ * The longitude for the map view.
204
+ * @type {number}
205
+ */
206
+ @tracked longitude = DEFAULT_LONGITUDE;
55
207
 
56
- @computed('args.{tileSourceUrl,darkMode}') get tileSourceUrl() {
57
- const { darkMode, tileSourceUrl } = this.args;
208
+ /**
209
+ * Indicates if coordinate setting should be skipped.
210
+ * @type {boolean}
211
+ */
212
+ @tracked skipSetCoordinates = false;
58
213
 
59
- if (darkMode === true) {
60
- return 'https://{s}.tile.jawg.io/jawg-matrix/{z}/{x}/{y}{r}.png?access-token=';
61
- }
214
+ /**
215
+ * The user's latitude from the currentUser.
216
+ * @type {number}
217
+ */
218
+ @alias('currentUser.latitude') userLatitude;
62
219
 
63
- return tileSourceUrl ?? 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
64
- }
220
+ /**
221
+ * The user's longitude from the currentUser.
222
+ * @type {number}
223
+ */
224
+ @alias('currentUser.longitude') userLongitude;
65
225
 
66
226
  /**
67
227
  * Creates an instance of LiveMapComponent.
@@ -69,26 +229,85 @@ export default class LiveMapComponent extends Component {
69
229
  */
70
230
  constructor() {
71
231
  super(...arguments);
232
+ this.skipSetCoordinates = getWithDefault(this.args, 'skipSetCoordinates', false);
233
+ this.zoom = getWithDefault(this.args, 'zoom', 12);
234
+ this.tileSourceUrl = getWithDefault(this.args, 'tileSourceUrl', 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png');
72
235
 
73
- this.skipSetCoordinates = this.args.skipSetCoordinates ?? false;
236
+ if (this.args.darkMode === true) {
237
+ this.tileSourceUrl = 'https://{s}.tile.jawg.io/jawg-matrix/{z}/{x}/{y}{r}.png?access-token=';
238
+ }
74
239
  }
75
240
 
76
241
  /**
77
- * ----------------------------------------------------------------------------
78
- * SETUP
79
- * ----------------------------------------------------------------------------
242
+ * Initializes the LiveMapComponent by triggering events, setting initial coordinates,
243
+ * and loading required live data.
80
244
  *
81
- * Functions for initialization and setup.
245
+ * @memberof LiveMapComponent
246
+ * @action
247
+ * @function
82
248
  */
83
- @action async setupLiveMap() {
249
+ @action setupComponent() {
84
250
  // trigger that initial coordinates have been set
85
- this.universe.trigger('livemap.loaded', this);
251
+ this.universe.trigger('fleetops.livemap.loaded', this);
252
+
253
+ // set initial coordinates
254
+ this.setInitialCoordinates();
255
+
256
+ // load required live data
257
+ this.fetchLiveData('routes');
258
+ this.fetchLiveData('vehicles', {
259
+ onLoaded: (vehicles) => {
260
+ this.watchMovingObjects('vehicles', vehicles);
261
+ },
262
+ });
263
+ this.fetchLiveData('drivers', {
264
+ onLoaded: (drivers) => {
265
+ this.watchMovingObjects('drivers', drivers);
266
+ },
267
+ });
268
+ this.fetchLiveData('places');
269
+ this.listen();
270
+ this.ready();
271
+ }
272
+
273
+ /**
274
+ * Marks the LiveMapComponent as ready by setting the "isReady" property and triggering
275
+ * the "onReady" action and a "fleetops.livemap.ready" event.
276
+ *
277
+ * @memberof LiveMapComponent
278
+ * @function
279
+ */
280
+ ready() {
281
+ this.isReady = true;
282
+ this.triggerAction('onReady');
283
+ this.universe.trigger('fleetops.livemap.ready', this);
284
+ }
86
285
 
286
+ /**
287
+ * Sets the initial coordinates for the LiveMapComponent.
288
+ *
289
+ * This function checks if initial coordinates are available in the appCache, and if not,
290
+ * it fetches the coordinates using the "getInitialCoordinates" function. It sets the
291
+ * latitude and longitude properties and triggers an event to notify that coordinates
292
+ * have been set.
293
+ *
294
+ * @memberof LiveMapComponent
295
+ * @async
296
+ * @function
297
+ * @returns {Promise<[number, number] | null>} An array containing the latitude and longitude
298
+ * if available, or null if the function is skipped.
299
+ */
300
+ async setInitialCoordinates() {
87
301
  if (this.skipSetCoordinates === false) {
88
302
  if (this.appCache.has(['map_latitude', 'map_longitude'])) {
89
303
  this.latitude = this.appCache.get('map_latitude');
90
304
  this.longitude = this.appCache.get('map_longitude');
91
305
  this.isReady = true;
306
+
307
+ // trigger that initial coordinates is set to livemap component
308
+ this.universe.trigger('fleetops.livemap.has_coordinates', { latitude: this.latitude, longitude: this.longitude });
309
+
310
+ return [this.latitude, this.longitude];
92
311
  }
93
312
 
94
313
  const { latitude, longitude } = await this.getInitialCoordinates();
@@ -100,39 +319,49 @@ export default class LiveMapComponent extends Component {
100
319
 
101
320
  this.latitude = latitude;
102
321
  this.longitude = longitude;
103
- }
104
-
105
- // trigger that initial coordinates have been set
106
- this.universe.trigger('livemap.has_coordinates', { latitude: this.latitude, longitude: this.longitude });
107
-
108
- this.routes = await this.fetchActiveRoutes();
109
- this.drivers = await this.fetchActiveDrivers();
110
- this.places = await this.fetchActivePlaces();
111
- this.serviceAreaRecords = await this.fetchServiceAreas();
112
- this.isReady = true;
322
+ this.isReady = true;
113
323
 
114
- this.watchDrivers(this.drivers);
115
- this.listenForOrders();
324
+ // trigger that initial coordinates is set to livemap component
325
+ this.universe.trigger('fleetops.livemap.has_coordinates', { latitude: this.latitude, longitude: this.longitude });
116
326
 
117
- if (typeof this.args.onReady === 'function') {
118
- this.args.onReady(this);
327
+ return [this.latitude, this.longitude];
119
328
  }
120
329
 
121
- // add context event
122
- this.universe.trigger('livemap.ready', this);
330
+ return null;
123
331
  }
124
332
 
125
- @action setMapReference(event) {
333
+ /**
334
+ * Sets up the LiveMap component and the Leaflet map instance.
335
+ *
336
+ * This function initializes the LiveMap component, associates it with the Leaflet map instance,
337
+ * triggers the "fleetops.livemap.leaflet_ready" event, and performs additional setup tasks like
338
+ * configuring context menus, hiding draw controls, and associating the map with the "serviceAreas"
339
+ * service. It also triggers the "onLoad" action with the provided event and target.
340
+ *
341
+ * @action
342
+ * @function
343
+ * @param {Event} event - The event object.
344
+ */
345
+ @action setupMap(event) {
126
346
  const { target } = event;
127
347
 
128
348
  // set liveMapComponent component to instance
129
- set(event, 'target.liveMap', this);
349
+ set(target, 'liveMap', this);
130
350
 
131
351
  // set map instance
132
352
  this.leafletMap = target;
133
353
 
354
+ // trigger liveMap ready through universe
355
+ this.universe.trigger('fleetops.livemap.leaflet_ready', event, target);
356
+
357
+ // make fleetops map globally available on the window
358
+ window.FleetOpsLeafletMap = target;
359
+
360
+ // store this component to universe
361
+ this.universe.set('FleetOpsLiveMap', this);
362
+
134
363
  // setup context menu
135
- this.setupContextMenu(target);
364
+ this.createMapContextMenu(target);
136
365
 
137
366
  // hide draw controls by default
138
367
  this.hideDrawControls();
@@ -140,98 +369,269 @@ export default class LiveMapComponent extends Component {
140
369
  // set instance to service areas service
141
370
  this.serviceAreas.setMapInstance(target);
142
371
 
143
- if (typeof this.args.onLoad === 'function') {
144
- this.args.onLoad(...arguments);
145
- }
372
+ // trigger map loaded event
373
+ this.triggerAction('onLoad', ...arguments);
146
374
  }
147
375
 
148
- @action setupContextMenu(map) {
149
- if (!map?.contextmenu) {
150
- return;
376
+ /**
377
+ * Invokes an action by name on the current component and its arguments (if defined).
378
+ *
379
+ * This function checks if an action with the specified name exists on the current component.
380
+ * If found, it invokes the action with the provided parameters. It also checks the component's
381
+ * arguments for the action and invokes it if defined.
382
+ *
383
+ * @action
384
+ * @function
385
+ * @param {string} actionName - The name of the action to trigger.
386
+ * @param {...any} params - Optional parameters to pass to the action.
387
+ */
388
+ @action triggerAction(actionName, ...params) {
389
+ if (typeof this[actionName] === 'function') {
390
+ this[actionName](...params);
151
391
  }
152
392
 
153
- // reset items if any
154
- if (typeof map?.contextmenu?.removeAllItems === 'function') {
155
- map?.contextmenu?.removeAllItems();
393
+ if (typeof this.args[actionName] === 'function') {
394
+ this.args[actionName](...params);
156
395
  }
396
+ }
157
397
 
158
- const { contextmenu } = map;
159
- const contextMenuItems = this.buildContextMenuItems();
398
+ /**
399
+ * Fetches live data from the specified path and updates the component state accordingly.
400
+ *
401
+ * @memberof LiveMapComponent
402
+ * @function
403
+ * @param {string} path - The path to fetch live data from.
404
+ * @param {Object} [options={}] - Optional configuration options.
405
+ * @param {Object} [options.params={}] - Additional parameters to include in the request.
406
+ * @param {Function} [options.onLoaded] - A callback function to execute when the data is loaded.
407
+ * @returns {Promise} A promise that resolves with the fetched data.
408
+ */
409
+ fetchLiveData(path, options = {}) {
410
+ this.isLoading = true;
160
411
 
161
- contextMenuItems.forEach((options) => contextmenu.addItem(options));
412
+ const internalName = camelize(path);
413
+ const callbackFnName = `on${internalName}Loaded`;
414
+ const params = getWithDefault(options, 'params', {});
162
415
 
163
- if (contextmenu.enabled === true || contextmenu._enabled === true) {
164
- return;
165
- }
416
+ return this.fetch
417
+ .get(`fleet-ops/live/${path}`, params, { normalizeToEmberData: true, normalizeModelType: singularize(internalName) })
418
+ .then((data) => {
419
+ this.triggerAction(callbackFnName);
420
+ this.createVisibilityControl(internalName);
421
+ this[internalName] = data;
166
422
 
167
- contextmenu.enable();
168
- }
423
+ if (typeof options.onLoaded === 'function') {
424
+ options.onLoaded(data);
425
+ }
169
426
 
170
- @action toggleDrawControlContextMenuItem() {
171
- const index = this.currentContextMenuItems.findIndex((options) => options.text?.includes('draw controls'));
427
+ return data;
428
+ })
429
+ .finally(() => {
430
+ this.isLoading = false;
431
+ });
432
+ }
172
433
 
173
- if (index > 0) {
174
- const options = this.currentContextMenuItems.objectAt(index);
434
+ /**
435
+ * Creates or updates a visibility control for a specific element by name.
436
+ *
437
+ * @function
438
+ * @param {string} name - The name or identifier for the visibility control.
439
+ * @param {boolean} [visible=true] - A boolean value indicating whether the element is initially visible (default is true).
440
+ */
441
+ createVisibilityControl(name, visible = true) {
442
+ this.visibilityControls = {
443
+ ...this.visibilityControls,
444
+ [name]: visible,
445
+ };
446
+ }
175
447
 
176
- if (!isBlank(options)) {
177
- options.text = this.isDrawControlsVisible ? 'Hide draw controls...' : 'Enable draw controls...';
178
- }
448
+ /**
449
+ * Hide all visibility controls associated with the current instance.
450
+ */
451
+ hideAll() {
452
+ const controls = Object.keys(this.visibilityControls);
179
453
 
180
- this.leafletMap?.contextmenu?.removeItem(index);
181
- this.leafletMap?.contextmenu?.insertItem(options, index);
454
+ for (let i = 0; i < controls.length; i++) {
455
+ const control = controls.objectAt(i);
456
+ this.hide(control);
182
457
  }
183
458
  }
184
459
 
185
- @action removeServiceAreaFromContextMenu(serviceArea) {
186
- const index = this.currentContextMenuItems.findIndex((options) => options.text?.includes(`Focus Service Area: ${serviceArea.name}`));
460
+ /**
461
+ * Show all visibility controls associated with the current instance.
462
+ */
463
+ showAll() {
464
+ const controls = Object.keys(this.visibilityControls);
187
465
 
188
- if (index > 0) {
189
- this.leafletMap?.contextmenu?.removeItem(index);
466
+ for (let i = 0; i < controls.length; i++) {
467
+ const control = controls.objectAt(i);
468
+ this.show(control);
190
469
  }
191
470
  }
192
471
 
193
- @action rebuildContextMenu() {
194
- const map = this.leafletMap;
472
+ /**
473
+ * Hides a specific element by name using a visibility control.
474
+ *
475
+ * @function
476
+ * @param {string} name - The name or identifier of the element to hide.
477
+ */
478
+ hide(name) {
479
+ if (isArray(name)) {
480
+ return name.forEach(this.hide);
481
+ }
482
+
483
+ this.createVisibilityControl(name, false);
484
+ }
195
485
 
196
- if (map) {
197
- this.setupContextMenu(map);
486
+ /**
487
+ * Shows a specific element by name using a visibility control.
488
+ *
489
+ * @function
490
+ * @param {string} name - The name or identifier of the element to show.
491
+ */
492
+ show(name) {
493
+ if (isArray(name)) {
494
+ return name.forEach(this.show);
198
495
  }
496
+
497
+ this.createVisibilityControl(name, true);
199
498
  }
200
499
 
201
- @action setFn(actionName, callback) {
202
- this[actionName] = callback;
500
+ /**
501
+ * Check if a specific element or feature is currently visible based on its name.
502
+ *
503
+ * @param {string} name - The name of the element or feature to check visibility for.
504
+ * @returns {boolean} Returns `true` if the element or feature is currently visible, `false` otherwise.
505
+ */
506
+ isVisible(name) {
507
+ return this.visibilityControls[name] === true;
203
508
  }
204
509
 
205
- @action shouldSkipSettingInitialCoordinates() {
206
- this.skipSetCoordinates = true;
510
+ /**
511
+ * Toggles the context menu item for enabling/disabling draw controls.
512
+ *
513
+ * @param {Object} [options] - Optional settings for the context menu item.
514
+ * @param {string} [options.onText='Hide draw controls...'] - Text to display when enabling draw controls.
515
+ * @param {string} [options.offText='Enable draw controls...'] - Text to display when disabling draw controls.
516
+ * @param {string} [options.callback=function] - Callback function to trigger after toggle.
517
+ */
518
+ toggleDrawControlContextMenuItem(options = {}) {
519
+ const toggle = !this.isVisible('drawControls');
520
+
521
+ this.leafletContextmenuManager.toggleContextMenuItem('map', 'draw controls', {
522
+ onText: 'Hide draw controls...',
523
+ offText: 'Enable draw controls...',
524
+ toggle,
525
+ callback: (isToggled) => {
526
+ if (isToggled) {
527
+ this.showDrawControls();
528
+ } else {
529
+ this.hideDrawControls();
530
+ }
531
+ },
532
+ ...options,
533
+ });
534
+ }
535
+
536
+ /**
537
+ * Removes a specific service area from the context menu.
538
+ *
539
+ * @param {Object} serviceArea - The service area to be removed from the context menu.
540
+ */
541
+ removeServiceAreaFromContextMenu(serviceArea) {
542
+ this.leafletContextmenuManager.removeItemFromContextMenu('map', `Focus Service Area: ${serviceArea.name}`);
543
+ }
544
+
545
+ /**
546
+ * Get a Leaflet layer from the map based on its ID.
547
+ *
548
+ * @param {string} id - The ID of the Leaflet layer to retrieve.
549
+ * @returns {Object|null} The found Leaflet layer or `null` if not found.
550
+ */
551
+ getLeafletLayerById(id) {
552
+ return this.leafletMapManager.getLeafletLayerById(this.leafletMap, id);
553
+ }
554
+
555
+ /**
556
+ * Find a specific Leaflet layer on the map using a callback function.
557
+ *
558
+ * @param {Function} callback - A callback function that defines the condition for finding the layer.
559
+ * @returns {Object|null} The found Leaflet layer or `null` if not found.
560
+ */
561
+ findLeafletLayer(callback) {
562
+ return this.leafletMapManager.findLeafletLayer(this.leafletMap, callback);
563
+ }
564
+
565
+ /**
566
+ * Find an editable layer in the collection by its record ID.
567
+ *
568
+ * @param {Object} record - The record with the ID used for lookup.
569
+ * @returns {Layer|null} The found editable layer, or null if not found.
570
+ * @memberof LiveMapComponent
571
+ */
572
+ getLeafletLayerByRecordId(record) {
573
+ const id = getWithDefault(record, 'id', record);
574
+ let targetLayer = null;
575
+
576
+ this.leafletMap.eachLayer((layer) => {
577
+ // Check if the layer has an ID property
578
+ if (layer.record_id === id) {
579
+ targetLayer = layer;
580
+ }
581
+ });
582
+
583
+ return targetLayer;
207
584
  }
208
585
 
209
586
  /**
210
- * ----------------------------------------------------------------------------
211
- * TRACKED LAYER UTILITIES
212
- * ----------------------------------------------------------------------------
587
+ * Push an editable layer to the collection of editable layers.
213
588
  *
214
- * Functions provide utility for managing tracked editable layers.
589
+ * @param {Layer} layer - The layer to be added to the collection.
590
+ * @memberof LiveMapComponent
215
591
  */
216
- @action pushEditableLayer(layer) {
592
+ pushEditableLayer(layer) {
217
593
  if (!this.editableLayers.includes(layer)) {
218
594
  this.editableLayers.pushObject(layer);
219
595
  }
220
596
  }
221
597
 
222
- @action removeEditableLayerByRecordId(record) {
223
- const index = this.editableLayers.findIndex((layer) => layer.record_id === record?.id ?? record);
598
+ /**
599
+ * Remove an editable layer from the collection by its record ID.
600
+ *
601
+ * @param {Object} record - The record with the ID used for removal.
602
+ * @memberof LiveMapComponent
603
+ */
604
+ removeEditableLayerByRecordId(record) {
605
+ const id = getWithDefault(record, 'id', record);
606
+ const index = this.editableLayers.findIndex((layer) => layer.record_id === id);
224
607
  const layer = this.editableLayers.objectAt(index);
225
608
 
226
- this.drawFeatureGroup?.addLayer(layer);
227
- this.editableLayers.removeAt(index);
609
+ if (this.drawFeatureGroup) {
610
+ this.drawFeatureGroup.addLayer(layer);
611
+ this.editableLayers.removeAt(index);
612
+ }
228
613
  }
229
614
 
230
- @action findEditableLayerByRecordId(record) {
231
- return this.editableLayers.find((layer) => layer.record_id === record?.id ?? record);
615
+ /**
616
+ * Find an editable layer in the collection by its record ID.
617
+ *
618
+ * @param {Object} record - The record with the ID used for lookup.
619
+ * @returns {Layer|null} The found editable layer, or null if not found.
620
+ * @memberof LiveMapComponent
621
+ */
622
+ findEditableLayerByRecordId(record) {
623
+ const id = getWithDefault(record, 'id', record);
624
+ return this.editableLayers.find((layer) => layer.record_id === id);
232
625
  }
233
626
 
234
- @action peekRecordForLayer(layer) {
627
+ /**
628
+ * Peek a record for a given layer by its record ID and type.
629
+ *
630
+ * @param {Layer} layer - The layer associated with a record.
631
+ * @returns {Object|null} The peeked record, or null if not found.
632
+ * @memberof LiveMapComponent
633
+ */
634
+ peekRecordForLayer(layer) {
235
635
  if (layer.record_id && layer.record_type) {
236
636
  return this.store.peekRecord(dasherize(layer.record_type), layer.record_id);
237
637
  }
@@ -240,27 +640,22 @@ export default class LiveMapComponent extends Component {
240
640
  }
241
641
 
242
642
  /**
243
- * ----------------------------------------------------------------------------
244
- * LAYER EVENTS
245
- * ----------------------------------------------------------------------------
643
+ * Handle the 'drawstop' event.
246
644
  *
247
- * Functions that are only triggered from the `LiveMap` Leaflet Layer Component callbacks.
645
+ * @param {Event} event - The 'drawstop' event object.
646
+ * @param {Layer} layer - The layer associated with the event.
647
+ * @memberof LiveMapComponent
248
648
  */
249
-
250
- @action onAction(actionName, ...params) {
251
- if (typeof this[actionName] === 'function') {
252
- this[actionName](...params);
253
- }
254
-
255
- if (typeof this.args[actionName] === 'function') {
256
- this.args[actionName](...params);
257
- }
258
- }
259
-
260
649
  @action onDrawDrawstop(event, layer) {
261
650
  this.serviceAreas.createGenericLayer(event, layer);
262
651
  }
263
652
 
653
+ /**
654
+ * Handle the 'deleted' event for drawn elements.
655
+ *
656
+ * @param {Event} event - The 'deleted' event object.
657
+ * @memberof LiveMapComponent
658
+ */
264
659
  @action onDrawDeleted(event) {
265
660
  /** @var {L.LayerGroup} layers */
266
661
  const { layers } = event;
@@ -278,6 +673,12 @@ export default class LiveMapComponent extends Component {
278
673
  });
279
674
  }
280
675
 
676
+ /**
677
+ * Handle the 'edited' event for drawn elements.
678
+ *
679
+ * @param {Event} event - The 'edited' event object.
680
+ * @memberof LiveMapComponent
681
+ */
281
682
  @action onDrawEdited(event) {
282
683
  /** @var {L.LayerGroup} layers */
283
684
  const { layers } = event;
@@ -301,89 +702,219 @@ export default class LiveMapComponent extends Component {
301
702
  allSettled(requests);
302
703
  }
303
704
 
705
+ /**
706
+ * Handle the addition of a service area layer.
707
+ *
708
+ * @param {ServiceAreaModel} serviceArea - The service area object.
709
+ * @param {Event} event - The event object associated with the addition.
710
+ * @memberof LiveMapComponent
711
+ */
304
712
  @action onServiceAreaLayerAdded(serviceArea, event) {
305
713
  const { target } = event;
306
714
 
307
715
  set(target, 'record_id', serviceArea.id);
308
716
  set(target, 'record_type', 'service-area');
309
717
 
310
- // add to draw feature group
311
- this.drawFeatureGroup?.addLayer(target);
718
+ // set the layer instance to the serviceArea model
719
+ set(serviceArea, '_layer', target);
720
+
721
+ if (this.drawFeatureGroup) {
722
+ // add to draw feature group
723
+ this.drawFeatureGroup.addLayer(target);
724
+ }
312
725
 
313
726
  // this.flyToBoundsOnly(target);
314
- this.createContextMenuForServiceArea(serviceArea, target);
727
+ this.createServiceAreaContextMenu(serviceArea, target);
315
728
  this.pushEditableLayer(target);
316
729
  }
317
730
 
731
+ /**
732
+ * Handle the addition of a zone layer.
733
+ *
734
+ * @param {ZoneModel} zone - The zone object.
735
+ * @param {Event} event - The event object associated with the addition.
736
+ * @memberof LiveMapComponent
737
+ */
318
738
  @action onZoneLayerAdd(zone, event) {
319
739
  const { target } = event;
320
740
 
321
741
  set(target, 'record_id', zone.id);
322
742
  set(target, 'record_type', 'zone');
323
743
 
324
- // add to draw feature group
325
- this.drawFeatureGroup?.addLayer(target);
744
+ // set the layer instance to the zone model
745
+ set(zone, '_layer', target);
326
746
 
327
- this.createContextMenuForZone(zone, target);
747
+ if (this.drawFeatureGroup) {
748
+ // add to draw feature group
749
+ this.drawFeatureGroup.addLayer(target);
750
+ }
751
+
752
+ this.createZoneContextMenu(zone, target);
328
753
  this.pushEditableLayer(target);
329
754
  }
330
755
 
756
+ /**
757
+ * Handle the creation of the draw feature group.
758
+ *
759
+ * @param {DrawFeatureGroup} drawFeatureGroup - The draw feature group instance.
760
+ * @memberof LiveMapComponent
761
+ */
331
762
  @action onDrawFeatureGroupCreated(drawFeatureGroup) {
332
763
  this.drawFeatureGroup = drawFeatureGroup;
333
764
  }
334
765
 
335
- @action onDriverAdded(driver, event) {
336
- const { target } = event;
766
+ /**
767
+ * Handle the addition of a driver marker.
768
+ *
769
+ * @param {DriverModel} driver - The driver object.
770
+ * @param {Event} event - The event object associated with the addition.
771
+ * @memberof LiveMapComponent
772
+ */
773
+ @action onDriverAdded(driver, event) {
774
+ const { target } = event;
775
+
776
+ set(target, 'record_id', driver.id);
777
+ set(target, 'record_type', 'driver');
778
+
779
+ // set the marker instance to the driver model
780
+ set(driver, '_marker', target);
781
+
782
+ this.createDriverContextMenu(driver, target);
783
+ }
784
+
785
+ /**
786
+ * Handle the click event of a driver marker.
787
+ *
788
+ * @param {DriverModel} driver - The driver object.
789
+ * @param {Event} event - The event object associated with the addition.
790
+ * @memberof LiveMapComponent
791
+ */
792
+ @action onDriverClicked(driver) {
793
+ this.contextPanel.focus(driver);
794
+ }
795
+
796
+ /**
797
+ * Handle the addition of a vehicle marker.
798
+ *
799
+ * @param {VehicleModel} vehicle - The vehicle object.
800
+ * @param {Event} event - The event object associated with the addition.
801
+ * @memberof LiveMapComponent
802
+ */
803
+ @action onVehicleAdded(vehicle, event) {
804
+ const { target } = event;
805
+
806
+ set(target, 'record_id', vehicle.id);
807
+ set(target, 'record_type', 'vehicle');
808
+
809
+ // set the marker instance to the vehicle model
810
+ set(vehicle, '_marker', target);
811
+
812
+ this.createVehicleContextMenu(vehicle, target);
813
+ }
814
+
815
+ /**
816
+ * Handle the click event of a vehicle marker.
817
+ *
818
+ * @param {VehicleModel} vehicle - The vehicle object.
819
+ * @param {Event} event - The event object associated with the addition.
820
+ * @memberof LiveMapComponent
821
+ */
822
+ @action onVehicleClicked(vehicle) {
823
+ this.contextPanel.focus(vehicle);
824
+ }
825
+
826
+ /**
827
+ * Handle the creation of the draw control.
828
+ *
829
+ * @param {DrawControl} drawControl - The draw control instance.
830
+ * @memberof LiveMapComponent
831
+ */
832
+ @action onDrawControlCreated(drawControl) {
833
+ this.drawControl = drawControl;
834
+ }
835
+
836
+ /**
837
+ * Hide the draw controls on the map.
838
+ *
839
+ * @param {Object} [options={}] - Additional options.
840
+ * @param {string|boolean} [options.text] - Text to set for the menu item or `true` to set the default text.
841
+ * @param {function} [options.callback] - A callback function to execute.
842
+ * @memberof LiveMapComponent
843
+ */
844
+ @action hideDrawControls(options = {}) {
845
+ this.hide('drawControls');
337
846
 
338
- // set the marker instance to the driver model
339
- set(driver, '_marker', target);
847
+ const text = getWithDefault(options, 'text');
848
+ const callback = getWithDefault(options, 'callback');
340
849
 
341
- console.log('onDriverAdded()', ...arguments);
850
+ if (typeof callback === 'function') {
851
+ callback();
852
+ }
342
853
 
343
- this.createContextMenuForDriver(driver, target);
344
- }
854
+ if (typeof text === 'string') {
855
+ this.leafletContextmenuManager.changeMenuItemText('map', 'draw controls', text);
856
+ }
345
857
 
346
- @action onDrawControlCreated(drawControl) {
347
- this.drawControl = drawControl;
858
+ if (text === true) {
859
+ this.leafletContextmenuManager.changeMenuItemText('map', 'draw controls', 'Enable draw controls...');
860
+ }
861
+
862
+ if (this.drawControl) {
863
+ this.leafletMap.removeControl(this.drawControl);
864
+ }
348
865
  }
349
866
 
350
867
  /**
351
- * ----------------------------------------------------------------------------
352
- * LEAFLET UTILITIES
353
- * ----------------------------------------------------------------------------
354
- *
355
- * Functions are used to help or utilize on Leaflet Layers/ Controls.
868
+ * Show the draw controls on the map.
356
869
  *
870
+ * @param {Object} [options={}] - Additional options.
871
+ * @param {string|boolean} [options.text] - Text to set for the menu item or `true` to set the default text.
872
+ * @param {function} [options.callback] - A callback function to execute.
873
+ * @memberof LiveMapComponent
357
874
  */
358
- @action removeDrawingControl() {
359
- if (isBlank(this.drawControl)) {
360
- return;
875
+ @action showDrawControls(options = {}) {
876
+ this.show('drawControls');
877
+
878
+ const text = getWithDefault(options, 'text');
879
+ const callback = getWithDefault(options, 'callback');
880
+
881
+ if (typeof callback === 'function') {
882
+ callback();
361
883
  }
362
884
 
363
- this.isDrawControlsVisible = false;
364
- this.leafletMap?.removeControl(this.drawControl);
365
- this.toggleDrawControlContextMenuItem();
366
- }
885
+ if (typeof text === 'string') {
886
+ this.leafletContextmenuManager.changeMenuItemText('map', 'draw controls', text);
887
+ }
367
888
 
368
- // alias for `removeDrawingControl()`
369
- @action hideDrawControls() {
370
- this.removeDrawingControl();
371
- }
889
+ if (text === true) {
890
+ this.leafletContextmenuManager.changeMenuItemText('map', 'draw controls', 'Hide draw controls...');
891
+ }
372
892
 
373
- @action enableDrawControls() {
374
- this.isDrawControlsVisible = true;
375
- this.leafletMap?.addControl(this.drawControl);
376
- this.toggleDrawControlContextMenuItem();
893
+ if (this.drawControl) {
894
+ this.leafletMap.addControl(this.drawControl);
895
+ }
377
896
  }
378
897
 
379
- @action focusLayerByRecord(record) {
380
- const layer = this.findEditableLayerByRecordId(record);
898
+ /**
899
+ * Focus on a layer associated with a record.
900
+ *
901
+ * @param {Object} record - The record to focus on.
902
+ * @memberof LiveMapComponent
903
+ */
904
+ @action focusLayerBoundsByRecord(record) {
905
+ const layer = this.getLeafletLayerByRecordId(record);
381
906
 
382
907
  if (layer) {
383
908
  this.flyToBoundsOnly(layer);
384
909
  }
385
910
  }
386
911
 
912
+ /**
913
+ * Fly to a service area layer on the map.
914
+ *
915
+ * @param {ServiceAreaModel} serviceArea - The service area object to fly to.
916
+ * @memberof LiveMapComponent
917
+ */
387
918
  @action flyToServiceArea(serviceArea) {
388
919
  const layer = this.findEditableLayerByRecordId(serviceArea);
389
920
 
@@ -392,11 +923,12 @@ export default class LiveMapComponent extends Component {
392
923
  }
393
924
  }
394
925
 
395
- // alias for `flyToServiceArea()`
396
- @action jumpToServiceArea(serviceArea) {
397
- return this.flyToServiceArea(serviceArea);
398
- }
399
-
926
+ /**
927
+ * Focus on a service area by activating it and then flying to it on the map.
928
+ *
929
+ * @param {ServiceArea} serviceArea - The service area to focus on.
930
+ * @memberof LiveMapComponent
931
+ */
400
932
  @action focusServiceArea(serviceArea) {
401
933
  this.activateServiceArea(serviceArea);
402
934
 
@@ -409,7 +941,13 @@ export default class LiveMapComponent extends Component {
409
941
  );
410
942
  }
411
943
 
412
- @action blurAllServiceAreas(except = []) {
944
+ /**
945
+ * Blur all service areas except for those specified in the 'except' array.
946
+ *
947
+ * @param {Array} except - An array of records to exclude from blurring.
948
+ * @memberof LiveMapComponent
949
+ */
950
+ blurAllServiceAreas(except = []) {
413
951
  if (!isArray(except)) {
414
952
  except = [];
415
953
  }
@@ -441,7 +979,13 @@ export default class LiveMapComponent extends Component {
441
979
  }
442
980
  }
443
981
 
444
- @action focusAllServiceAreas(except = []) {
982
+ /**
983
+ * Focus on all service areas except for those specified in the 'except' array by activating them.
984
+ *
985
+ * @param {Array} except - An array of records to exclude from activation.
986
+ * @memberof LiveMapComponent
987
+ */
988
+ focusAllServiceAreas(except = []) {
445
989
  if (!isArray(except)) {
446
990
  except = [];
447
991
  }
@@ -463,93 +1007,143 @@ export default class LiveMapComponent extends Component {
463
1007
  }
464
1008
  }
465
1009
 
466
- @action blurServiceArea(serviceArea) {
1010
+ /**
1011
+ * Blur a specific service area by removing it from the active service areas.
1012
+ *
1013
+ * @param {ServiceAreaModel} serviceArea - The service area to blur.
1014
+ * @memberof LiveMapComponent
1015
+ */
1016
+ blurServiceArea(serviceArea) {
467
1017
  if (this.activeServiceAreas.includes(serviceArea)) {
468
1018
  this.activeServiceAreas.removeObject(serviceArea);
469
1019
  }
470
1020
  }
471
1021
 
472
- @action activateServiceArea(serviceArea) {
1022
+ /**
1023
+ * Activate a service area by adding it to the active service areas.
1024
+ *
1025
+ * @param {ServiceAreaModel} serviceArea - The service area to activate.
1026
+ * @memberof LiveMapComponent
1027
+ */
1028
+ activateServiceArea(serviceArea) {
473
1029
  if (!this.activeServiceAreas.includes(serviceArea)) {
474
1030
  this.activeServiceAreas.pushObject(serviceArea);
475
1031
  }
476
1032
  }
477
1033
 
478
- @action hideDrivers() {
479
- this.isDriversVisible = false;
480
- }
481
-
482
- @action showDrivers() {
483
- this.isDriversVisible = true;
484
- }
485
-
486
- @action toggleDrivers() {
487
- this.isDriversVisible = !this.isDriversVisible;
488
- }
489
-
490
- @action hidePlaces() {
491
- this.isPlacesVisible = false;
492
- }
493
-
494
- @action showPlaces() {
495
- this.isPlacesVisible = true;
496
- }
497
-
498
- @action togglePlaces() {
499
- this.isPlacesVisible = !this.isPlacesVisible;
500
- }
501
-
502
- @action hideRoutes() {
503
- this.isRoutesVisible = false;
504
- }
505
-
506
- @action showRoutes() {
507
- this.isRoutesVisible = true;
508
- }
509
-
510
- @action toggleRoutes() {
511
- this.isRoutesVisible = !this.isRoutesVisible;
512
- }
513
-
1034
+ /**
1035
+ * Show coordinates information by displaying them as an info notification.
1036
+ *
1037
+ * @param {Event} event - The event containing latitude and longitude information.
1038
+ * @memberof LiveMapComponent
1039
+ */
514
1040
  @action showCoordinates(event) {
515
1041
  this.notifications.info(event.latlng);
516
1042
  }
517
1043
 
1044
+ /**
1045
+ * Center the map on a specific location provided in the event.
1046
+ *
1047
+ * @param {Event} event - The event containing the target location (latlng).
1048
+ * @memberof LiveMapComponent
1049
+ */
518
1050
  @action centerMap(event) {
519
- this.leafletMap?.panTo(event.latlng);
1051
+ this.leafletMap.panTo(event.latlng);
520
1052
  }
521
1053
 
1054
+ /**
1055
+ * Zoom in on the map.
1056
+ *
1057
+ * @memberof LiveMapComponent
1058
+ */
522
1059
  @action zoomIn() {
523
- this.leafletMap?.zoomIn();
1060
+ this.leafletMap.zoomIn();
524
1061
  }
525
1062
 
1063
+ /**
1064
+ * Zoom out on the map.
1065
+ *
1066
+ * @memberof LiveMapComponent
1067
+ */
526
1068
  @action zoomOut() {
527
- this.leafletMap?.zoomOut();
1069
+ this.leafletMap.zoomOut();
1070
+ }
1071
+
1072
+ /**
1073
+ * Set the maximum bounds of the map based on the provided layer's bounds.
1074
+ *
1075
+ * @param {Layer} layer - The layer used to determine the map's maximum bounds.
1076
+ * @memberof LiveMapComponent
1077
+ */
1078
+ setMaxBoundsFromLayer(layer) {
1079
+ if (layer && typeof layer.getBounds === 'function') {
1080
+ const bounds = layer.getBounds();
1081
+
1082
+ this.leafletMap.flyToBounds(bounds);
1083
+ this.leafletMap.setMaxBounds(bounds);
1084
+ }
528
1085
  }
529
1086
 
530
- @action setMaxBoundsFromLayer(layer) {
531
- const bounds = layer?.getBounds();
1087
+ /**
1088
+ * Fly to and focus on a specific layer's bounds on the map.
1089
+ *
1090
+ * @param {Layer} layer - The layer to focus on.
1091
+ * @memberof LiveMapComponent
1092
+ */
1093
+ flyToBoundsOnly(layer) {
1094
+ if (layer && typeof layer.getBounds === 'function') {
1095
+ const bounds = layer.getBounds();
532
1096
 
533
- this.leafletMap?.flyToBounds(bounds);
534
- this.leafletMap?.setMaxBounds(bounds);
1097
+ this.leafletMap.flyToBounds(bounds);
1098
+ }
535
1099
  }
536
1100
 
537
- @action flyToBoundsOnly(layer) {
538
- const bounds = layer?.getBounds();
1101
+ /**
1102
+ * Focus on a specific layer and optionally zoom in/out on it.
1103
+ *
1104
+ * @param {Layer} layer - The layer to focus on.
1105
+ * @param {number} zoom - The zoom level for the focus operation.
1106
+ * @param {Object} options - Additional options for the focus operation.
1107
+ * @memberof LiveMapComponent
1108
+ */
1109
+ @action focusLayer(layer, zoom, options = {}) {
1110
+ this.leafletMapManager.flyToLayer(this.leafletMap, layer, zoom, options);
539
1111
 
540
- this.leafletMap?.flyToBounds(bounds);
1112
+ if (typeof options.onAfterFocus === 'function') {
1113
+ options.onAfterFocus(layer);
1114
+ }
541
1115
  }
542
1116
 
543
1117
  /**
544
- * ----------------------------------------------------------------------------
545
- * CONTEXT MENU INITIALIZERS
546
- * ----------------------------------------------------------------------------
1118
+ * Focuses the Leaflet map on a specific layer associated with a record.
547
1119
  *
548
- * Functions that are used to build context menu for layers and controls on the map.
1120
+ * @param {Object} record - The record associated with the target layer.
1121
+ * @param {number} zoom - The desired zoom level for the map.
1122
+ * @param {Object} [options={}] - Additional options for the map focus.
1123
+ * @returns {void}
549
1124
  *
1125
+ * @example
1126
+ * focusLayerByRecord(recordData, 12, { animate: true });
550
1127
  */
1128
+ @action focusLayerByRecord(record, zoom, options = {}) {
1129
+ const layer = this.getLeafletLayerByRecordId(record);
1130
+
1131
+ if (layer) {
1132
+ this.focusLayer(layer, zoom, options);
1133
+ }
1134
+
1135
+ if (typeof options.onAfterFocusWithRecord === 'function') {
1136
+ options.onAfterFocusWithRecord(record, layer);
1137
+ }
1138
+ }
551
1139
 
552
- @action buildContextMenuItems() {
1140
+ /**
1141
+ * Create a context menu for the map with various options.
1142
+ *
1143
+ * @param {L.Map} map - The map to which the context menu is attached.
1144
+ * @memberof LiveMapComponent
1145
+ */
1146
+ @action createMapContextMenu(map) {
553
1147
  const contextmenuItems = [
554
1148
  {
555
1149
  text: 'Show coordinates...',
@@ -572,8 +1166,8 @@ export default class LiveMapComponent extends Component {
572
1166
  index: 3,
573
1167
  },
574
1168
  {
575
- text: this.isDrawControlsVisible ? `Hide draw controls...` : `Enable draw controls...`,
576
- callback: () => (this.isDrawControlsVisible ? this.hideDrawControls() : this.enableDrawControls()),
1169
+ text: this.isVisible('drawControls') ? `Hide draw controls...` : `Enable draw controls...`,
1170
+ callback: this.toggleDrawControlContextMenuItem.bind(this),
577
1171
  index: 4,
578
1172
  },
579
1173
  {
@@ -586,67 +1180,159 @@ export default class LiveMapComponent extends Component {
586
1180
  },
587
1181
  ];
588
1182
 
589
- // add service areas to context menu
590
- const serviceAreas = this.serviceAreas.getFromCache() ?? [];
1183
+ // Add Service Area Context Menu Items
1184
+ const serviceAreas = this.serviceAreas.getFromCache();
591
1185
 
592
- if (serviceAreas?.length > 0) {
1186
+ if (isArray(serviceAreas)) {
593
1187
  contextmenuItems.pushObject({
594
1188
  separator: true,
595
1189
  });
596
- }
597
1190
 
598
- // add to context menu
599
- for (let i = 0; i < serviceAreas?.length; i++) {
600
- const serviceArea = serviceAreas.objectAt(i);
1191
+ // Add for each Service Area
1192
+ for (let i = 0; i < serviceAreas.length; i++) {
1193
+ const serviceArea = serviceAreas.objectAt(i);
1194
+ const nextIndex = contextmenuItems.length + 2;
601
1195
 
602
- contextmenuItems.pushObject({
603
- text: `Focus Service Area: ${serviceArea.name}`,
604
- callback: () => this.focusServiceArea(serviceArea),
605
- index: (contextmenuItems.lastObject?.index ?? 0) + 1 + i,
606
- });
1196
+ contextmenuItems.pushObject({
1197
+ text: `Focus Service Area: ${serviceArea.name}`,
1198
+ callback: () => this.focusServiceArea(serviceArea),
1199
+ index: nextIndex,
1200
+ });
1201
+ }
607
1202
  }
608
1203
 
609
- this.currentContextMenuItems = contextmenuItems;
1204
+ // create contextmenu registry
1205
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu('map', map, contextmenuItems);
1206
+
1207
+ // trigger that contextmenu registry was created
1208
+ this.universe.createRegistryEvent('contextmenu:map', 'created', contextmenuRegistry, this.leafletContextmenuManager);
610
1209
 
611
- return contextmenuItems;
1210
+ return contextmenuRegistry;
1211
+ }
1212
+
1213
+ /**
1214
+ * Rebuild the context menu for the map.
1215
+ * This function calls the `createMapContextMenu` method.
1216
+ * @memberof LiveMapComponent
1217
+ */
1218
+ rebuildMapContextMenu() {
1219
+ this.createMapContextMenu(this.leafletMap);
612
1220
  }
613
1221
 
614
- @action createContextMenuForDriver(driver, layer) {
1222
+ /**
1223
+ * Create a context menu for a driver marker on the map.
1224
+ *
1225
+ * @param {DriverModel} driver - The driver associated with the marker.
1226
+ * @param {L.Layer} layer - The layer representing the driver marker.
1227
+ * @memberof LiveMapComponent
1228
+ */
1229
+ @action createDriverContextMenu(driver, layer) {
615
1230
  const contextmenuItems = [
616
1231
  {
617
1232
  separator: true,
618
1233
  },
619
1234
  {
620
1235
  text: `View Driver: ${driver.name}`,
621
- // callback: () => this.editServiceAreaDetails(serviceArea)
1236
+ callback: () => this.contextPanel.focus(driver),
622
1237
  },
623
1238
  {
624
1239
  text: `Edit Driver: ${driver.name}`,
625
- // callback: () => this.editServiceAreaDetails(serviceArea)
1240
+ callback: () => this.contextPanel.focus(driver, 'editing'),
626
1241
  },
627
1242
  {
628
1243
  text: `Delete Driver: ${driver.name}`,
629
- // callback: () => this.deleteServiceArea(serviceArea)
1244
+ callback: () => this.crud.delete(driver),
1245
+ },
1246
+ {
1247
+ text: `View Vehicle for: ${driver.name}`,
1248
+ callback: () => this.contextPanel.focus(driver.vehicle),
1249
+ },
1250
+ ];
1251
+
1252
+ // append items from universe registry
1253
+ const registeredContextMenuItems = this.universe.getMenuItemsFromRegistry('contextmenu:driver');
1254
+ if (isArray(registeredContextMenuItems)) {
1255
+ contextmenuItems = [
1256
+ ...contextmenuItems,
1257
+ ...registeredContextMenuItems.map((menuItem) => {
1258
+ return {
1259
+ text: menuItem.title,
1260
+ callback: () => {
1261
+ return menuItem.onClick(driver, layer, menuItem);
1262
+ },
1263
+ };
1264
+ }),
1265
+ ];
1266
+ }
1267
+
1268
+ // create contextmenu registry
1269
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`driver:${driver.public_id}`, layer, contextmenuItems, { driver });
1270
+
1271
+ // trigger that contextmenu registry was created
1272
+ this.universe.createRegistryEvent('contextmenu:driver', 'created', contextmenuRegistry, this.leafletContextmenuManager);
1273
+
1274
+ return contextmenuRegistry;
1275
+ }
1276
+
1277
+ /**
1278
+ * Create a context menu for a vehicle marker on the map.
1279
+ *
1280
+ * @param {Vehicle} vehicle - The vehicle associated with the marker.
1281
+ * @param {Layer} layer - The layer representing the vehicle marker.
1282
+ * @memberof LiveMapComponent
1283
+ */
1284
+ @action createVehicleContextMenu(vehicle, layer) {
1285
+ let contextmenuItems = [
1286
+ {
1287
+ separator: true,
630
1288
  },
631
1289
  {
632
- text: `Assign Order to Driver: ${driver.name}`,
633
- // callback: () => this.deleteServiceArea(serviceArea)
1290
+ text: `View Vehicle: ${vehicle.displayName}`,
1291
+ callback: () => this.contextPanel.focus(vehicle),
634
1292
  },
635
1293
  {
636
- text: `View Vehicle for: ${driver.name}`,
637
- // callback: () => this.deleteServiceArea(serviceArea)
1294
+ text: `Edit Vehicle: ${vehicle.displayName}`,
1295
+ callback: () => this.contextPanel.focus(vehicle, 'editing'),
1296
+ },
1297
+ {
1298
+ text: `Delete Vehicle: ${vehicle.displayName}`,
1299
+ callback: () => this.crud.delete(vehicle),
638
1300
  },
639
1301
  ];
640
1302
 
641
- if (typeof layer?.bindContextMenu === 'function') {
642
- layer.bindContextMenu({
643
- contextmenu: true,
644
- contextmenuItems,
645
- });
1303
+ // append items from universe registry
1304
+ const registeredContextMenuItems = this.universe.getMenuItemsFromRegistry('contextmenu:vehicle');
1305
+ if (isArray(registeredContextMenuItems)) {
1306
+ contextmenuItems = [
1307
+ ...contextmenuItems,
1308
+ ...registeredContextMenuItems.map((menuItem) => {
1309
+ return {
1310
+ text: menuItem.title,
1311
+ callback: () => {
1312
+ return menuItem.onClick(vehicle, layer, menuItem);
1313
+ },
1314
+ };
1315
+ }),
1316
+ ];
646
1317
  }
1318
+
1319
+ // create contextmenu registry
1320
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`vehicle:${vehicle.public_id}`, layer, contextmenuItems, { vehicle });
1321
+
1322
+ // trigger that contextmenu registry was created
1323
+ this.universe.createRegistryEvent('contextmenu:vehicle', 'created', contextmenuRegistry, this.leafletContextmenuManager);
1324
+
1325
+ return contextmenuRegistry;
647
1326
  }
648
1327
 
649
- @action createContextMenuForZone(zone, layer) {
1328
+ /**
1329
+ * Create a context menu for a zone layer on the map.
1330
+ *
1331
+ * @param {ZoneModel} zone - The zone associated with the layer.
1332
+ * @param {Layer} layer - The layer representing the zone.
1333
+ * @memberof LiveMapComponent
1334
+ */
1335
+ @action createZoneContextMenu(zone, layer) {
650
1336
  const contextmenuItems = [
651
1337
  {
652
1338
  separator: true,
@@ -670,15 +1356,23 @@ export default class LiveMapComponent extends Component {
670
1356
  },
671
1357
  ];
672
1358
 
673
- if (typeof layer?.bindContextMenu === 'function') {
674
- layer.bindContextMenu({
675
- contextmenu: true,
676
- contextmenuItems,
677
- });
678
- }
1359
+ // create contextmenu registry
1360
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`zone:${zone.public_id}`, layer, contextmenuItems, { zone });
1361
+
1362
+ // trigger that contextmenu registry was created
1363
+ this.universe.createRegistryEvent('contextmenu:zone', 'created', contextmenuRegistry, this.leafletContextmenuManager);
1364
+
1365
+ return contextmenuRegistry;
679
1366
  }
680
1367
 
681
- @action createContextMenuForServiceArea(serviceArea, layer) {
1368
+ /**
1369
+ * Create a context menu for a service area layer on the map.
1370
+ *
1371
+ * @param {ServiceAreaModel} serviceArea - The service area associated with the layer.
1372
+ * @param {Layer} layer - The layer representing the service area.
1373
+ * @memberof LiveMapComponent
1374
+ */
1375
+ @action createServiceAreaContextMenu(serviceArea, layer) {
682
1376
  const contextmenuItems = [
683
1377
  {
684
1378
  separator: true,
@@ -704,30 +1398,32 @@ export default class LiveMapComponent extends Component {
704
1398
  callback: () =>
705
1399
  this.serviceAreas.deleteServiceArea(serviceArea, {
706
1400
  onFinish: () => {
707
- this.rebuildContextMenu();
1401
+ this.rebuildMapContextMenu();
708
1402
  this.removeEditableLayerByRecordId(serviceArea);
709
1403
  },
710
1404
  }),
711
1405
  },
712
1406
  ];
713
1407
 
714
- if (typeof layer?.bindContextMenu === 'function') {
715
- layer.bindContextMenu({
716
- contextmenu: true,
717
- contextmenuItems,
718
- });
719
- }
1408
+ // create contextmenu registry
1409
+ const contextmenuRegistry = this.leafletContextmenuManager.createContextMenu(`service-area:${serviceArea.public_id}`, layer, contextmenuItems, { serviceArea });
1410
+
1411
+ // trigger that contextmenu registry was created
1412
+ this.universe.createRegistryEvent('contextmenu:service-area', 'created', contextmenuRegistry, this.leafletContextmenuManager);
1413
+
1414
+ return contextmenuRegistry;
720
1415
  }
721
1416
 
722
1417
  /**
723
- * ----------------------------------------------------------------------------
724
- * Async/Socket Functions
725
- * ----------------------------------------------------------------------------
1418
+ * Listens for events on the company channel and logs incoming data.
726
1419
  *
727
- * Functions are used to fetch date or handle socket callbacks/initializations.
1420
+ * This function sets up a WebSocket connection, subscribes to the company-specific channel,
1421
+ * and listens for events. When events are received, it logs them to the console.
728
1422
  *
1423
+ * @async
1424
+ * @function
729
1425
  */
730
- @action async listenForOrders() {
1426
+ async listen() {
731
1427
  // setup socket
732
1428
  const socket = this.socket.instance();
733
1429
 
@@ -751,26 +1447,49 @@ export default class LiveMapComponent extends Component {
751
1447
  })();
752
1448
  }
753
1449
 
754
- @action watchDrivers(drivers = []) {
755
- // setup socket
1450
+ /**
1451
+ * Watches and manages moving objects (e.g., drivers or vehicles) on the LiveMapComponent.
1452
+ *
1453
+ * This function can be used to watch different types of moving objects by specifying
1454
+ * the 'type' parameter.
1455
+ *
1456
+ * @action
1457
+ * @function
1458
+ * @param {string} objectType - The type of moving object to watch (e.g., 'drivers', 'vehicles').
1459
+ * @param {Array} movingObjects - An array of moving objects to watch.
1460
+ */
1461
+ watchMovingObjects(objectType, objects = []) {
1462
+ // Setup socket
756
1463
  const socket = this.socket.instance();
757
1464
 
758
- // listen for stivers
759
- for (let i = 0; i < drivers.length; i++) {
760
- const driver = drivers.objectAt(i);
761
- this.listenForDriver(driver, socket);
1465
+ // Listen for moving objects
1466
+ for (let i = 0; i < objects.length; i++) {
1467
+ const movingObject = objects.objectAt(i);
1468
+ this.listenForMovingObject(objectType, movingObject, socket);
762
1469
  }
763
1470
  }
764
1471
 
765
- @action async listenForDriver(driver, socket) {
766
- // listen on company channel
767
- const channelId = `driver.${driver.id}`;
1472
+ /**
1473
+ * Listens for events related to a specific type of moving object (e.g., driver or vehicle) and manages the associated marker.
1474
+ *
1475
+ * This function subscribes to the channel corresponding to the provided 'objectType' and the specific 'movingObject'
1476
+ * to listen for location-related events. It processes and updates the associated marker when events are received.
1477
+ *
1478
+ * @async
1479
+ * @function
1480
+ * @param {string} objectType - The type of moving object being watched (e.g., 'drivers', 'vehicles').
1481
+ * @param {Object} movingObject - The specific moving object to track.
1482
+ * @param {Socket} socket - The WebSocket instance used for communication.
1483
+ */
1484
+ async listenForMovingObject(objectType, movingObject, socket) {
1485
+ // Listen on the specific channel
1486
+ const channelId = `${objectType}.${movingObject.id}`;
768
1487
  const channel = socket.subscribe(channelId);
769
1488
 
770
- // track channel
1489
+ // Track the channel
771
1490
  this.channels.pushObject(channel);
772
1491
 
773
- // listen to channel for events
1492
+ // Listen to the channel for events
774
1493
  await channel.listener('subscribe').once();
775
1494
 
776
1495
  // Initialize an empty buffer to store incoming events
@@ -787,11 +1506,19 @@ export default class LiveMapComponent extends Component {
787
1506
  // Process sorted events
788
1507
  for (const output of eventBuffer) {
789
1508
  const { event, data } = output;
1509
+
1510
+ // log incoming event
790
1511
  console.log(`${event} - #${data.additionalData.index} (${output.created_at}) [ ${data.location.coordinates.join(' ')} ]`);
791
- // update driver heading degree
792
- driver._marker.setRotationAngle(data.heading);
793
- // move driver's marker to new coordinates
794
- driver._marker.slideTo(data.location.coordinates, { duration: 2000 });
1512
+
1513
+ // get movingObject marker
1514
+ const objectMarker = movingObject._layer;
1515
+
1516
+ if (objectMarker) {
1517
+ // Update the object's heading degree
1518
+ objectMarker.setRotationAngle(data.heading);
1519
+ // Move the object's marker to new coordinates
1520
+ objectMarker.slideTo(data.location.coordinates, { duration: 2000 });
1521
+ }
795
1522
  }
796
1523
 
797
1524
  // Clear the buffer
@@ -801,12 +1528,12 @@ export default class LiveMapComponent extends Component {
801
1528
  // Start a timer to process the buffer at intervals
802
1529
  setInterval(processBuffer, bufferTime);
803
1530
 
804
- // get incoming data and console out
1531
+ // Get incoming data and console out
805
1532
  (async () => {
806
1533
  for await (let output of channel) {
807
1534
  const { event } = output;
808
1535
 
809
- if (event === 'driver.location_changed' || event === 'driver.simulated_location_changed') {
1536
+ if (event === `${objectType}.location_changed` || event === `${objectType}.simulated_location_changed`) {
810
1537
  // Add the incoming event to the buffer
811
1538
  eventBuffer.push(output);
812
1539
  }
@@ -814,15 +1541,27 @@ export default class LiveMapComponent extends Component {
814
1541
  })();
815
1542
  }
816
1543
 
1544
+ /**
1545
+ * Close all socket channels associated subscribed to.
1546
+ * @memberof LiveMapComponent
1547
+ */
817
1548
  @action closeChannels() {
818
- for (let i = 0; i < this.channels.length; i++) {
819
- const channel = this.channels.objectAt(i);
1549
+ if (isArray(this.channels)) {
1550
+ for (let i = 0; i < this.channels.length; i++) {
1551
+ const channel = this.channels.objectAt(i);
820
1552
 
821
- channel.close();
1553
+ channel.close();
1554
+ }
822
1555
  }
823
1556
  }
824
1557
 
825
- @action getInitialCoordinates() {
1558
+ /**
1559
+ * Retrieve the initial coordinates for the map view.
1560
+ *
1561
+ * @returns {Promise} A promise that resolves to an object containing latitude and longitude.
1562
+ * @memberof LiveMapComponent
1563
+ */
1564
+ getInitialCoordinates() {
826
1565
  const initialCoordinates = {
827
1566
  latitude: DEFAULT_LATITUDE,
828
1567
  longitude: DEFAULT_LONGITUDE,
@@ -880,73 +1619,13 @@ export default class LiveMapComponent extends Component {
880
1619
  });
881
1620
  }
882
1621
 
883
- @action fetchActiveRoutes() {
884
- this.isLoading = true;
885
-
886
- return new Promise((resolve) => {
887
- this.fetch
888
- .get('fleet-ops/live/routes')
889
- .then((routes) => {
890
- this.isLoading = false;
891
-
892
- if (typeof this.args.onRoutesLoaded === 'function') {
893
- this.args.onRoutesLoaded(routes);
894
- }
895
-
896
- resolve(routes);
897
- })
898
- .catch(() => {
899
- resolve([]);
900
- });
901
- });
902
- }
903
-
904
- @action fetchActiveDrivers() {
905
- this.isLoading = true;
906
-
907
- return new Promise((resolve) => {
908
- this.fetch
909
- .get('fleet-ops/live/drivers', {}, { normalizeToEmberData: true, normalizeModelType: 'driver' })
910
- .then((drivers) => {
911
- this.isLoading = false;
912
-
913
- if (typeof this.args.onDriversLoaded === 'function') {
914
- this.args.onDriversLoaded(drivers);
915
- }
916
-
917
- resolve(drivers);
918
- })
919
- .catch(() => {
920
- resolve([]);
921
- });
922
- });
923
- }
924
-
925
- @action fetchActivePlaces() {
926
- this.isLoading = true;
927
-
928
- // get the center of map
929
- const center = this.leafletMap.getCenter();
930
-
931
- return new Promise((resolve) => {
932
- this.fetch
933
- .get('fleet-ops/live/places', { within: center }, { normalizeToEmberData: true, normalizeModelType: 'place' })
934
- .then((places) => {
935
- this.isLoading = false;
936
-
937
- if (typeof this.args.onPlacesLoaded === 'function') {
938
- this.args.onPlacesLoaded(places);
939
- }
940
-
941
- resolve(places);
942
- })
943
- .catch(() => {
944
- resolve([]);
945
- });
946
- });
947
- }
948
-
949
- @action fetchServiceAreas() {
1622
+ /**
1623
+ * Fetch service areas and cache them if not already cached.
1624
+ *
1625
+ * @returns {Promise} A promise that resolves to an array of service area records.
1626
+ * @memberof LiveMapComponent
1627
+ */
1628
+ fetchServiceAreas() {
950
1629
  this.isLoading = true;
951
1630
 
952
1631
  return new Promise((resolve) => {
@@ -968,14 +1647,26 @@ export default class LiveMapComponent extends Component {
968
1647
  });
969
1648
  }
970
1649
 
971
- @action getLocalLatitude() {
1650
+ /**
1651
+ * Get the local latitude for the map view.
1652
+ *
1653
+ * @returns {number} The local latitude.
1654
+ * @memberof LiveMapComponent
1655
+ */
1656
+ getLocalLatitude() {
972
1657
  const whois = this.currentUser.getOption('whois');
973
1658
  const latitude = this.appCache.get('map_latitude');
974
1659
 
975
1660
  return latitude || whois?.latitude || DEFAULT_LATITUDE;
976
1661
  }
977
1662
 
978
- @action getLocalLongitude() {
1663
+ /**
1664
+ * Get the local longitude for the map view.
1665
+ *
1666
+ * @returns {number} The local longitude.
1667
+ * @memberof LiveMapComponent
1668
+ */
1669
+ getLocalLongitude() {
979
1670
  const whois = this.currentUser.getOption('whois');
980
1671
  const longitude = this.appCache.get('map_longitude');
981
1672