becomap 1.5.70 → 1.5.72

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.
package/public/index.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
 
4
4
  <head>
@@ -22,43 +22,182 @@
22
22
 
23
23
  <body>
24
24
  <div id="mapContainer"></div>
25
-
26
- <!-- External libs -->
27
25
  <script src="https://unpkg.com/maplibre-gl@4.4.1/dist/maplibre-gl.js"></script>
28
26
  <script src="https://unpkg.com/@turf/turf@7.1.0/turf.min.js"></script>
29
-
30
- <!-- Core Map SDK Integration -->
31
- <script>
32
- // ============================================================================
27
+ <script>// ============================================================================
33
28
  // GLOBAL VARIABLES
34
29
  // ============================================================================
35
30
  window._mapView = null;
36
31
  window._site = null;
37
32
  window._eventListeners = new Map();
33
+ window._bridgeHealth = {
34
+ isConnected: false,
35
+ lastHeartbeat: null,
36
+ connectionAttempts: 0,
37
+ maxRetries: 3
38
+ };
39
+ window._operationQueue = [];
40
+ window._isProcessingQueue = false;
41
+ window._appState = 'initializing'; // 'initializing', 'ready', 'error', 'destroyed'
38
42
 
39
43
  // ============================================================================
40
44
  // UTILITY FUNCTIONS
41
45
  // ============================================================================
42
46
 
