@aggdirect/coolmap-services 1.4.8 → 4.0.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.
@@ -0,0 +1,1339 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Component, InjectionToken, inject, NgZone, Injectable, signal } from '@angular/core';
3
+ import { BehaviorSubject } from 'rxjs';
4
+ import mapboxgl from 'mapbox-gl';
5
+ import * as turf from '@turf/turf';
6
+ import { HttpClient } from '@angular/common/http';
7
+
8
+ class CoolmapServices {
9
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CoolmapServices, deps: [], target: i0.ɵɵFactoryTarget.Component });
10
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: CoolmapServices, isStandalone: true, selector: "lib-coolmap-services", ngImport: i0, template: `
11
+ <p>
12
+ coolmap-services works!
13
+ </p>
14
+ `, isInline: true, styles: [""] });
15
+ }
16
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CoolmapServices, decorators: [{
17
+ type: Component,
18
+ args: [{ selector: 'lib-coolmap-services', imports: [], template: `
19
+ <p>
20
+ coolmap-services works!
21
+ </p>
22
+ ` }]
23
+ }] });
24
+
25
+ const COOLMAP_CONFIG = new InjectionToken('COOLMAP_CONFIG');
26
+ function provideCoolmapConfig(config) {
27
+ return {
28
+ provide: COOLMAP_CONFIG,
29
+ useValue: config
30
+ };
31
+ }
32
+
33
+ class UtilsService {
34
+ http = inject(HttpClient);
35
+ config = inject(COOLMAP_CONFIG);
36
+ ngZone = inject(NgZone);
37
+ // BehaviorSubjects for state (converting to simple Signals later where practical)
38
+ preventnavChange = new BehaviorSubject(false);
39
+ navChangeObserve = this.preventnavChange.asObservable();
40
+ clearViewRouteforJobCode = new BehaviorSubject(false);
41
+ clearViewRouteforJobCodeObserve = this.clearViewRouteforJobCode.asObservable();
42
+ preVentJobdetailclose = new BehaviorSubject(false);
43
+ getpreVentJobdetailclose = this.preVentJobdetailclose.asObservable();
44
+ routeDetailsUtility = new BehaviorSubject({});
45
+ getrouteDetailsUtility = this.routeDetailsUtility.asObservable();
46
+ removeMapEntity = new BehaviorSubject({});
47
+ removeMapEntityUtility = this.removeMapEntity.asObservable();
48
+ dict = new Map();
49
+ unitsList;
50
+ materialsList;
51
+ materialsListForCustomer;
52
+ customersList;
53
+ locationList;
54
+ // Options filtering properties
55
+ pickupOptions = [];
56
+ destOptions = [];
57
+ ownerOptions = [];
58
+ customerOptions = [];
59
+ unitOptions = [];
60
+ materialOptions = [];
61
+ jcodeOptions = [];
62
+ driverOption = [];
63
+ truckingCompanayOption = [];
64
+ routeNameOptions = [];
65
+ getDateFormat(strVal, seprater) {
66
+ seprater = seprater ? seprater : '-';
67
+ const mydate = strVal;
68
+ if (!mydate)
69
+ return '';
70
+ const month = String(mydate.getMonth() + 1).padStart(2, '0');
71
+ const day = String(mydate.getDate()).padStart(2, '0');
72
+ return (mydate.getFullYear() +
73
+ seprater +
74
+ month +
75
+ seprater +
76
+ day);
77
+ }
78
+ getData(path) {
79
+ return this.http.get(`${this.config.analyticsRESTURL}${this.config.repository}/${path}`);
80
+ }
81
+ getRestData(path) {
82
+ return this.http.get(`${this.config.RESTURLPrefix}${path}`);
83
+ }
84
+ postdata(path, data) {
85
+ return this.http.post(`${this.config.analyticsRESTURL}${this.config.repository}/${path}`, data);
86
+ }
87
+ postDataWithRestUrl(path, data) {
88
+ return this.http.post(`${this.config.RESTURLPrefix}${path}`, data);
89
+ }
90
+ // Instead of MatSnackBar, we just use a generic console/alert fallback for now.
91
+ openSnackBar(message, className) {
92
+ }
93
+ clearOptions() {
94
+ this.pickupOptions = [];
95
+ this.destOptions = [];
96
+ this.ownerOptions = [];
97
+ this.customerOptions = [];
98
+ this.unitOptions = [];
99
+ this.materialOptions = [];
100
+ this.jcodeOptions = [];
101
+ this.routeNameOptions = [];
102
+ }
103
+ makeOptions(item) {
104
+ // Porting V1 makeOptions safely
105
+ if (item.order_number) {
106
+ if (this.jcodeOptions.findIndex((elem) => elem.job_id === item.job_id) === -1) {
107
+ this.jcodeOptions.push({ job_code: item.order_number, job_id: item.job_id });
108
+ }
109
+ // Assuming tasks structure if Mapbox Route logic was merged, else driver_list from V1
110
+ const driver_list = item.driver_list || [];
111
+ if (driver_list.length > 0) {
112
+ driver_list.forEach((driver) => {
113
+ if (this.driverOption.findIndex((elem) => elem === driver.driver_name) === -1) {
114
+ this.driverOption.push(driver.driver_name);
115
+ }
116
+ if (this.truckingCompanayOption.findIndex((elem) => elem === driver.trucking_company) === -1) {
117
+ this.truckingCompanayOption.push(driver.trucking_company);
118
+ }
119
+ });
120
+ }
121
+ }
122
+ if (item.pickup_location && this.pickupOptions.findIndex((elem) => elem === item.pickup_location) === -1)
123
+ this.pickupOptions.push(item.pickup_location);
124
+ if (item.delivery_location && this.destOptions.findIndex((elem) => elem === item.delivery_location) === -1)
125
+ this.destOptions.push(item.delivery_location);
126
+ if (item.customer_name && this.customerOptions.findIndex((customer) => customer === item.customer_name) === -1)
127
+ this.customerOptions.push(item.customer_name);
128
+ if (item.unit && this.unitOptions.findIndex((elem) => elem === item.unit) === -1)
129
+ this.unitOptions.push(item.unit);
130
+ if (item.material && this.materialOptions.findIndex((elem) => elem === item.material) === -1)
131
+ this.materialOptions.push(item.material);
132
+ if (item.route_name && this.routeNameOptions.findIndex((elem) => elem === item.route_name) === -1)
133
+ this.routeNameOptions.push(item.route_name);
134
+ }
135
+ setdictValue(key, value) {
136
+ this.dict.set(key, value);
137
+ }
138
+ getdictValue(key) {
139
+ const val = this.dict.get(key);
140
+ return val ? JSON.parse(val) : null;
141
+ }
142
+ removedictValue(key) {
143
+ this.dict.delete(key);
144
+ }
145
+ filter(value, filters) {
146
+ if (typeof value !== 'string') {
147
+ return [];
148
+ }
149
+ const filterValue = value.toLowerCase();
150
+ if (filterValue === '') {
151
+ return [];
152
+ }
153
+ const searchResults = [];
154
+ this.unitOptions.forEach((unit) => {
155
+ if (unit.toLowerCase().includes(filterValue)) {
156
+ searchResults.push({ type: 'unit', label: unit, value: unit });
157
+ }
158
+ });
159
+ this.customerOptions.forEach((unit) => {
160
+ if (unit.toLowerCase().includes(filterValue)) {
161
+ searchResults.push({ type: 'customer', label: unit, value: unit });
162
+ }
163
+ });
164
+ if (this.materialOptions && this.materialOptions.length) {
165
+ this.materialOptions.forEach((unit) => {
166
+ if (unit.toLowerCase().includes(filterValue)) {
167
+ searchResults.push({ type: 'material', label: unit, value: unit });
168
+ }
169
+ });
170
+ }
171
+ this.pickupOptions.forEach((unit) => {
172
+ if (unit.toLowerCase().includes(filterValue)) {
173
+ searchResults.push({
174
+ type: 'pickup location',
175
+ label: unit,
176
+ value: unit,
177
+ });
178
+ }
179
+ });
180
+ this.destOptions.forEach((unit) => {
181
+ if (unit.toLowerCase().includes(filterValue)) {
182
+ searchResults.push({
183
+ type: 'destination location',
184
+ label: unit,
185
+ value: unit,
186
+ });
187
+ }
188
+ });
189
+ this.jcodeOptions.forEach((unit) => {
190
+ if (unit.job_code.toLowerCase().includes(filterValue)) {
191
+ searchResults.push({
192
+ type: 'job',
193
+ label: unit['job_code'],
194
+ value: unit,
195
+ });
196
+ }
197
+ });
198
+ this.driverOption.forEach((unit) => {
199
+ if (unit.toLowerCase().includes(filterValue)) {
200
+ searchResults.push({ type: 'Driver', label: unit, value: unit });
201
+ }
202
+ });
203
+ this.truckingCompanayOption.forEach((unit) => {
204
+ if (unit.toLowerCase().includes(filterValue)) {
205
+ searchResults.push({
206
+ type: 'Trucking Company',
207
+ label: unit,
208
+ value: unit,
209
+ });
210
+ }
211
+ });
212
+ this.routeNameOptions.forEach((unit) => {
213
+ if (unit.toLowerCase().includes(filterValue)) {
214
+ searchResults.push({ type: 'Route name', label: unit, value: unit });
215
+ }
216
+ });
217
+ // Filter out options that are already selected in the filters array
218
+ const searchDict = {};
219
+ filters.forEach((filter) => {
220
+ searchDict[filter['name'] + filter['type']] = filter;
221
+ });
222
+ const furtherFilter = [];
223
+ searchResults.forEach((search) => {
224
+ if (!(search['label'] + search['type'] in searchDict)) {
225
+ furtherFilter.push(search);
226
+ }
227
+ });
228
+ return furtherFilter;
229
+ }
230
+ getSearchResults(list, filterval) {
231
+ return list.filter((element) => {
232
+ const result_list_boolean = [];
233
+ if (filterval.length > 0) {
234
+ if (filterval[0]['type'] === 'unit') {
235
+ result_list_boolean.push(filterval[0]['name'] === element['unit']);
236
+ }
237
+ if (filterval[0]['type'] === 'customer') {
238
+ result_list_boolean.push(filterval[0]['name'] === element['customer_name']);
239
+ }
240
+ if (filterval[0]['type'] === 'material') {
241
+ result_list_boolean.push(filterval[0]['name'] === element['material']);
242
+ }
243
+ if (filterval[0]['type'] === 'pickup location') {
244
+ result_list_boolean.push(filterval[0]['name'] === element['pickup_location']);
245
+ }
246
+ if (filterval[0]['type'] === 'destination location') {
247
+ result_list_boolean.push(filterval[0]['name'] === element['delivery_location']);
248
+ }
249
+ if (filterval[0]['type'] === 'job') {
250
+ result_list_boolean.push(filterval[0]['name'] === element['order_number']);
251
+ }
252
+ if (filterval[0]['type'] === 'Route name') {
253
+ result_list_boolean.push(filterval[0]['name'] === element['route_name']);
254
+ }
255
+ if (filterval[0]['type'] === 'Driver') {
256
+ const driverList = element.driver_list || [];
257
+ const index = driverList.findIndex((ele) => {
258
+ return filterval[0]['name'] === ele['driver_name'];
259
+ });
260
+ if (index !== -1)
261
+ result_list_boolean.push(true);
262
+ }
263
+ if (filterval[0]['type'] === 'Trucking Company') {
264
+ const driverList = element.driver_list || [];
265
+ const index = driverList.findIndex((ele) => {
266
+ return filterval[0]['name'] === ele['trucking_company'];
267
+ });
268
+ if (index !== -1)
269
+ result_list_boolean.push(true);
270
+ }
271
+ }
272
+ if (result_list_boolean.length > 0) {
273
+ return result_list_boolean.reduce((prev, curr) => prev && curr);
274
+ }
275
+ return false;
276
+ });
277
+ }
278
+ fetchUnitsList() {
279
+ return new Promise((resolve) => {
280
+ if (!this.unitsList) {
281
+ this.getData('unit/list/view').subscribe((res) => {
282
+ if (res) {
283
+ this.unitsList = res.data;
284
+ resolve(this.unitsList);
285
+ }
286
+ });
287
+ }
288
+ else {
289
+ resolve(this.unitsList);
290
+ }
291
+ });
292
+ }
293
+ fetchMaterialsListForCustomer() {
294
+ return new Promise((resolve) => {
295
+ if (!this.materialsListForCustomer) {
296
+ this.getData('material/list/view').subscribe((res) => {
297
+ this.materialsListForCustomer = this.filtermaterialList(res.data);
298
+ resolve(this.materialsListForCustomer);
299
+ });
300
+ }
301
+ else {
302
+ resolve(this.materialsListForCustomer);
303
+ }
304
+ });
305
+ }
306
+ filtermaterialList(list) {
307
+ let meterialList = [];
308
+ list.forEach((item) => {
309
+ if (item.sub && item.sub.length > 0) {
310
+ item.sub.forEach((subItem) => {
311
+ if (subItem.add_to_marketplace) {
312
+ subItem.material = item.material;
313
+ subItem.material_id = item.material_id;
314
+ subItem.label = item.material + ' | ' + subItem.sub_material;
315
+ meterialList.push(subItem);
316
+ }
317
+ });
318
+ }
319
+ });
320
+ meterialList.sort((a, b) => a.label.localeCompare(b.label));
321
+ return meterialList;
322
+ }
323
+ fetchCustomersList() {
324
+ return new Promise((resolve) => {
325
+ if (!this.customersList) {
326
+ this.getData('company/list/view').subscribe((res) => {
327
+ if (res) {
328
+ this.customersList = res.data;
329
+ resolve(this.customersList);
330
+ }
331
+ });
332
+ }
333
+ else {
334
+ resolve(this.customersList);
335
+ }
336
+ });
337
+ }
338
+ fetchLocationlist() {
339
+ return new Promise((resolve, reject) => {
340
+ this.getRestData('locations/all').subscribe({
341
+ next: (res) => {
342
+ if (res) {
343
+ this.locationList = res.data.map((object) => {
344
+ object.formatted_address = `${object.name} | ${object.street} ${object.city}, ${object.state} ${object.zip}`;
345
+ return {
346
+ city: object.city,
347
+ lng: object.longitude,
348
+ location_id: object.location_id,
349
+ lat: object.latitude,
350
+ name: object.name,
351
+ state: object.state,
352
+ street: object.street,
353
+ zip: object.zip,
354
+ formatted_address: object.formatted_address
355
+ };
356
+ });
357
+ resolve(this.locationList);
358
+ }
359
+ },
360
+ error: (err) => {
361
+ console.error(err);
362
+ reject(err);
363
+ }
364
+ });
365
+ });
366
+ }
367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
368
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UtilsService, providedIn: 'root' });
369
+ }
370
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UtilsService, decorators: [{
371
+ type: Injectable,
372
+ args: [{ providedIn: 'root' }]
373
+ }] });
374
+
375
+ class MapboxService {
376
+ map;
377
+ mapContainer = null;
378
+ markerOriginList = new Map();
379
+ markerDestinationList = new Map();
380
+ activeRoutesRegistry = new Map();
381
+ pathCache = new Map();
382
+ // Native 3D Simulation Engine
383
+ svgOverlay = null;
384
+ activeArcs = {};
385
+ initiatecoolmap = new BehaviorSubject(true);
386
+ reintiatecoolmap = this.initiatecoolmap.asObservable();
387
+ isMapReady = signal(false, ...(ngDevMode ? [{ debugName: "isMapReady" }] : []));
388
+ bounds = new mapboxgl.LngLatBounds();
389
+ originDestinationCordinates = [];
390
+ padding;
391
+ windowActualHeightWidth = { availHeight: 0 };
392
+ popup;
393
+ customTopForCustomer;
394
+ utils = inject(UtilsService);
395
+ config = inject(COOLMAP_CONFIG);
396
+ currentStyleIsDark = false;
397
+ constructor() {
398
+ this.customTopForCustomer = this.config.repository === 'customer' ? 65 : 0;
399
+ if (typeof window !== 'undefined') {
400
+ this.windowActualHeightWidth.availHeight = window.innerHeight > window.screen.availHeight ? window.innerHeight : window.screen.availHeight;
401
+ window.addEventListener('resize', this.onResize.bind(this));
402
+ }
403
+ }
404
+ updateTheme(isDark) {
405
+ if (!this.map) {
406
+ this.currentStyleIsDark = isDark;
407
+ return;
408
+ }
409
+ if (this.currentStyleIsDark === isDark)
410
+ return;
411
+ this.currentStyleIsDark = isDark;
412
+ const style = isDark
413
+ ? 'mapbox://styles/mapbox/dark-v11'
414
+ : 'mapbox://styles/mapbox/light-v11';
415
+ this.map.setStyle(style);
416
+ this.map.once('style.load', () => {
417
+ this.reAddLayers();
418
+ });
419
+ }
420
+ getRegistryKeys() {
421
+ return Array.from(this.activeRoutesRegistry.keys());
422
+ }
423
+ // Renamed to initializeMap to preserve the existing bindings in coolmap.component.ts
424
+ initializeMap(el, isDark = false) {
425
+ this.currentStyleIsDark = isDark;
426
+ return new Promise((resolve, reject) => {
427
+ this.mapContainer = el;
428
+ if (this.map)
429
+ this.map.remove();
430
+ const token = typeof window !== 'undefined' ? window.__env?.mapboxAccessToken || '' : '';
431
+ mapboxgl.accessToken = token;
432
+ this.map = new mapboxgl.Map({
433
+ container: el,
434
+ style: isDark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11',
435
+ center: [-77.036873, 38.907192],
436
+ zoom: 10,
437
+ bearing: 0,
438
+ pitch: 65,
439
+ interactive: true,
440
+ attributionControl: false
441
+ });
442
+ this.map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right');
443
+ this.map.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-right');
444
+ this.map.once('load', (res) => {
445
+ this.isMapReady.set(true);
446
+ resolve(res);
447
+ });
448
+ });
449
+ }
450
+ // Animated gradient path
451
+ loadMapProperty(pinRouteGeojson, index, unit, routeProps, bottom) {
452
+ return new Promise((resolve) => {
453
+ if (!this.map || !this.isMapReady())
454
+ return resolve(false);
455
+ const origin = pinRouteGeojson.features[0].geometry.coordinates[0];
456
+ const linecolor = unit === 'Ton' ? '#ff7272' : unit === 'Load' ? '#a3c52e' : '#ae23d1';
457
+ const destination = pinRouteGeojson.features[0].geometry.coordinates[pinRouteGeojson.features[0].geometry.coordinates.length - 1];
458
+ this.extendBound(pinRouteGeojson.features[0].geometry.coordinates, true).then(() => {
459
+ const point = {
460
+ type: 'FeatureCollection',
461
+ features: [
462
+ { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: origin } },
463
+ ],
464
+ };
465
+ const lineDistance = turf.length(pinRouteGeojson.features[0]);
466
+ const arc = [];
467
+ const steps = 10 * pinRouteGeojson.features[0].geometry.coordinates.length;
468
+ for (let i = 0; i < lineDistance; i += lineDistance / steps) {
469
+ const segment = turf.along(pinRouteGeojson.features[0], i);
470
+ arc.push(segment.geometry.coordinates);
471
+ }
472
+ pinRouteGeojson.features[0].geometry.coordinates = arc;
473
+ const pinRoute = pinRouteGeojson.features[0].geometry.coordinates;
474
+ if (pinRoute && pinRoute.length) {
475
+ const marker = new mapboxgl.Marker(document.createElement('div'))
476
+ .setLngLat(pinRoute[0])
477
+ .addTo(this.map)
478
+ .togglePopup();
479
+ if (this.map.getSource(`line${index}`)) {
480
+ this.removeRouteAndMarker(index).then(() => { });
481
+ }
482
+ this.map.addSource(`line${index}`, {
483
+ type: 'geojson',
484
+ lineMetrics: true,
485
+ data: pinRouteGeojson,
486
+ });
487
+ this.map.addLayer({
488
+ type: 'line',
489
+ source: `line${index}`,
490
+ id: `line${index}`,
491
+ paint: {
492
+ 'line-width': 2,
493
+ 'line-gradient': [
494
+ 'interpolate', ['linear'], ['line-progress'],
495
+ 0, unit === 'Ton' ? '#d7f7e4' : unit === 'Load' ? '#c9d8f5' : '#f5dcc1',
496
+ 1, unit === 'Ton' ? '#ff7272' : unit === 'Load' ? '#a3c52e' : '#ae23d1',
497
+ ],
498
+ },
499
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
500
+ });
501
+ const dataSetForMap = {
502
+ counter: 0,
503
+ pinRouteGeojson,
504
+ steps,
505
+ point,
506
+ pointId: `point${index}`,
507
+ marker,
508
+ pinRoute,
509
+ lineId: `line${index}`,
510
+ index,
511
+ origin,
512
+ destination,
513
+ lineDistance,
514
+ linecolor,
515
+ route: routeProps,
516
+ routeType: 'addroute',
517
+ isViewRoute: true,
518
+ };
519
+ this.createMarker(dataSetForMap);
520
+ this.map.on('mouseenter', `line${index}`, (e) => {
521
+ this.map.setPaintProperty(`line${index}`, 'line-width', 5);
522
+ this.map.setPaintProperty(`line${index}`, 'line-opacity', 1);
523
+ const datasetForPopup = {
524
+ coordinate: [e.lngLat.lng, e.lngLat.lat],
525
+ pickup: routeProps?.pickup_location || '',
526
+ drop: routeProps?.delivery_location || '',
527
+ routeType: routeProps?.project ? 'Project' : 'Route',
528
+ title: routeProps?.project ? routeProps.project : routeProps?.route_name || '',
529
+ material: routeProps?.material || '',
530
+ type: routeProps?.unit || '',
531
+ };
532
+ if (this.popup)
533
+ this.popup.remove();
534
+ this.createPopup(datasetForPopup);
535
+ });
536
+ this.map.on('mouseleave', `line${index}`, () => {
537
+ this.map.setPaintProperty(`line${index}`, 'line-width', 2);
538
+ if (this.popup)
539
+ this.popup.remove();
540
+ });
541
+ }
542
+ });
543
+ this.map.once('idle', () => {
544
+ resolve(true);
545
+ });
546
+ });
547
+ }
548
+ createMarker(routeDetails) {
549
+ if (!this.map || !routeDetails.origin[0] || !routeDetails.origin[1] || !routeDetails.destination[0] || !routeDetails.destination[1])
550
+ return;
551
+ const popup = new mapboxgl.Popup({ closeButton: false }).setHTML('<b>Pickup: </b>' + (routeDetails.route?.pickup_location || ''));
552
+ const popupForDestination = new mapboxgl.Popup({ closeButton: false }).setHTML('<b>Delivery: </b>' + (routeDetails.route?.delivery_location || ''));
553
+ // Origin Marker (Pickup)
554
+ const el = document.createElement('div');
555
+ el.className = 'cursor-pointer active:scale-95 transition-transform duration-200 drop-shadow-lg';
556
+ const pickupLabel = 'P';
557
+ el.innerHTML = `
558
+ <svg width="30" height="38" viewBox="0 0 30 38" fill="none" xmlns="http://www.w3.org/2000/svg">
559
+ <path d="M15 0C6.71573 0 0 6.71573 0 15C0 26.25 15 38 15 38C15 38 30 26.25 30 15C30 6.71573 23.2843 0 15 0Z" fill="${routeDetails.linecolor}"/>
560
+ <circle cx="15" cy="15" r="9" fill="white"/>
561
+ <text x="15" y="19" text-anchor="middle" fill="black" style="font-family: Inter, sans-serif; font-size: 13px; font-weight: 800;">${pickupLabel}</text>
562
+ </svg>
563
+ `;
564
+ const originMarker = new mapboxgl.Marker({ element: el, anchor: 'bottom' })
565
+ .setPopup(popup)
566
+ .setLngLat(routeDetails.origin)
567
+ .addTo(this.map);
568
+ originMarker.getElement().addEventListener('mouseenter', () => originMarker.togglePopup());
569
+ originMarker.getElement().addEventListener('mouseleave', () => originMarker.togglePopup());
570
+ // Destination Marker (Delivery)
571
+ const elementForDestination = document.createElement('div');
572
+ elementForDestination.className = 'cursor-pointer active:scale-95 transition-transform duration-200 drop-shadow-lg';
573
+ const deliveryLabel = 'D';
574
+ elementForDestination.innerHTML = `
575
+ <svg width="30" height="38" viewBox="0 0 30 38" fill="none" xmlns="http://www.w3.org/2000/svg">
576
+ <path d="M15 0C6.71573 0 0 6.71573 0 15C0 26.25 15 38 15 38C15 38 30 26.25 30 15C30 6.71573 23.2843 0 15 0Z" fill="${routeDetails.linecolor}"/>
577
+ <circle cx="15" cy="15" r="9" fill="white"/>
578
+ <text x="15" y="19" text-anchor="middle" fill="black" style="font-family: Inter, sans-serif; font-size: 13px; font-weight: 800;">${deliveryLabel}</text>
579
+ </svg>
580
+ `;
581
+ const destinationMarker = new mapboxgl.Marker({ element: elementForDestination, anchor: 'bottom' })
582
+ .setPopup(popupForDestination)
583
+ .setLngLat(routeDetails.destination)
584
+ .addTo(this.map);
585
+ destinationMarker.getElement().addEventListener('mouseenter', () => destinationMarker.togglePopup());
586
+ destinationMarker.getElement().addEventListener('mouseleave', () => destinationMarker.togglePopup());
587
+ const id = `${routeDetails.routeType}-${routeDetails.index}`;
588
+ this.markerOriginList.set(id, originMarker);
589
+ this.markerDestinationList.set(id, destinationMarker);
590
+ // True 3D Elevated Arc Simulation Engine using Native SVG Overlays
591
+ try {
592
+ this.initSvgOverlay();
593
+ const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
594
+ pathEl.setAttribute('fill', 'none');
595
+ pathEl.setAttribute('stroke-width', '3');
596
+ pathEl.style.cursor = 'pointer';
597
+ pathEl.style.pointerEvents = 'stroke'; // Allow interactivity!
598
+ const gradId = `arc-grad-${id}`;
599
+ const defs = this.svgOverlay.querySelector('defs');
600
+ let grad = defs.querySelector(`#${gradId}`);
601
+ if (!grad) {
602
+ grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
603
+ grad.setAttribute('id', gradId);
604
+ defs.appendChild(grad);
605
+ }
606
+ grad.innerHTML = `
607
+ <stop offset="0%" stop-color="${routeDetails.linecolor}" stop-opacity="1"/>
608
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0.8"/>
609
+ `;
610
+ pathEl.setAttribute('stroke', `url(#${gradId})`);
611
+ this.svgOverlay.appendChild(pathEl);
612
+ this.activeArcs[id] = {
613
+ origin: routeDetails.origin,
614
+ destination: routeDetails.destination,
615
+ color: routeDetails.linecolor,
616
+ pathEl
617
+ };
618
+ // Native tooltips hovering strictly matching V1 Deck.GL behaviors
619
+ pathEl.addEventListener('mouseenter', (e) => {
620
+ pathEl.setAttribute('stroke-width', '6');
621
+ // Find geo coordinate exactly under cursor natively
622
+ const rect = this.svgOverlay.getBoundingClientRect();
623
+ const lngLat = this.map.unproject([e.clientX - rect.left, e.clientY - rect.top]);
624
+ const payload = {
625
+ layer: { props: { data: { route: routeDetails.route, index: routeDetails.index, routeType: routeDetails.routeType } } },
626
+ coordinate: [lngLat.lng, lngLat.lat],
627
+ color: routeDetails.linecolor
628
+ };
629
+ this.showRoutePopup(payload, e, routeDetails.isViewRoute);
630
+ });
631
+ pathEl.addEventListener('mouseleave', () => {
632
+ pathEl.setAttribute('stroke-width', '3');
633
+ const payload = { color: null, layer: { props: { data: { index: routeDetails.index, routeType: routeDetails.routeType } } } };
634
+ this.showRoutePopup(payload, null, routeDetails.isViewRoute);
635
+ });
636
+ this.updateSvgPaths(); // Force plot
637
+ }
638
+ catch (e) {
639
+ console.warn("Native overarching arc generation skipped:", e);
640
+ }
641
+ }
642
+ showRoutePopup(arcDetails, event, isViewRoute) {
643
+ const data = arcDetails.layer.props.data;
644
+ const layerId = isViewRoute
645
+ ? `line${data.index}`
646
+ : `route-layer-${data.routeType || 'jobcode'}-${data.index}`;
647
+ if (this.popup) {
648
+ this.popup.remove();
649
+ if (this.map.getLayer(layerId)) {
650
+ this.map.setPaintProperty(layerId, 'line-width', 2);
651
+ }
652
+ }
653
+ if (arcDetails.color && this.map.getLayer(layerId) && this.map.getLayoutProperty(layerId, 'visibility') !== 'none') {
654
+ this.map.setPaintProperty(layerId, 'line-width', 5);
655
+ // Removed line-opacity overwrite here because bezier lines inherit opacity and break if set raw on a gradient.
656
+ const datasetForPopup = {
657
+ coordinate: arcDetails.coordinate,
658
+ pickup: arcDetails.layer.props.data.route.pickup_location || '',
659
+ drop: arcDetails.layer.props.data.route.delivery_location || '',
660
+ jobCode: arcDetails.layer.props.data.route.project ? arcDetails.layer.props.data.route.order_number : '',
661
+ customer: arcDetails.layer.props.data.route.project ? arcDetails.layer.props.data.route.customer_name : '',
662
+ routeType: arcDetails.layer.props.data.route.project ? 'Project' : 'Route',
663
+ title: arcDetails.layer.props.data.route.project ? arcDetails.layer.props.data.route.project : arcDetails.layer.props.data.route.route_name || '',
664
+ material: arcDetails.layer.props.data.route.material || '',
665
+ type: arcDetails.layer.props.data.route.unit || '',
666
+ };
667
+ this.createPopup(datasetForPopup);
668
+ }
669
+ }
670
+ createPopup(datasetForPopup) {
671
+ if (!this.map)
672
+ return;
673
+ this.popup = new mapboxgl.Popup({
674
+ closeButton: false,
675
+ closeOnClick: false,
676
+ closeOnMove: true,
677
+ anchor: 'bottom-left',
678
+ });
679
+ // Extract location short names
680
+ const pickupDisplay = datasetForPopup.pickup?.split('|')[1] || datasetForPopup.pickup || 'Unknown Pickup';
681
+ const dropDisplay = datasetForPopup.drop?.split('|')[1] || datasetForPopup.drop || 'Unknown Delivery';
682
+ this.popup
683
+ .setLngLat(datasetForPopup.coordinate)
684
+ .setHTML(`<div class="destination p-3 bg-white dark:bg-slate-800 rounded shadow-lg text-sm text-gray-800 dark:text-gray-200">
685
+ <div class="duration space-y-1 mb-2">
686
+ <p class="pickprt"><b class="text-gray-900 dark:text-white">Pickup Location:</b> ${pickupDisplay}</p>
687
+ <p class="dropprt"><b class="text-gray-900 dark:text-white">Drop Location:</b> ${dropDisplay}</p>
688
+ </div>
689
+ ${datasetForPopup.jobCode ? `<span class="block"><b>Job Code:</b> ${datasetForPopup.jobCode}</span>` : ''}
690
+ ${datasetForPopup.customer ? `<span class="block"><b>Customer:</b> ${datasetForPopup.customer}</span>` : ''}
691
+ <span class="block"><b>${datasetForPopup.routeType} Name:</b> ${datasetForPopup.title}</span>
692
+ ${datasetForPopup.material ? `<span class="block"><b>Material:</b> ${datasetForPopup.material}</span>` : ''}
693
+ <span class="block"><b>Type:</b> ${datasetForPopup.type}</span>
694
+ </div>`)
695
+ .addTo(this.map);
696
+ }
697
+ // Draw Line
698
+ drawLine(cordinates, index, route, enablefitbound, routeType, isStyleRefresh) {
699
+ return new Promise((resolve) => {
700
+ if (!this.map || !this.isMapReady()) {
701
+ resolve();
702
+ return;
703
+ }
704
+ // Calculate bounds for this specific route
705
+ const routeBounds = new mapboxgl.LngLatBounds();
706
+ if (cordinates && cordinates.length > 0) {
707
+ cordinates.forEach((coord) => routeBounds.extend(coord));
708
+ }
709
+ const registryId = `${routeType}-${index}`;
710
+ // Register the route with its coordinates and pre-calculated bounds
711
+ this.activeRoutesRegistry.set(registryId, {
712
+ cordinates,
713
+ route,
714
+ routeType,
715
+ bounds: routeBounds,
716
+ index
717
+ });
718
+ let origin = cordinates[0];
719
+ let destination = cordinates[cordinates.length - 1];
720
+ if (origin[0] && origin[1] && destination && destination[0] && destination[1]) {
721
+ // Execute plotting logic IMMEDIATELY to trigger the map's render cycle
722
+ const linecolor = this.provideLineColor(route['unit'], routeType);
723
+ if (enablefitbound) {
724
+ const padTop = Math.max(0, (this.padding?.top || 0) + this.customTopForCustomer + 50);
725
+ const padBot = Math.max(0, (this.padding?.bottom || 0) + (this.windowActualHeightWidth.availHeight - (window.innerHeight - 65)) + 50);
726
+ const padLeft = Math.max(0, (this.padding?.left || 0) + 350);
727
+ const padRight = Math.max(0, (this.padding?.right || 0) + 50);
728
+ const customPadding = { top: padTop, bottom: padBot, left: padLeft, right: padRight };
729
+ this.map.fitBounds([origin, destination], { padding: customPadding, pitch: 65 }, { fitboundCompleteJob: true });
730
+ }
731
+ const sourceId = `route-source-${routeType}-${index}`;
732
+ const layerId = `route-layer-${routeType}-${index}`;
733
+ if (this.map.getSource(sourceId)) {
734
+ this.removeRouteAndMarker(index, routeType).then(() => { });
735
+ }
736
+ this.map.addSource(sourceId, {
737
+ type: 'geojson',
738
+ data: {
739
+ type: 'Feature',
740
+ properties: {},
741
+ geometry: { type: 'LineString', coordinates: cordinates },
742
+ },
743
+ });
744
+ this.map.addLayer({
745
+ id: layerId,
746
+ type: 'line',
747
+ source: sourceId,
748
+ paint: { 'line-color': linecolor, 'line-width': 2 },
749
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
750
+ });
751
+ if (!isStyleRefresh) {
752
+ const dataSetForMap = { origin, destination, index, linecolor, route, routeType };
753
+ this.createMarker(dataSetForMap);
754
+ }
755
+ this.map.on('mouseenter', layerId, (e) => {
756
+ if (this.popup)
757
+ this.popup.remove();
758
+ this.map.setPaintProperty(layerId, 'line-width', 5);
759
+ this.map.setPaintProperty(layerId, 'line-opacity', 1);
760
+ const datasetForPopup = {
761
+ coordinate: [e.lngLat.lng, e.lngLat.lat],
762
+ pickup: route.pickup_location || '',
763
+ drop: route.delivery_location || '',
764
+ jobCode: route.project ? route.order_number : null,
765
+ customer: route.project ? route.customer_name : null,
766
+ routeType: route.project ? 'Project' : 'Route',
767
+ title: route.project ? route.project : route.route_name || '',
768
+ material: route.material || '',
769
+ type: route.unit || '',
770
+ };
771
+ this.createPopup(datasetForPopup);
772
+ });
773
+ this.map.on('mouseleave', layerId, () => {
774
+ this.map.setPaintProperty(layerId, 'line-width', 2);
775
+ if (this.popup)
776
+ this.popup.remove();
777
+ });
778
+ // SAFETY: Use a timeout to ensure resolve() is always called even if 'idle' event skips
779
+ let resolved = false;
780
+ const complete = () => {
781
+ if (!resolved) {
782
+ resolved = true;
783
+ resolve();
784
+ }
785
+ };
786
+ this.map.once('idle', complete);
787
+ setTimeout(complete, 500); // 500ms safety timeout
788
+ }
789
+ else {
790
+ resolve();
791
+ }
792
+ });
793
+ }
794
+ provideLineColor(unitType, type) {
795
+ let checkType = type && !['jobrouteList', 'addroute'].includes(type) ? true : false;
796
+ let color;
797
+ switch (unitType) {
798
+ case 'Ton':
799
+ color = checkType ? '#39c471' : '#ff7272';
800
+ break;
801
+ case 'Load':
802
+ color = checkType ? '#326ad3' : '#a3c52e';
803
+ break;
804
+ case 'Hourly':
805
+ color = checkType ? '#ffad56' : '#ae23d1';
806
+ break;
807
+ default:
808
+ color = '#3b82f6';
809
+ break;
810
+ }
811
+ return color;
812
+ }
813
+ hexToRGB(hex) {
814
+ return hex
815
+ .replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => '#' + r + r + g + g + b + b)
816
+ .substring(1)
817
+ .match(/.{2}/g)
818
+ .map((x) => parseInt(x, 16));
819
+ }
820
+ // --- SVG 3D Engine Methods ---
821
+ initSvgOverlay() {
822
+ if (this.svgOverlay || !this.mapContainer)
823
+ return;
824
+ this.svgOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
825
+ this.svgOverlay.style.position = 'absolute';
826
+ this.svgOverlay.style.top = '0';
827
+ this.svgOverlay.style.left = '0';
828
+ this.svgOverlay.style.width = '100%';
829
+ this.svgOverlay.style.height = '100%';
830
+ this.svgOverlay.style.pointerEvents = 'none';
831
+ this.svgOverlay.style.zIndex = '10';
832
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
833
+ this.svgOverlay.appendChild(defs);
834
+ this.mapContainer.appendChild(this.svgOverlay);
835
+ this.map.on('render', () => this.updateSvgPaths());
836
+ }
837
+ updateSvgPaths() {
838
+ if (!this.svgOverlay || !this.map)
839
+ return;
840
+ for (const key in this.activeArcs) {
841
+ const arc = this.activeArcs[key];
842
+ const p1 = this.map.project(arc.origin);
843
+ const p2 = this.map.project(arc.destination);
844
+ // Safety Guard: Check for Infinity and extreme numbers (Number.MAX_VALUE)
845
+ // which Mapbox can return during transitions.
846
+ // 1e15 is a safe threshold for JS math and SVG rendering engines.
847
+ const isUsable = (p) => Number.isFinite(p.x) && Math.abs(p.x) < 1e15 &&
848
+ Number.isFinite(p.y) && Math.abs(p.y) < 1e15;
849
+ if (!isUsable(p1) || !isUsable(p2))
850
+ continue;
851
+ const dx = p2.x - p1.x;
852
+ const dy = p2.y - p1.y;
853
+ const dist = Math.sqrt(dx * dx + dy * dy);
854
+ // Secondary check for distance finiteness
855
+ if (!Number.isFinite(dist) || dist > 1e15)
856
+ continue;
857
+ const mx = (p1.x + p2.x) / 2;
858
+ let controlY = ((p1.y + p2.y) / 2) - (dist * 1.05);
859
+ // Final sanity check for calculated curve peaks
860
+ controlY = Math.max(100, controlY);
861
+ if (!Number.isFinite(controlY) || !Number.isFinite(mx))
862
+ continue;
863
+ // Update with rounded values to ensure clean and valid attribute data
864
+ arc.pathEl.setAttribute('d', `M ${p1.x.toFixed(2)} ${p1.y.toFixed(2)} Q ${mx.toFixed(2)} ${controlY.toFixed(2)} ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`);
865
+ // Sync the gradient positioning physically mapped to coordinate space
866
+ const grad = this.svgOverlay.querySelector(`#arc-grad-${key}`);
867
+ if (grad) {
868
+ const minX = Math.min(p1.x, p2.x);
869
+ const maxX = Math.max(p1.x, p2.x);
870
+ const minY = Math.min(p1.y, p2.y, controlY);
871
+ const maxY = Math.max(p1.y, p2.y, controlY);
872
+ const w = Math.max(1, maxX - minX);
873
+ const h = Math.max(1, maxY - minY);
874
+ grad.setAttribute('x1', `${((p1.x - minX) / w) * 100}%`);
875
+ grad.setAttribute('y1', `${((p1.y - minY) / h) * 100}%`);
876
+ grad.setAttribute('x2', `${((p2.x - minX) / w) * 100}%`);
877
+ grad.setAttribute('y2', `${((p2.y - minY) / h) * 100}%`);
878
+ }
879
+ }
880
+ }
881
+ async removeRouteAndMarker(index, routeType = 'jobcode') {
882
+ const registryId = `${routeType}-${index}`;
883
+ // Clean up registry
884
+ if (this.activeRoutesRegistry.has(registryId)) {
885
+ this.activeRoutesRegistry.delete(registryId);
886
+ }
887
+ if (!this.map)
888
+ return true;
889
+ try {
890
+ if (this.activeArcs[registryId]) {
891
+ const arc = this.activeArcs[registryId];
892
+ if (arc.pathEl.parentNode)
893
+ arc.pathEl.parentNode.removeChild(arc.pathEl);
894
+ delete this.activeArcs[registryId];
895
+ }
896
+ if (this.svgOverlay) {
897
+ const grad = this.svgOverlay.querySelector(`defs #arc-grad-${registryId}`);
898
+ if (grad && grad.parentNode)
899
+ grad.parentNode.removeChild(grad);
900
+ }
901
+ const layerId = `route-layer-${routeType}-${index}`;
902
+ const sourceId = `route-source-${routeType}-${index}`;
903
+ this.map.getLayer(layerId) ? this.map.removeLayer(layerId) : null;
904
+ this.map.getLayer(`line${index}`) ? this.map.removeLayer(`line${index}`) : null;
905
+ this.map.getSource(sourceId) ? this.map.removeSource(sourceId) : null;
906
+ this.map.getSource(`line${index}`) ? this.map.removeSource(`line${index}`) : null;
907
+ this.markerOriginList.get(registryId)?.remove();
908
+ this.markerOriginList.delete(registryId);
909
+ this.markerDestinationList.get(registryId)?.remove();
910
+ this.markerDestinationList.delete(registryId);
911
+ this.markerOriginList.get(String(index))?.remove();
912
+ this.markerDestinationList.get(String(index))?.remove();
913
+ if (this.popup)
914
+ this.popup.remove();
915
+ return true;
916
+ }
917
+ catch (e) {
918
+ console.warn('Error removing route/marker:', e);
919
+ return false;
920
+ }
921
+ }
922
+ reAddLayers() {
923
+ this.activeRoutesRegistry.forEach((data, regId) => {
924
+ // Use the stored raw index instead of the composite registry key
925
+ this.drawLine(data.cordinates, data.index, data.route, false, data.routeType, true);
926
+ });
927
+ }
928
+ findMarkerBound(index) {
929
+ const id = String(index);
930
+ const originMarker = this.markerOriginList.get(id);
931
+ const originLng = originMarker?.getLngLat()?.lng;
932
+ const originLat = originMarker?.getLngLat()?.lat;
933
+ if (originLng !== undefined && originLat !== undefined) {
934
+ const indexOfCordinates = this.originDestinationCordinates.findIndex((x) => x[0][0].toFixed(6) === originLng.toFixed(6) &&
935
+ x[0][1].toFixed(6) === originLat.toFixed(6));
936
+ if (indexOfCordinates >= 0) {
937
+ this.originDestinationCordinates.splice(indexOfCordinates, 1);
938
+ }
939
+ }
940
+ }
941
+ async filterRoute(ID, visibility, showAllFitbound) {
942
+ if (ID && this.map) {
943
+ if (this.map.getLayer(`route-for-job-code${ID}`)) {
944
+ this.map.setLayoutProperty(`route-for-job-code${ID}`, 'visibility', visibility);
945
+ const id = String(ID);
946
+ const originM = this.markerOriginList.get(id);
947
+ if (originM) {
948
+ originM.getElement().style.display = visibility === 'visible' ? 'block' : 'none';
949
+ }
950
+ const destinationM = this.markerDestinationList.get(id);
951
+ if (destinationM) {
952
+ destinationM.getElement().style.display = visibility === 'visible' ? 'block' : 'none';
953
+ }
954
+ if (this.activeArcs[ID]) {
955
+ this.activeArcs[ID].pathEl.style.display = visibility === 'visible' ? 'block' : 'none';
956
+ }
957
+ if (visibility === 'none' && showAllFitbound) {
958
+ this.findMarkerBound(ID);
959
+ this.extendReBound();
960
+ }
961
+ }
962
+ if (this.map.getLayer(`arc-layer${ID}`)) {
963
+ this.map.setLayoutProperty(`arc-layer${ID}`, 'visibility', visibility);
964
+ }
965
+ }
966
+ return true;
967
+ }
968
+ extendBound(route, showAllFitbound) {
969
+ return new Promise((resolve) => {
970
+ if (route && this.map) {
971
+ if (typeof route === 'string') {
972
+ let path = route.split(';');
973
+ path = path.map((ele) => this.formateLatLong(ele));
974
+ path = path.filter((ele) => ele && ele.length > 1);
975
+ route = path;
976
+ }
977
+ if (route[0] && route[0][0] && route[0][1] && route[route.length - 1] && route[route.length - 1][0] && route[route.length - 1][1]) {
978
+ this.originDestinationCordinates.push(route);
979
+ this.bounds = new mapboxgl.LngLatBounds();
980
+ this.originDestinationCordinates.forEach((routeItem) => {
981
+ routeItem.forEach((coord) => {
982
+ this.bounds.extend(coord);
983
+ });
984
+ });
985
+ }
986
+ }
987
+ if (showAllFitbound && this.map) {
988
+ setTimeout(() => {
989
+ if (showAllFitbound && !this.bounds.isEmpty()) {
990
+ this.map.fitBounds(this.bounds, { pitch: 65, maxZoom: 14, padding: 90 }, { fitboundComplete: true });
991
+ }
992
+ }, 100);
993
+ this.map.once('moveend', (event) => {
994
+ if (event.fitboundComplete) {
995
+ resolve(true);
996
+ }
997
+ });
998
+ }
999
+ else {
1000
+ resolve(true);
1001
+ }
1002
+ });
1003
+ }
1004
+ extendReBound(bottom) {
1005
+ return new Promise((resolve) => {
1006
+ this.bounds = new mapboxgl.LngLatBounds();
1007
+ if (this.originDestinationCordinates.length > 0) {
1008
+ this.originDestinationCordinates.forEach((item, index) => {
1009
+ item.forEach((route) => {
1010
+ this.bounds.extend(route);
1011
+ });
1012
+ if (index === this.originDestinationCordinates.length - 1) {
1013
+ const padTop = Math.max(0, (this.padding?.top || 0) + this.customTopForCustomer + 80);
1014
+ const padBot = Math.max(0, (this.padding?.bottom || 0) + (this.windowActualHeightWidth.availHeight - (window.innerHeight - 65)) + 80);
1015
+ const padLeft = Math.max(0, (this.padding?.left || 0) + 350);
1016
+ const padRight = Math.max(0, (this.padding?.right || 0) + 80);
1017
+ const customPadding = {
1018
+ top: padTop, bottom: padBot,
1019
+ left: padLeft, right: padRight,
1020
+ };
1021
+ setTimeout(() => {
1022
+ if (this.originDestinationCordinates.length > 0 && !this.bounds.isEmpty() && this.map) {
1023
+ this.map.fitBounds(this.bounds, { padding: customPadding, pitch: 65, maxZoom: 14 });
1024
+ }
1025
+ }, 500);
1026
+ resolve(true);
1027
+ }
1028
+ });
1029
+ }
1030
+ else {
1031
+ resolve(true);
1032
+ }
1033
+ });
1034
+ }
1035
+ async plotRoute(route, i, type, enablefitbound, showAllFitbound) {
1036
+ const cacheKey = route['job_id'] || route['route_id'];
1037
+ // 1. Check cache FIRST
1038
+ if (this.pathCache.has(cacheKey)) {
1039
+ const path = this.pathCache.get(cacheKey);
1040
+ route['path'] = path;
1041
+ this.extendBound(path, showAllFitbound);
1042
+ await this.drawLine(path, i, route, enablefitbound, type);
1043
+ route['index'] = i;
1044
+ return true;
1045
+ }
1046
+ // 2. Check for inline path (Avoid API if already available)
1047
+ if (route['path'] && (Array.isArray(route['path']) || typeof route['path'] === 'string')) {
1048
+ let path = typeof route['path'] === 'string' ? route['path'].split(';') : route['path'];
1049
+ if (Array.isArray(path) && path.length > 0) {
1050
+ // Format if they are still strings
1051
+ if (typeof path[0] === 'string') {
1052
+ path = path.map((ele) => this.formateLatLong(ele));
1053
+ path = path.filter((ele) => ele && ele.length > 1);
1054
+ }
1055
+ if (path.length > 0) {
1056
+ this.pathCache.set(cacheKey, path);
1057
+ route['path'] = path;
1058
+ this.extendBound(path, showAllFitbound);
1059
+ await this.drawLine(path, i, route, enablefitbound, type);
1060
+ route['index'] = i;
1061
+ return true;
1062
+ }
1063
+ }
1064
+ }
1065
+ // 3. Fallback to API call ONLY for jobcode (if path was missing)
1066
+ return new Promise((resolve, reject) => {
1067
+ if (['jobcode'].includes(type) || route['job_id']) {
1068
+ let param = { job: route['job_id'] };
1069
+ this.utils.postDataWithRestUrl('schedule/job/path', param).subscribe({
1070
+ next: async (res) => {
1071
+ let path = [];
1072
+ if (res['data'] && res['data']['route']) {
1073
+ path = res['data']['route'].split(';');
1074
+ path = path.map((ele) => this.formateLatLong(ele));
1075
+ path = path.filter((ele) => ele && ele.length > 1);
1076
+ this.pathCache.set(cacheKey, path);
1077
+ }
1078
+ if (path && path.length > 0) {
1079
+ route['path'] = path;
1080
+ this.extendBound(route['path'], showAllFitbound);
1081
+ await this.drawLine(route['path'], i, route, enablefitbound, type);
1082
+ route['index'] = i;
1083
+ }
1084
+ else {
1085
+ this.extendBound(null, showAllFitbound);
1086
+ }
1087
+ resolve(true);
1088
+ },
1089
+ error: (err) => {
1090
+ console.warn('Failed path retrieval', err);
1091
+ reject(false);
1092
+ }
1093
+ });
1094
+ }
1095
+ else {
1096
+ this.extendBound(null, showAllFitbound);
1097
+ resolve(true);
1098
+ }
1099
+ });
1100
+ }
1101
+ /**
1102
+ * Batch toggle visibility for routes on the map.
1103
+ * If visibleIds is empty, shows everything in the registry.
1104
+ * Otherwise, shows only those in visibleIds and hides the rest.
1105
+ */
1106
+ setRoutesVisibility(visibleIds, showAllOverride, prefix) {
1107
+ const showAll = showAllOverride !== undefined ? showAllOverride : (visibleIds.length === 0);
1108
+ this.activeRoutesRegistry.forEach((data, id) => {
1109
+ const registryId = String(id).trim();
1110
+ // If a prefix is provided, only operate on routes matching that prefix.
1111
+ if (prefix && !registryId.startsWith(`${prefix}-`)) {
1112
+ return;
1113
+ }
1114
+ const isVisible = showAll || visibleIds.includes(registryId);
1115
+ const visibility = isVisible ? 'visible' : 'none';
1116
+ const display = isVisible ? 'block' : 'none';
1117
+ // Toggle New Layer Format
1118
+ const newLayerId = `route-layer-${data.routeType}-${data.index || id}`;
1119
+ if (this.map.getLayer(newLayerId)) {
1120
+ this.map.setLayoutProperty(newLayerId, 'visibility', visibility);
1121
+ }
1122
+ // Legacy/Alternate Layer Formats
1123
+ const legacyId = `route-for-job-code${id}`;
1124
+ if (this.map.getLayer(legacyId)) {
1125
+ this.map.setLayoutProperty(legacyId, 'visibility', visibility);
1126
+ }
1127
+ if (this.map.getLayer(`line${id}`)) {
1128
+ this.map.setLayoutProperty(`line${id}`, 'visibility', visibility);
1129
+ }
1130
+ // Toggle Markers
1131
+ const originM = this.markerOriginList.get(registryId);
1132
+ if (originM)
1133
+ originM.getElement().style.display = display;
1134
+ const destM = this.markerDestinationList.get(registryId);
1135
+ if (destM)
1136
+ destM.getElement().style.display = display;
1137
+ // Toggle Arcs
1138
+ if (this.activeArcs[registryId]) {
1139
+ this.activeArcs[registryId].pathEl.style.display = display;
1140
+ }
1141
+ });
1142
+ // Recalculate bounds based on visible routes
1143
+ this.recalculateVisibleBounds(visibleIds);
1144
+ }
1145
+ recalculateVisibleBounds(visibleIds) {
1146
+ const showAll = visibleIds.length === 0;
1147
+ this.bounds = new mapboxgl.LngLatBounds();
1148
+ this.originDestinationCordinates = [];
1149
+ const normalizedVisibleIds = visibleIds.map(vid => String(vid).trim());
1150
+ this.activeRoutesRegistry.forEach((data, id) => {
1151
+ const registryId = String(id).trim();
1152
+ if (showAll || normalizedVisibleIds.includes(registryId)) {
1153
+ if (data.bounds && !data.bounds.isEmpty()) {
1154
+ this.bounds.extend(data.bounds);
1155
+ this.originDestinationCordinates.push(data.cordinates);
1156
+ }
1157
+ else if (data.cordinates && data.cordinates.length > 0) {
1158
+ // Fallback if bounds weren't pre-calculated
1159
+ data.cordinates.forEach((coord) => this.bounds.extend(coord));
1160
+ this.originDestinationCordinates.push(data.cordinates);
1161
+ }
1162
+ }
1163
+ });
1164
+ if (!this.bounds.isEmpty() && this.map) {
1165
+ const canvas = this.map.getCanvas();
1166
+ const mapW = canvas.clientWidth;
1167
+ const mapH = canvas.clientHeight;
1168
+ let padTop = Math.max(0, (Number(this.padding?.top) || 0) + (Number(this.customTopForCustomer) || 0) + 80);
1169
+ let padBot = Math.max(0, (Number(this.padding?.bottom) || 0) + (Math.max(0, Number(this.windowActualHeightWidth.availHeight) - (Number(window.innerHeight) - 65))) + 80);
1170
+ let padLeft = Math.max(0, (Number(this.padding?.left) || 0) + (window.innerWidth > 1000 ? 350 : 50));
1171
+ let padRight = Math.max(0, (Number(this.padding?.right) || 0) + 80);
1172
+ // Safety Clamps: Ensure padding doesn't exceed canvas size
1173
+ const maxHorizPad = mapW * 0.8;
1174
+ const maxVertPad = mapH * 0.8;
1175
+ if (padLeft + padRight > maxHorizPad) {
1176
+ const factor = maxHorizPad / (padLeft + padRight);
1177
+ padLeft *= factor;
1178
+ padRight *= factor;
1179
+ }
1180
+ if (padTop + padBot > maxVertPad) {
1181
+ const factor = maxVertPad / (padTop + padBot);
1182
+ padTop *= factor;
1183
+ padBot *= factor;
1184
+ }
1185
+ try {
1186
+ this.map.fitBounds(this.bounds, {
1187
+ padding: { top: padTop, bottom: padBot, left: padLeft, right: padRight },
1188
+ pitch: 65,
1189
+ maxZoom: 12,
1190
+ duration: 800
1191
+ });
1192
+ }
1193
+ catch (err) {
1194
+ console.warn('fitBounds failed, trying with default padding', err);
1195
+ this.map.fitBounds(this.bounds, { padding: 50, pitch: 65, maxZoom: 16 });
1196
+ }
1197
+ }
1198
+ }
1199
+ async clearAllRoutes() {
1200
+ // 1. Remove all markers
1201
+ this.markerOriginList.forEach(marker => marker.remove());
1202
+ this.markerDestinationList.forEach(marker => marker.remove());
1203
+ this.markerOriginList.clear();
1204
+ this.markerDestinationList.clear();
1205
+ // 2. Remove all arcs
1206
+ Object.keys(this.activeArcs).forEach(index => {
1207
+ const arc = this.activeArcs[index];
1208
+ if (arc.pathEl.parentNode)
1209
+ arc.pathEl.parentNode.removeChild(arc.pathEl);
1210
+ if (this.svgOverlay) {
1211
+ const grad = this.svgOverlay.querySelector(`defs #arc-grad-${index}`);
1212
+ if (grad && grad.parentNode)
1213
+ grad.parentNode.removeChild(grad);
1214
+ }
1215
+ });
1216
+ this.activeArcs = {};
1217
+ // 3. Remove all line layers and sources using registry metadata
1218
+ this.activeRoutesRegistry.forEach((data, regId) => {
1219
+ const type = data.routeType || 'jobcode';
1220
+ const idx = data.index || regId.split('-').pop();
1221
+ const layerId = `route-layer-${type}-${idx}`;
1222
+ const sourceId = `route-source-${type}-${idx}`;
1223
+ this.map.getLayer(layerId) ? this.map.removeLayer(layerId) : null;
1224
+ this.map.getSource(sourceId) ? this.map.removeSource(sourceId) : null;
1225
+ // Legacy fallback cleanup
1226
+ this.map.getLayer(`line${idx}`) ? this.map.removeLayer(`line${idx}`) : null;
1227
+ this.map.getSource(`line${idx}`) ? this.map.removeSource(`line${idx}`) : null;
1228
+ this.map.getLayer(`route-for-job-code${idx}`) ? this.map.removeLayer(`route-for-job-code${idx}`) : null;
1229
+ this.map.getSource(`route-source-for-job-code${idx}`) ? this.map.removeSource(`route-source-for-job-code${idx}`) : null;
1230
+ });
1231
+ // 4. Reset registries and bounds
1232
+ this.activeRoutesRegistry.clear();
1233
+ this.originDestinationCordinates = [];
1234
+ this.bounds = new mapboxgl.LngLatBounds();
1235
+ if (this.popup)
1236
+ this.popup.remove();
1237
+ return true;
1238
+ }
1239
+ clearBound() {
1240
+ this.bounds = new mapboxgl.LngLatBounds();
1241
+ this.originDestinationCordinates = [];
1242
+ this.clearPadding();
1243
+ }
1244
+ formateLatLong(latlong) {
1245
+ return latlong ? latlong.split(',').map((x) => +x).reverse() : null;
1246
+ }
1247
+ clearBoundWithCordinates() {
1248
+ this.bounds = new mapboxgl.LngLatBounds();
1249
+ this.originDestinationCordinates = [];
1250
+ }
1251
+ onResize(event) {
1252
+ if (!this.bounds.isEmpty() && this.map) {
1253
+ this.windowActualHeightWidth.availHeight =
1254
+ window.innerHeight > window.screen.availHeight ? window.innerHeight : window.screen.availHeight;
1255
+ setTimeout(() => {
1256
+ const hRes = event.target ? event.target.innerHeight : window.innerHeight;
1257
+ this.map.fitBounds(this.bounds, {
1258
+ padding: {
1259
+ top: (this.padding?.top || 0) + this.customTopForCustomer,
1260
+ bottom: (this.padding?.bottom || 0) + (this.windowActualHeightWidth.availHeight - (hRes - 65)),
1261
+ left: (this.padding?.left || 0),
1262
+ right: (this.padding?.right || 0),
1263
+ },
1264
+ pitch: 65,
1265
+ });
1266
+ }, 500);
1267
+ }
1268
+ }
1269
+ setWindowHeight(screen) {
1270
+ this.windowActualHeightWidth.availHeight = screen;
1271
+ }
1272
+ setPadding(padding) {
1273
+ this.padding = padding;
1274
+ }
1275
+ clearPadding() {
1276
+ this.padding = null;
1277
+ }
1278
+ removeJobFromMap(data) {
1279
+ data.forEach((ele, index) => {
1280
+ const id = ele['job_id'] ? ele['job_id'] : ele['route_id'];
1281
+ this.removeRouteAndMarker(id);
1282
+ if (index === data.length - 1) {
1283
+ this.extendReBound();
1284
+ }
1285
+ });
1286
+ }
1287
+ resize() {
1288
+ if (this.map) {
1289
+ setTimeout(() => this.map.resize(), 0);
1290
+ }
1291
+ }
1292
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MapboxService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1293
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MapboxService, providedIn: 'root' });
1294
+ }
1295
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MapboxService, decorators: [{
1296
+ type: Injectable,
1297
+ args: [{
1298
+ providedIn: 'root'
1299
+ }]
1300
+ }], ctorParameters: () => [] });
1301
+
1302
+ class CoolmapService {
1303
+ mapbox = inject(MapboxService);
1304
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1305
+ plottingIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "plottingIds" }] : []));
1306
+ plotRoute(route, i, type = 'jobcode', enablefitbound, showAllFitbound) {
1307
+ if (!route)
1308
+ return;
1309
+ return this.mapbox.plotRoute(route, i, type, enablefitbound, showAllFitbound);
1310
+ }
1311
+ loadMapProperty(pinRouteGeojson, index, unit, routeProps) {
1312
+ this.mapbox.loadMapProperty(pinRouteGeojson, index, unit, routeProps);
1313
+ }
1314
+ removeRouteAndMarker(index, type = 'jobcode') {
1315
+ return this.mapbox.removeRouteAndMarker(index, type);
1316
+ }
1317
+ clearAllRoutes() {
1318
+ return this.mapbox.clearAllRoutes();
1319
+ }
1320
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CoolmapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1321
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CoolmapService, providedIn: 'root' });
1322
+ }
1323
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: CoolmapService, decorators: [{
1324
+ type: Injectable,
1325
+ args: [{
1326
+ providedIn: 'root'
1327
+ }]
1328
+ }] });
1329
+
1330
+ /*
1331
+ * Public API Surface of coolmap-services
1332
+ */
1333
+
1334
+ /**
1335
+ * Generated bundle index. Do not edit.
1336
+ */
1337
+
1338
+ export { COOLMAP_CONFIG, CoolmapService, CoolmapServices, MapboxService, UtilsService, provideCoolmapConfig };
1339
+ //# sourceMappingURL=coolmap-services.mjs.map