@fleetbase/fleetops-engine 0.2.8 → 0.3.0

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