43
47
  /**
44
- * Sends messages to native platform (iOS/Android)
48
+ * Checks if native bridge is available
49
+ * @returns {boolean} - True if native bridge is available
50
+ */
51
+ function isNativeBridgeAvailable() {
52
+ return !!(window.webkit?.messageHandlers?.jsHandler || window.jsHandler?.postMessage);
53
+ }
54
+
55
+ /**
56
+ * Updates bridge health status
57
+ * @param {boolean} isConnected - Connection status
58
+ */
59
+ function updateBridgeHealth(isConnected) {
60
+ window._bridgeHealth.isConnected = isConnected;
61
+ window._bridgeHealth.lastHeartbeat = Date.now();
62
+
63
+ if (isConnected) {
64
+ window._bridgeHealth.connectionAttempts = 0;
65
+ processOperationQueue();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Sends messages to native platform with retry mechanism
45
71
  * @param {string} type - Message type
46
72
  * @param {any} payload - Message payload
73
+ * @param {boolean} skipQueue - Skip queue and send immediately
74
+ */
75
+ function notifyNative(type, payload = null, skipQueue = false) {
76
+ const message = { type, payload, timestamp: Date.now() };
77
+
78
+ if (!isNativeBridgeAvailable()) {
79
+ console.warn("Native handler not available:", message);
80
+
81
+ if (!skipQueue && window._bridgeHealth.connectionAttempts < window._bridgeHealth.maxRetries) {
82
+ queueOperation(() => notifyNative(type, payload, true));
83
+ return;
84
+ }
85
+
86
+ // Fallback: store in localStorage for debugging
87
+ try {
88
+ const failedMessages = JSON.parse(localStorage.getItem('becomap_failed_messages') || '[]');
89
+ failedMessages.push(message);
90
+ localStorage.setItem('becomap_failed_messages', JSON.stringify(failedMessages.slice(-50))); // Keep last 50
91
+ } catch (e) {
92
+ console.warn('Failed to store message in localStorage:', e);
93
+ }
94
+ return;
95
+ }
96
+
97
+ try {
98
+ const messageStr = JSON.stringify(message);
99
+
100
+ if (window.webkit?.messageHandlers?.jsHandler) {
101
+ // iOS
102
+ window.webkit.messageHandlers.jsHandler.postMessage(messageStr);
103
+ } else if (window.jsHandler?.postMessage) {
104
+ // Android
105
+ window.jsHandler.postMessage(messageStr);
106
+ }
107
+
108
+ updateBridgeHealth(true);
109
+ } catch (error) {
110
+ console.error('Failed to send message to native:', error, message);
111
+ updateBridgeHealth(false);
112
+
113
+ if (!skipQueue) {
114
+ queueOperation(() => notifyNative(type, payload, true));
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Queues an operation for later execution
121
+ * @param {Function} operation - Operation to queue
122
+ */
123
+ function queueOperation(operation) {
124
+ window._operationQueue.push({
125
+ operation,
126
+ timestamp: Date.now(),
127
+ retries: 0
128
+ });
129
+
130
+ // Process queue after a short delay
131
+ setTimeout(processOperationQueue, 100);
132
+ }
133
+
134
+ /**
135
+ * Processes queued operations
47
136
  */
48
- function notifyNative(type, payload = null) {
49
- const message = JSON.stringify({ type, payload });
50
-
51
- if (window.webkit?.messageHandlers?.jsHandler) {
52
- // iOS
53
- window.webkit.messageHandlers.jsHandler.postMessage(message);
54
- } else if (window.jsHandler?.postMessage) {
55
- // Android
56
- window.jsHandler.postMessage(message);
57
- } else {
58
- console.log("Native handler not available:", message);
137
+ function processOperationQueue() {
138
+ if (window._isProcessingQueue || !isNativeBridgeAvailable()) {
139
+ return;
140
+ }
141
+
142
+ window._isProcessingQueue = true;
143
+
144
+ try {
145
+ const now = Date.now();
146
+ const maxAge = 30000; // 30 seconds
147
+
148
+ // Remove expired operations
149
+ window._operationQueue = window._operationQueue.filter(item =>
150
+ now - item.timestamp < maxAge
151
+ );
152
+
153
+ // Process remaining operations
154
+ while (window._operationQueue.length > 0 && isNativeBridgeAvailable()) {
155
+ const item = window._operationQueue.shift();
156
+
157
+ try {
158
+ item.operation();
159
+ } catch (error) {
160
+ console.error('Error processing queued operation:', error);
161
+
162
+ // Retry failed operations up to 3 times
163
+ if (item.retries < 3) {
164
+ item.retries++;
165
+ window._operationQueue.unshift(item);
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ } finally {
171
+ window._isProcessingQueue = false;
59
172
  }
60
173
  }
61
174
 
175
+ /**
176
+ * Throttle function to prevent event flooding
177
+ * @param {Function} func - Function to throttle
178
+ * @param {number} delay - Delay in milliseconds
179
+ * @returns {Function} - Throttled function
180
+ */
181
+ function throttle(func, delay) {
182
+ let timeoutId;
183
+ let lastExecTime = 0;
184
+
185
+ return function (...args) {
186
+ const currentTime = Date.now();
187
+
188
+ if (currentTime - lastExecTime > delay) {
189
+ func.apply(this, args);
190
+ lastExecTime = currentTime;
191
+ } else {
192
+ clearTimeout(timeoutId);
193
+ timeoutId = setTimeout(() => {
194
+ func.apply(this, args);
195
+ lastExecTime = Date.now();
196
+ }, delay - (currentTime - lastExecTime));
197
+ }
198
+ };
199
+ }
200
+
62
201
  /**
63
202
  * Safely converts objects to JSON with error handling
64
203
  * @param {any} obj - Object to convert
@@ -74,20 +213,97 @@
74
213
  }
75
214
 
76
215
  /**
77
- * Executes function with error handling and native notification
216
+ * Executes function with enhanced error handling, timeout, and retry mechanism
78
217
  * @param {Function} fn - Function to execute
79
218
  * @param {string} errorType - Error type for native notification
219
+ * @param {any} errorDetails - Additional error details to include in the payload
80
220
  * @param {any} fallbackValue - Fallback value if function fails
221
+ * @param {number} timeout - Timeout in milliseconds (default: 5000)
222
+ * @param {number} maxRetries - Maximum retry attempts (default: 0)
81
223
  */
82
- function executeWithErrorHandling(fn, errorType, fallbackValue = null) {
83
- try {
84
- const result = fn();
85
- return result !== undefined ? result : fallbackValue;
86
- } catch (error) {
87
- console.error(`${errorType} error:`, error);
88
- notifyNative(errorType, { message: error.message });
89
- return fallbackValue;
90
- }
224
+ function executeWithErrorHandling(fn, errorType, errorDetails = null, fallbackValue = null, timeout = 5000, maxRetries = 0) {
225
+ return new Promise((resolve) => {
226
+ let retryCount = 0;
227
+
228
+ function attemptExecution() {
229
+ try {
230
+ // Validate pre-conditions
231
+ if (window._appState === 'destroyed') {
232
+ throw new Error('Application has been destroyed');
233
+ }
234
+
235
+ if (!window._mapView && errorDetails?.operation !== 'init') {
236
+ throw new Error('MapView not initialized');
237
+ }
238
+
239
+ // Set up timeout
240
+ const timeoutId = setTimeout(() => {
241
+ throw new Error(`Operation timed out after ${timeout}ms`);
242
+ }, timeout);
243
+
244
+ const result = fn();
245
+ clearTimeout(timeoutId);
246
+
247
+ // Handle promises
248
+ if (result && typeof result.then === 'function') {
249
+ result
250
+ .then(res => resolve(res !== undefined ? res : fallbackValue))
251
+ .catch(handleError);
252
+ } else {
253
+ resolve(result !== undefined ? result : fallbackValue);
254
+ }
255
+
256
+ } catch (error) {
257
+ handleError(error);
258
+ }
259
+ }
260
+
261
+ function handleError(error) {
262
+ console.error(`${errorType} error (attempt ${retryCount + 1}):`, error);
263
+
264
+ const errorPayload = {
265
+ message: error.message,
266
+ stack: error.stack,
267
+ timestamp: Date.now(),
268
+ attempt: retryCount + 1,
269
+ maxRetries: maxRetries + 1,
270
+ appState: window._appState,
271
+ bridgeHealth: { ...window._bridgeHealth },
272
+ ...errorDetails
273
+ };
274
+
275
+ // Retry logic for transient errors
276
+ if (retryCount < maxRetries && isRetriableError(error)) {
277
+ retryCount++;
278
+ const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000); // Exponential backoff
279
+ setTimeout(attemptExecution, delay);
280
+ return;
281
+ }
282
+
283
+ notifyNative(errorType, errorPayload);
284
+ resolve(fallbackValue);
285
+ }
286
+
287
+ attemptExecution();
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Determines if an error is retriable
293
+ * @param {Error} error - The error to check
294
+ * @returns {boolean} - True if error is retriable
295
+ */
296
+ function isRetriableError(error) {
297
+ const retriableMessages = [
298
+ 'network error',
299
+ 'timeout',
300
+ 'connection failed',
301
+ 'temporary failure'
302
+ ];
303
+
304
+ return retriableMessages.some(msg =>
305
+ error.message.toLowerCase().includes(msg)
306
+ );
91
307
  }
92
308
 
93
309
  // ============================================================================
@@ -109,44 +325,100 @@
109
325
 
110
326
  // Map load event
111
327
  const loadListenerId = mapView.eventsHandler.on('load', () => {
112
- notifyNative("mapLoad", { timestamp: Date.now() });
328
+ try {
329
+ window._appState = 'ready';
330
+ notifyNative("onRenderComplete", {
331
+ site: safeToJSON(window._site),
332
+ timestamp: Date.now(),
333
+ appState: window._appState
334
+ });
335
+ } catch (error) {
336
+ console.error('Error in load event handler:', error);
337
+ notifyNative("onError", {
338
+ message: error.message,
339
+ event: 'load',
340
+ timestamp: Date.now()
341
+ });
342
+ }
113
343
  });
114
344
 
115
- // View change event
116
- const viewChangeListenerId = mapView.eventsHandler.on('viewChange', (args) => {
117
- notifyNative("viewChange", {
118
- viewOptions: safeToJSON(args.viewOptions),
119
- timestamp: Date.now()
120
- });
121
- });
345
+ // View change event (throttled to prevent flooding)
346
+ const throttledViewChange = throttle((args) => {
347
+ try {
348
+ notifyNative("onViewChange", {
349
+ viewOptions: safeToJSON(args.viewOptions),
350
+ timestamp: Date.now()
351
+ });
352
+ } catch (error) {
353
+ console.error('Error in viewChange event handler:', error);
354
+ }
355
+ }, 100); // Throttle to max 10 events per second
356
+
357
+ const viewChangeListenerId = mapView.eventsHandler.on('viewChange', throttledViewChange);
122
358
 
123
359
  // Location selection event
124
360
  const selectListenerId = mapView.eventsHandler.on('select', (args) => {
125
- notifyNative("locationSelect", {
126
- locations: args.locations?.map(loc => safeToJSON(loc)) || [],
127
- timestamp: Date.now()
128
- });
361
+ try {
362
+ notifyNative("onLocationSelect", {
363
+ locations: args.locations?.map(loc => safeToJSON(loc)) || [],
364
+ timestamp: Date.now()
365
+ });
366
+ } catch (error) {
367
+ console.error('Error in select event handler:', error);
368
+ notifyNative("onError", {
369
+ message: error.message,
370
+ event: 'select',
371
+ timestamp: Date.now()
372
+ });
373
+ }
129
374
  });
130
375
 
131
376
  // Floor switch event
132
377
  const switchToFloorListenerId = mapView.eventsHandler.on('switchToFloor', (args) => {
133
- notifyNative("floorSwitch", {
134
- floor: safeToJSON(args.floor),
135
- timestamp: Date.now()
136
- });
378
+ try {
379
+ notifyNative("onFloorSwitch", {
380
+ floor: safeToJSON(args.floor),
381
+ timestamp: Date.now()
382
+ });
383
+ } catch (error) {
384
+ console.error('Error in switchToFloor event handler:', error);
385
+ notifyNative("onError", {
386
+ message: error.message,
387
+ event: 'switchToFloor',
388
+ timestamp: Date.now()
389
+ });
390
+ }
137
391
  });
138
392
 
139
393
  // Route step load event
140
394
  const stepLoadListenerId = mapView.eventsHandler.on('stepLoad', (args) => {
141
- notifyNative("stepLoad", {
142
- step: safeToJSON(args.step),
143
- timestamp: Date.now()
144
- });
395
+ try {
396
+ notifyNative("onStepLoad", {
397
+ step: safeToJSON(args.step),
398
+ timestamp: Date.now()
399
+ });
400
+ } catch (error) {
401
+ console.error('Error in stepLoad event handler:', error);
402
+ notifyNative("onError", {
403
+ message: error.message,
404
+ event: 'stepLoad',
405
+ timestamp: Date.now()
406
+ });
407
+ }
145
408
  });
146
409
 
147
410
  // Walkthrough end event
148
411
  const walkthroughEndListenerId = mapView.eventsHandler.on('walkthroughEnd', () => {
149
- notifyNative("walkthroughEnd", { timestamp: Date.now() });
412
+ try {
413
+ notifyNative("onWalkthroughEnd", { timestamp: Date.now() });
414
+ } catch (error) {
415
+ console.error('Error in walkthroughEnd event handler:', error);
416
+ notifyNative("onError", {
417
+ message: error.message,
418
+ event: 'walkthroughEnd',
419
+ timestamp: Date.now()
420
+ });
421
+ }
150
422
  });
151
423
 
152
424
  // Store listener IDs for cleanup
@@ -182,36 +454,135 @@
182
454
  // ============================================================================
183
455
 
184
456
  /**
185
- * Initializes the map with site options
457
+ * Initializes the map with site options and enhanced error handling
186
458
  * @param {Object} siteOptions - Site configuration options
187
459
  */
188
- async function init(siteOptions) {
460
+ function init(siteOptions) {
461
+ // Validate input parameters
462
+ if (!siteOptions || typeof siteOptions !== 'object') {
463
+ const error = new Error('Invalid siteOptions provided');
464
+ notifyNative("onError", {
465
+ message: error.message,
466
+ timestamp: Date.now(),
467
+ operation: "init",
468
+ siteOptions: siteOptions
469
+ });
470
+ return;
471
+ }
472
+
473
+ // Check if already initialized
474
+ if (window._appState === 'ready' && window._mapView) {
475
+ console.warn('Map already initialized');
476
+ notifyNative("onRenderComplete", {
477
+ site: safeToJSON(window._site),
478
+ timestamp: Date.now(),
479
+ appState: window._appState
480
+ });
481
+ return;
482
+ }
483
+
484
+ window._appState = 'initializing';
485
+
189
486
  try {
190
487
  const container = document.getElementById('mapContainer');
191
488
  if (!container) {
192
489
  throw new Error('Map container not found');
193
490
  }
194
491
 
195
- if (!window.becomap?.getSite || !window.becomap?.getMapView) {
196
- throw new Error('Becomap UMD not loaded');
197
- }
198
-
199
- const mapOptions = { zoom: 18.5 };
200
- window._site = await window.becomap.getSite(siteOptions);
201
- window._mapView = await window.becomap.getMapView(container, window._site, mapOptions);
202
-
203
- // Setup event listeners
204
- setupMapViewEventListeners(window._mapView);
492
+ // Check if Becomap UMD is loaded with timeout
493
+ const checkBecomapLoaded = () => {
494
+ return new Promise((resolve, reject) => {
495
+ const maxAttempts = 50; // 5 seconds with 100ms intervals
496
+ let attempts = 0;
497
+
498
+ const checkInterval = setInterval(() => {
499
+ attempts++;
500
+
501
+ if (window.becomap?.getSite && window.becomap?.getMapView) {
502
+ clearInterval(checkInterval);
503
+ resolve();
504
+ } else if (attempts >= maxAttempts) {
505
+ clearInterval(checkInterval);
506
+ reject(new Error('Becomap UMD failed to load within timeout'));
507
+ }
508
+ }, 100);
509
+ });
510
+ };
511
+
512
+ // Initialize with proper error handling and timeouts
513
+ checkBecomapLoaded()
514
+ .then(() => {
515
+ const mapOptions = { zoom: 18.5 };
516
+
517
+ // Add timeout to getSite
518
+ const getSiteWithTimeout = Promise.race([
519
+ window.becomap.getSite(siteOptions),
520
+ new Promise((_, reject) =>
521
+ setTimeout(() => reject(new Error('getSite timeout')), 10000)
522
+ )
523
+ ]);
524
+
525
+ return getSiteWithTimeout;
526
+ })
527
+ .then(site => {
528
+ if (!site) {
529
+ throw new Error('Failed to load site data');
530
+ }
531
+
532
+ window._site = site;
533
+
534
+ // Add timeout to getMapView
535
+ const getMapViewWithTimeout = Promise.race([
536
+ window.becomap.getMapView(container, site, { zoom: 18.5 }),
537
+ new Promise((_, reject) =>
538
+ setTimeout(() => reject(new Error('getMapView timeout')), 15000)
539
+ )
540
+ ]);
541
+
542
+ return getMapViewWithTimeout;
543
+ })
544
+ .then(mapView => {
545
+ if (!mapView) {
546
+ throw new Error('Failed to create map view');
547
+ }
548
+
549
+ window._mapView = mapView;
550
+
551
+ // Setup event listeners
552
+ setupMapViewEventListeners(mapView);
553
+
554
+ // Initialize bridge health monitoring
555
+ updateBridgeHealth(isNativeBridgeAvailable());
556
+
557
+ console.log('Map initialization completed successfully');
558
+ })
559
+ .catch(err => {
560
+ window._appState = 'error';
561
+ console.error('Init error:', err);
562
+
563
+ notifyNative("onError", {
564
+ message: err.message,
565
+ stack: err.stack,
566
+ timestamp: Date.now(),
567
+ operation: "init",
568
+ siteOptions,
569
+ appState: window._appState,
570
+ containerExists: !!document.getElementById('mapContainer'),
571
+ becomapLoaded: !!(window.becomap?.getSite && window.becomap?.getMapView)
572
+ });
573
+ });
205
574
 
206
- notifyNative("onRenderComplete", {
207
- site: safeToJSON(window._site),
208
- timestamp: Date.now()
209
- });
210
575
  } catch (err) {
211
- console.error('Init error:', err);
212
- notifyNative("initError", {
576
+ window._appState = 'error';
577
+ console.error('Init synchronous error:', err);
578
+
579
+ notifyNative("onError", {
213
580
  message: err.message,
214
- timestamp: Date.now()
581
+ stack: err.stack,
582
+ timestamp: Date.now(),
583
+ operation: "init",
584
+ siteOptions,
585
+ appState: window._appState
215
586
  });
216
587
  }
217
588
  }
@@ -222,68 +593,104 @@
222
593
 
223
594
  // Floor and Location Methods
224
595
  globalThis.getCurrentFloor = () => {
225
- const floor = executeWithErrorHandling(
596
+ executeWithErrorHandling(
226
597
  () => window._mapView?.currentFloor,
227
- "getCurrentFloorError"
228
- );
229
- notifyNative("getCurrentFloor", safeToJSON(floor));
598
+ "onError",
599
+ { operation: "getCurrentFloor" }
600
+ ).then(floor => {
601
+ notifyNative("onGetCurrentFloor", safeToJSON(floor));
602
+ }).catch(error => {
603
+ console.error('Error in getCurrentFloor:', error);
604
+ notifyNative("onGetCurrentFloor", null);
605
+ });
230
606
  };
231
607
 
232
608
  globalThis.selectFloorWithId = (floor) => {
233
609
  executeWithErrorHandling(
234
610
  () => window._mapView?.selectFloorWithId(floor),
235
- "selectFloorError"
611
+ "onError",
612
+ { operation: "selectFloorWithId", floor }
236
613
  );
237
614
  };
238
615
 
239
616
  globalThis.selectLocationWithId = (location) => {
240
617
  executeWithErrorHandling(
241
618
  () => window._mapView?.selectLocationWithId(location),
242
- "selectLocationError"
619
+ "onError",
620
+ { operation: "selectLocationWithId", location }
243
621
  );
244
622
  };
245
623
 
246
624
  // Data Retrieval Methods
247
625
  globalThis.getCategories = () => {
248
- const categories = executeWithErrorHandling(
626
+ executeWithErrorHandling(
249
627
  () => window._mapView?.getCategories(),
250
- "getCategoriesError",
628
+ "onError",
629
+ { operation: "getCategories" },
251
630
  []
252
- );
253
- notifyNative("getCategories", categories?.map(cat => safeToJSON(cat)) || []);
631
+ ).then(categories => {
632
+ // Ensure categories is an array before mapping
633
+ const categoriesArray = Array.isArray(categories) ? categories : [];
634
+ notifyNative("onGetCategories", categoriesArray.map(cat => safeToJSON(cat)));
635
+ }).catch(error => {
636
+ console.error('Error in getCategories:', error);
637
+ notifyNative("onGetCategories", []);
638
+ });
254
639
  };
255
640
 
256
641
  globalThis.getLocations = () => {
257
- const locations = executeWithErrorHandling(
642
+ executeWithErrorHandling(
258
643
  () => window._mapView?.getLocations(),
259
- "getLocationsError",
644
+ "onError",
645
+ { operation: "getLocations" },
260
646
  []
261
- );
262
- notifyNative("getLocations", locations?.map(loc => safeToJSON(loc)) || []);
647
+ ).then(locations => {
648
+ // Ensure locations is an array before mapping
649
+ const locationsArray = Array.isArray(locations) ? locations : [];
650
+ notifyNative("onGetLocations", locationsArray.map(loc => safeToJSON(loc)));
651
+ }).catch(error => {
652
+ console.error('Error in getLocations:', error);
653
+ notifyNative("onGetLocations", []);
654
+ });
263
655
  };
264
656
 
265
- globalThis.getAllAmenities = () => {
266
- const amenities = executeWithErrorHandling(
657
+ globalThis.getAmenities = () => {
658
+ executeWithErrorHandling(
267
659
  () => window._mapView?.getAllAminityLocations(),
268
- "getAllAmenitiesError",
660
+ "onError",
661
+ { operation: "getAmenities" },
269
662
  []
270
- );
271
- notifyNative("getAllAmenities", amenities?.map(amenity => safeToJSON(amenity)) || []);
663
+ ).then(amenities => {
664
+ // Ensure amenities is an array before mapping
665
+ const amenitiesArray = Array.isArray(amenities) ? amenities : [];
666
+ notifyNative("onGetAmenities", amenitiesArray.map(amenity => safeToJSON(amenity)));
667
+ }).catch(error => {
668
+ console.error('Error in getAmenities:', error);
669
+ notifyNative("onGetAmenities", []);
670
+ });
272
671
  };
273
672
 
274
- globalThis.getAmenities = () => {
275
- const amenities = executeWithErrorHandling(
673
+ globalThis.getAmenityTypes = () => {
674
+ executeWithErrorHandling(
276
675
  () => window._mapView?.getAmenities(),
277
- "getAmenitiesError",
676
+ "onError",
677
+ { operation: "getAmenityTypes" },
278
678
  []
279
- );
280
- notifyNative("getAmenities", amenities?.map(amenity => safeToJSON(amenity)) || []);
679
+ ).then(amenities => {
680
+ // Ensure amenities is an array before mapping
681
+ const amenitiesArray = Array.isArray(amenities) ? amenities : [];
682
+ notifyNative("onGetAmenityTypes", amenitiesArray.map(amenity => safeToJSON(amenity)));
683
+ }).catch(error => {
684
+ console.error('Error in getAmenityTypes:', error);
685
+ notifyNative("onGetAmenityTypes", []);
686
+ });
281
687
  };
282
688
 
283
689
  globalThis.selectAmenities = (type) => {
284
690
  executeWithErrorHandling(
285
691
  () => window._mapView?.selectAmenities(type),
286
- "selectAmenitiesError"
692
+ "onError",
693
+ { operation: "selectAmenities", type }
287
694
  );
288
695
  };
289
696
 
@@ -291,41 +698,43 @@
291
698
  globalThis.getSessionId = async () => {
292
699
  try {
293
700
  const sessionId = await window._mapView?.getSessionId();
294
- notifyNative("getSessionId", sessionId);
701
+ notifyNative("onGetSessionId", sessionId);
295
702
  } catch (err) {
296
- notifyNative("getSessionIdError", {
703
+ notifyNative("onError", {
297
704
  message: err.message,
298
- timestamp: Date.now()
705
+ timestamp: Date.now(),
706
+ operation: "getSessionId"
299
707
  });
300
708
  }
301
709
  };
302
710
 
303
- globalThis.getQuestions = () => {
304
- const questions = executeWithErrorHandling(
305
- () => window._mapView?.getQuestions(),
306
- "getQuestionsError",
307
- []
308
- );
309
- notifyNative("getQuestions", questions?.map(q => safeToJSON(q)) || []);
310
- };
311
-
312
711
  globalThis.getHappenings = (type) => {
313
- const happenings = executeWithErrorHandling(
712
+ executeWithErrorHandling(
314
713
  () => window._mapView?.getHappenings(type),
315
- "getHappeningsError",
714
+ "onError",
715
+ { operation: "getHappenings", type },
316
716
  []
317
- );
318
- notifyNative("getHappenings", happenings?.map(h => safeToJSON(h)) || []);
717
+ ).then(happenings => {
718
+ // Ensure happenings is an array before mapping
719
+ const happeningsArray = Array.isArray(happenings) ? happenings : [];
720
+ notifyNative("onGetHappenings", happeningsArray.map(h => safeToJSON(h)));
721
+ }).catch(error => {
722
+ console.error('Error in getHappenings:', error);
723
+ notifyNative("onGetHappenings", []);
724
+ });
319
725
  };
320
726
 
321
727
  globalThis.getEventSuggestions = async (sessionId, answers) => {
322
728
  try {
323
729
  const suggestions = await window._mapView?.getEventSuggestions(sessionId, answers);
324
- notifyNative("getEventSuggestions", suggestions?.map(s => safeToJSON(s)) || []);
730
+ notifyNative("onGetEventSuggestions", suggestions?.map(s => safeToJSON(s)) || []);
325
731
  } catch (err) {
326
- notifyNative("getEventSuggestionsError", {
732
+ notifyNative("onError", {
327
733
  message: err.message,
328
- timestamp: Date.now()
734
+ timestamp: Date.now(),
735
+ operation: "getEventSuggestions",
736
+ sessionId,
737
+ answers
329
738
  });
330
739
  }
331
740
  };
@@ -334,70 +743,79 @@
334
743
  globalThis.focusTo = (location, zoom, bearing, pitch) => {
335
744
  executeWithErrorHandling(
336
745
  () => window._mapView?.focusTo(location, zoom, bearing, pitch),
337
- "focusToError"
746
+ "onError",
747
+ { operation: "focusTo", location, zoom, bearing, pitch }
338
748
  );
339
749
  };
340
750
 
341
751
  globalThis.clearSelection = () => {
342
752
  executeWithErrorHandling(
343
753
  () => window._mapView?.clearSelection(),
344
- "clearSelectionError"
754
+ "onError",
755
+ { operation: "clearSelection" }
345
756
  );
346
757
  };
347
758
 
348
759
  globalThis.updateZoom = (zoom) => {
349
760
  executeWithErrorHandling(
350
761
  () => window._mapView?.updateZoom(zoom),
351
- "updateZoomError"
762
+ "onError",
763
+ { operation: "updateZoom", zoom }
352
764
  );
353
765
  };
354
766
 
355
767
  globalThis.updatePitch = (pitch) => {
356
768
  executeWithErrorHandling(
357
769
  () => window._mapView?.updatePitch(pitch),
358
- "updatePitchError"
770
+ "onError",
771
+ { operation: "updatePitch", pitch }
359
772
  );
360
773
  };
361
774
 
362
775
  globalThis.updateBearing = (bearing) => {
363
776
  executeWithErrorHandling(
364
777
  () => window._mapView?.updateBearing(bearing),
365
- "updateBearingError"
778
+ "onError",
779
+ { operation: "updateBearing", bearing }
366
780
  );
367
781
  };
368
782
 
369
783
  globalThis.enableMultiSelection = (val) => {
370
784
  executeWithErrorHandling(
371
785
  () => window._mapView?.enableMultiSelection(val),
372
- "enableMultiSelectionError"
786
+ "onError",
787
+ { operation: "enableMultiSelection", value: val }
373
788
  );
374
789
  };
375
790
 
376
791
  globalThis.setBounds = (sw, ne) => {
377
792
  executeWithErrorHandling(
378
793
  () => window._mapView?.setBounds(sw, ne),
379
- "setBoundsError"
794
+ "onError",
795
+ { operation: "setBounds", southwest: sw, northeast: ne }
380
796
  );
381
797
  };
382
798
 
383
799
  globalThis.setViewport = (options) => {
384
800
  executeWithErrorHandling(
385
801
  () => window._mapView?.setViewport(options),
386
- "setViewportError"
802
+ "onError",
803
+ { operation: "setViewport", options }
387
804
  );
388
805
  };
389
806
 
390
807
  globalThis.resetDefaultViewport = (options) => {
391
808
  executeWithErrorHandling(
392
809
  () => window._mapView?.resetDefaultViewport(options),
393
- "resetDefaultViewportError"
810
+ "onError",
811
+ { operation: "resetDefaultViewport", options }
394
812
  );
395
813
  };
396
814
 
397
815
  // Search Methods
398
816
  globalThis.searchForLocations = (q, callbackId) => {
399
817
  if (!window._mapView?.searchForLocations) {
400
- notifyNative("searchForLocations", {
818
+ notifyNative("onSearchForLocations", {
401
819
  callbackId,
402
820
  results: [],
403
821
  error: "Search method not available"
@@ -406,7 +824,7 @@
406
824
  }
407
825
 
408
826
  window._mapView.searchForLocations(q, (matches) => {
409
- notifyNative("searchForLocations", {
827
+ notifyNative("onSearchForLocations", {
410
828
  callbackId,
411
829
  results: matches?.map(m => safeToJSON(m)) || []
412
830
  });
@@ -415,7 +833,7 @@
415
833
 
416
834
  globalThis.searchForCategories = (q, callbackId) => {
417
835
  if (!window._mapView?.searchForCategories) {
418
- notifyNative("searchForCategories", {
836
+ notifyNative("onSearchForCategories", {
419
837
  callbackId,
420
838
  results: [],
421
839
  error: "Search method not available"
@@ -424,7 +842,7 @@
424
842
  }
425
843
 
426
844
  window._mapView.searchForCategories(q, (matches) => {
427
- notifyNative("searchForCategories", {
845
+ notifyNative("onSearchForCategories", {
428
846
  callbackId,
429
847
  results: matches?.map(m => safeToJSON(m)) || []
430
848
  });
@@ -438,33 +856,53 @@
438
856
  globalThis.getRoute = (startID, goalID, waypoints = [], routeOptions) => {
439
857
  try {
440
858
  const routes = window.becomap.getRouteById(startID, goalID, waypoints, routeOptions);
441
- notifyNative("getRoute", routes?.map(route => safeToJSON(route)) || []);
859
+ notifyNative("onGetRoute", routes?.map(route => safeToJSON(route)) || []);
442
860
  } catch (error) {
443
- notifyNative("getRouteError", {
861
+ notifyNative("onError", {
444
862
  message: error.message,
445
- timestamp: Date.now()
863
+ timestamp: Date.now(),
864
+ operation: "getRoute",
865
+ startID,
866
+ goalID,
867
+ waypoints,
868
+ routeOptions
446
869
  });
447
870
  }
448
871
  };
449
872
 
450
- globalThis.showRoute = () => {
873
+ globalThis.showRoute = (segmentOrderIndex) => {
451
874
  executeWithErrorHandling(
452
- () => window._mapView?.routeController?.showSavedRoute(),
453
- "showRouteError"
875
+ () => {
876
+ const routeController = window._mapView?.routeController;
877
+ if (!routeController) return;
878
+
879
+ if (segmentOrderIndex !== undefined) {
880
+ routeController.showSegmentByOrderIndex(segmentOrderIndex);
881
+ } else {
882
+ const segments = routeController.segments;
883
+ if (segments && segments.length > 0) {
884
+ routeController.showRoute(segments);
885
+ }
886
+ }
887
+ },
888
+ "onError",
889
+ { operation: "showRoute", segmentOrderIndex }
454
890
  );
455
891
  };
456
892
 
457
893
  globalThis.showStep = (step) => {
458
894
  executeWithErrorHandling(
459
895
  () => window._mapView?.routeController?.showStepByOrderIndex(step),
460
- "showStepError"
896
+ "onError",
897
+ { operation: "showStep", step }
461
898
  );
462
899
  };
463
900
 
464
901
  globalThis.clearAllRoutes = () => {
465
902
  executeWithErrorHandling(
466
903
  () => window._mapView?.routeController?.clearAllRoutes(),
467
- "clearAllRoutesError"
904
+ "onError",
905
+ { operation: "clearAllRoutes" }
468
906
  );
469
907
  };
470
908
 
@@ -473,28 +911,180 @@
473
911
  // ============================================================================
474
912
 
475
913
  /**
476
- * Cleanup function to be called when the webview is destroyed
914
+ * Enhanced cleanup function with proper resource management
477
915
  */
478
916
  globalThis.cleanup = () => {
479
- clearMapViewEventListeners();
480
- window._mapView = null;
481
- window._site = null;
917
+ try {
918
+ console.log('Starting cleanup process...');
919
+ window._appState = 'destroyed';
920
+
921
+ // Clear event listeners
922
+ clearMapViewEventListeners();
923
+
924
+ // Clear operation queue
925
+ window._operationQueue = [];
926
+ window._isProcessingQueue = false;
927
+
928
+ // Reset bridge health
929
+ window._bridgeHealth = {
930
+ isConnected: false,
931
+ lastHeartbeat: null,
932
+ connectionAttempts: 0,
933
+ maxRetries: 3
934
+ };
935
+
936
+ // Cleanup map view
937
+ if (window._mapView && typeof window._mapView.destroy === 'function') {
938
+ window._mapView.destroy();
939
+ }
940
+ window._mapView = null;
941
+
942
+ // Clear site data
943
+ window._site = null;
944
+
945
+ // Clear any stored failed messages
946
+ try {
947
+ localStorage.removeItem('becomap_failed_messages');
948
+ } catch (e) {
949
+ console.warn('Failed to clear localStorage:', e);
950
+ }
951
+
952
+ console.log('Cleanup completed successfully');
953
+
954
+ // Final notification to native
955
+ notifyNative("onCleanupComplete", {
956
+ timestamp: Date.now(),
957
+ appState: window._appState
958
+ }, true); // Skip queue for final message
959
+
960
+ } catch (error) {
961
+ console.error('Error during cleanup:', error);
962
+ notifyNative("onError", {
963
+ message: error.message,
964
+ operation: "cleanup",
965
+ timestamp: Date.now()
966
+ }, true);
967
+ }
968
+ };
969
+
970
+ /**
971
+ * Application state management
972
+ */
973
+ globalThis.getAppState = () => {
974
+ const state = {
975
+ appState: window._appState,
976
+ bridgeHealth: { ...window._bridgeHealth },
977
+ queueLength: window._operationQueue.length,
978
+ hasMapView: !!window._mapView,
979
+ hasSite: !!window._site,
980
+ timestamp: Date.now()
981
+ };
982
+
983
+ notifyNative("onGetAppState", state);
984
+ return state;
985
+ };
986
+
987
+ /**
988
+ * Health check function for native to verify bridge connectivity
989
+ */
990
+ globalThis.healthCheck = () => {
991
+ const healthData = {
992
+ timestamp: Date.now(),
993
+ appState: window._appState,
994
+ bridgeConnected: isNativeBridgeAvailable(),
995
+ mapViewReady: !!window._mapView,
996
+ siteLoaded: !!window._site,
997
+ queueLength: window._operationQueue.length,
998
+ lastHeartbeat: window._bridgeHealth.lastHeartbeat
999
+ };
1000
+
1001
+ updateBridgeHealth(true); // Update heartbeat
1002
+ notifyNative("onHealthCheck", healthData);
1003
+
1004
+ return healthData;
482
1005
  };
483
1006
 
1007
+ /**
1008
+ * Error recovery function
1009
+ */
1010
+ globalThis.recoverFromError = () => {
1011
+ try {
1012
+ console.log('Attempting error recovery...');
1013
+
1014
+ // Reset app state if in error
1015
+ if (window._appState === 'error') {
1016
+ window._appState = 'initializing';
1017
+ }
1018
+
1019
+ // Clear failed operations
1020
+ window._operationQueue = [];
1021
+ window._isProcessingQueue = false;
1022
+
1023
+ // Reset bridge health
1024
+ updateBridgeHealth(isNativeBridgeAvailable());
1025
+
1026
+ // Process any pending operations
1027
+ processOperationQueue();
1028
+
1029
+ notifyNative("onErrorRecovery", {
1030
+ timestamp: Date.now(),
1031
+ appState: window._appState,
1032
+ bridgeHealth: { ...window._bridgeHealth }
1033
+ });
1034
+
1035
+ console.log('Error recovery completed');
1036
+
1037
+ } catch (error) {
1038
+ console.error('Error during recovery:', error);
1039
+ notifyNative("onError", {
1040
+ message: error.message,
1041
+ operation: "recoverFromError",
1042
+ timestamp: Date.now()
1043
+ });
1044
+ }
1045
+ };
1046
+
1047
+ /**
1048
+ * Debug information function
1049
+ */
1050
+ globalThis.getDebugInfo = () => {
1051
+ const debugInfo = {
1052
+ timestamp: Date.now(),
1053
+ appState: window._appState,
1054
+ bridgeHealth: { ...window._bridgeHealth },
1055
+ queueLength: window._operationQueue.length,
1056
+ hasMapView: !!window._mapView,
1057
+ hasSite: !!window._site,
1058
+ eventListeners: window._eventListeners.size,
1059
+ userAgent: navigator.userAgent,
1060
+ url: window.location.href,
1061
+ becomapLoaded: !!(window.becomap?.getSite && window.becomap?.getMapView),
1062
+ containerExists: !!document.getElementById('mapContainer')
1063
+ };
1064
+
1065
+ // Get failed messages from localStorage
1066
+ try {
1067
+ const failedMessages = JSON.parse(localStorage.getItem('becomap_failed_messages') || '[]');
1068
+ debugInfo.failedMessagesCount = failedMessages.length;
1069
+ debugInfo.lastFailedMessage = failedMessages[failedMessages.length - 1];
1070
+ } catch (e) {
1071
+ debugInfo.failedMessagesError = e.message;
1072
+ }
1073
+
1074
+ notifyNative("onGetDebugInfo", debugInfo);
1075
+ return debugInfo;
1076
+ };
1077
+
1078
+ // Initialize bridge health monitoring
1079
+ updateBridgeHealth(isNativeBridgeAvailable());
1080
+
1081
+ // Set up periodic health checks
1082
+ setInterval(() => {
1083
+ if (window._appState !== 'destroyed') {
1084
+ updateBridgeHealth(isNativeBridgeAvailable());
1085
+ }
1086
+ }, 5000); // Check every 5 seconds
1087
+
484
1088
  // Export init function
485
- globalThis.init = init;
486
- </script>
487
-
488
- <!-- <script>
489
- const siteOptions = {
490
- clientId: "c079dfa3a77dad13351cfacd95841c2c2780fe08",
491
- clientSecret: "f62a59675b2a47ddb75f1f994d88e653",
492
- siteIdentifier: "67dcf5dd2f21c64e3225254f"
493
- };
494
- window.addEventListener("DOMContentLoaded", () => {
495
- init(siteOptions);
496
- });
497
- </script> -->
498
- </body>
499
-
500
- </html>
1089
+ globalThis.init = init;</script>
1090
+ </body>