becomap 1.5.69 → 1.5.71

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,1090 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>Becomap UMD SDK</title>
8
+ <style>
9
+ body,
10
+ html {
11
+ margin: 0;
12
+ padding: 0;
13
+ height: 100%;
14
+ }
15
+
16
+ #mapContainer {
17
+ width: 100%;
18
+ height: 100%;
19
+ }
20
+ </style>
21
+ </head>
22
+
23
+ <body>
24
+ <div id="mapContainer"></div>
25
+ <script src="https://unpkg.com/maplibre-gl@4.4.1/dist/maplibre-gl.js"></script>
26
+ <script src="https://unpkg.com/@turf/turf@7.1.0/turf.min.js"></script>
27
+ <script>// ============================================================================
28
+ // GLOBAL VARIABLES
29
+ // ============================================================================
30
+ window._mapView = null;
31
+ window._site = null;
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'
42
+
43
+ // ============================================================================
44
+ // UTILITY FUNCTIONS
45
+ // ============================================================================
46
+
47
+ /**
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
71
+ * @param {string} type - Message type
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
136
+ */
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;
172
+ }
173
+ }
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
+
201
+ /**
202
+ * Safely converts objects to JSON with error handling
203
+ * @param {any} obj - Object to convert
204
+ * @returns {any} - JSON object or original object if conversion fails
205
+ */
206
+ function safeToJSON(obj) {
207
+ try {
208
+ return obj?.toJSON ? obj.toJSON() : obj;
209
+ } catch (error) {
210
+ console.warn('Error converting to JSON:', error);
211
+ return obj;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Executes function with enhanced error handling, timeout, and retry mechanism
217
+ * @param {Function} fn - Function to execute
218
+ * @param {string} errorType - Error type for native notification
219
+ * @param {any} errorDetails - Additional error details to include in the payload
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)
223
+ */
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
+ );
307
+ }
308
+
309
+ // ============================================================================
310
+ // BCMapViewEvents CALLBACKS
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Sets up event listeners for BCMapViewEvents
315
+ * @param {Object} mapView - The map view instance
316
+ */
317
+ function setupMapViewEventListeners(mapView) {
318
+ if (!mapView || !mapView.eventsHandler) {
319
+ console.warn('MapView does not support event listeners');
320
+ return;
321
+ }
322
+
323
+ // Clear existing listeners
324
+ clearMapViewEventListeners();
325
+
326
+ // Map load event
327
+ const loadListenerId = mapView.eventsHandler.on('load', () => {
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
+ }
343
+ });
344
+
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);
358
+
359
+ // Location selection event
360
+ const selectListenerId = mapView.eventsHandler.on('select', (args) => {
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
+ }
374
+ });
375
+
376
+ // Floor switch event
377
+ const switchToFloorListenerId = mapView.eventsHandler.on('switchToFloor', (args) => {
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
+ }
391
+ });
392
+
393
+ // Route step load event
394
+ const stepLoadListenerId = mapView.eventsHandler.on('stepLoad', (args) => {
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
+ }
408
+ });
409
+
410
+ // Walkthrough end event
411
+ const walkthroughEndListenerId = mapView.eventsHandler.on('walkthroughEnd', () => {
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
+ }
422
+ });
423
+
424
+ // Store listener IDs for cleanup
425
+ window._eventListeners.set('load', loadListenerId);
426
+ window._eventListeners.set('viewChange', viewChangeListenerId);
427
+ window._eventListeners.set('select', selectListenerId);
428
+ window._eventListeners.set('switchToFloor', switchToFloorListenerId);
429
+ window._eventListeners.set('stepLoad', stepLoadListenerId);
430
+ window._eventListeners.set('walkthroughEnd', walkthroughEndListenerId);
431
+ }
432
+
433
+ /**
434
+ * Clears all map view event listeners
435
+ */
436
+ function clearMapViewEventListeners() {
437
+ if (!window._mapView || !window._mapView.eventsHandler) {
438
+ return;
439
+ }
440
+
441
+ window._eventListeners.forEach((listenerId, eventName) => {
442
+ try {
443
+ window._mapView.eventsHandler.off(eventName, listenerId);
444
+ } catch (error) {
445
+ console.warn(`Error removing ${eventName} listener:`, error);
446
+ }
447
+ });
448
+
449
+ window._eventListeners.clear();
450
+ }
451
+
452
+ // ============================================================================
453
+ // INITIALIZATION
454
+ // ============================================================================
455
+
456
+ /**
457
+ * Initializes the map with site options and enhanced error handling
458
+ * @param {Object} siteOptions - Site configuration options
459
+ */
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
+
486
+ try {
487
+ const container = document.getElementById('mapContainer');
488
+ if (!container) {
489
+ throw new Error('Map container not found');
490
+ }
491
+
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
+ });
574
+
575
+ } catch (err) {
576
+ window._appState = 'error';
577
+ console.error('Init synchronous error:', err);
578
+
579
+ notifyNative("onError", {
580
+ message: err.message,
581
+ stack: err.stack,
582
+ timestamp: Date.now(),
583
+ operation: "init",
584
+ siteOptions,
585
+ appState: window._appState
586
+ });
587
+ }
588
+ }
589
+
590
+ // ============================================================================
591
+ // MAP VIEW METHODS
592
+ // ============================================================================
593
+
594
+ // Floor and Location Methods
595
+ globalThis.getCurrentFloor = () => {
596
+ executeWithErrorHandling(
597
+ () => window._mapView?.currentFloor,
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
+ });
606
+ };
607
+
608
+ globalThis.selectFloorWithId = (floor) => {
609
+ executeWithErrorHandling(
610
+ () => window._mapView?.selectFloorWithId(floor),
611
+ "onError",
612
+ { operation: "selectFloorWithId", floor }
613
+ );
614
+ };
615
+
616
+ globalThis.selectLocationWithId = (location) => {
617
+ executeWithErrorHandling(
618
+ () => window._mapView?.selectLocationWithId(location),
619
+ "onError",
620
+ { operation: "selectLocationWithId", location }
621
+ );
622
+ };
623
+
624
+ // Data Retrieval Methods
625
+ globalThis.getCategories = () => {
626
+ executeWithErrorHandling(
627
+ () => window._mapView?.getCategories(),
628
+ "onError",
629
+ { operation: "getCategories" },
630
+ []
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
+ });
639
+ };
640
+
641
+ globalThis.getLocations = () => {
642
+ executeWithErrorHandling(
643
+ () => window._mapView?.getLocations(),
644
+ "onError",
645
+ { operation: "getLocations" },
646
+ []
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
+ });
655
+ };
656
+
657
+ globalThis.getAmenities = () => {
658
+ executeWithErrorHandling(
659
+ () => window._mapView?.getAllAminityLocations(),
660
+ "onError",
661
+ { operation: "getAmenities" },
662
+ []
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
+ });
671
+ };
672
+
673
+ globalThis.getAmenityTypes = () => {
674
+ executeWithErrorHandling(
675
+ () => window._mapView?.getAmenities(),
676
+ "onError",
677
+ { operation: "getAmenityTypes" },
678
+ []
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
+ });
687
+ };
688
+
689
+ globalThis.selectAmenities = (type) => {
690
+ executeWithErrorHandling(
691
+ () => window._mapView?.selectAmenities(type),
692
+ "onError",
693
+ { operation: "selectAmenities", type }
694
+ );
695
+ };
696
+
697
+ // Session and Event Methods
698
+ globalThis.getSessionId = async () => {
699
+ try {
700
+ const sessionId = await window._mapView?.getSessionId();
701
+ notifyNative("onGetSessionId", sessionId);
702
+ } catch (err) {
703
+ notifyNative("onError", {
704
+ message: err.message,
705
+ timestamp: Date.now(),
706
+ operation: "getSessionId"
707
+ });
708
+ }
709
+ };
710
+
711
+ globalThis.getHappenings = (type) => {
712
+ executeWithErrorHandling(
713
+ () => window._mapView?.getHappenings(type),
714
+ "onError",
715
+ { operation: "getHappenings", type },
716
+ []
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
+ });
725
+ };
726
+
727
+ globalThis.getEventSuggestions = async (sessionId, answers) => {
728
+ try {
729
+ const suggestions = await window._mapView?.getEventSuggestions(sessionId, answers);
730
+ notifyNative("onGetEventSuggestions", suggestions?.map(s => safeToJSON(s)) || []);
731
+ } catch (err) {
732
+ notifyNative("onError", {
733
+ message: err.message,
734
+ timestamp: Date.now(),
735
+ operation: "getEventSuggestions",
736
+ sessionId,
737
+ answers
738
+ });
739
+ }
740
+ };
741
+
742
+ // Viewport and Camera Methods
743
+ globalThis.focusTo = (location, zoom, bearing, pitch) => {
744
+ executeWithErrorHandling(
745
+ () => window._mapView?.focusTo(location, zoom, bearing, pitch),
746
+ "onError",
747
+ { operation: "focusTo", location, zoom, bearing, pitch }
748
+ );
749
+ };
750
+
751
+ globalThis.clearSelection = () => {
752
+ executeWithErrorHandling(
753
+ () => window._mapView?.clearSelection(),
754
+ "onError",
755
+ { operation: "clearSelection" }
756
+ );
757
+ };
758
+
759
+ globalThis.updateZoom = (zoom) => {
760
+ executeWithErrorHandling(
761
+ () => window._mapView?.updateZoom(zoom),
762
+ "onError",
763
+ { operation: "updateZoom", zoom }
764
+ );
765
+ };
766
+
767
+ globalThis.updatePitch = (pitch) => {
768
+ executeWithErrorHandling(
769
+ () => window._mapView?.updatePitch(pitch),
770
+ "onError",
771
+ { operation: "updatePitch", pitch }
772
+ );
773
+ };
774
+
775
+ globalThis.updateBearing = (bearing) => {
776
+ executeWithErrorHandling(
777
+ () => window._mapView?.updateBearing(bearing),
778
+ "onError",
779
+ { operation: "updateBearing", bearing }
780
+ );
781
+ };
782
+
783
+ globalThis.enableMultiSelection = (val) => {
784
+ executeWithErrorHandling(
785
+ () => window._mapView?.enableMultiSelection(val),
786
+ "onError",
787
+ { operation: "enableMultiSelection", value: val }
788
+ );
789
+ };
790
+
791
+ globalThis.setBounds = (sw, ne) => {
792
+ executeWithErrorHandling(
793
+ () => window._mapView?.setBounds(sw, ne),
794
+ "onError",
795
+ { operation: "setBounds", southwest: sw, northeast: ne }
796
+ );
797
+ };
798
+
799
+ globalThis.setViewport = (options) => {
800
+ executeWithErrorHandling(
801
+ () => window._mapView?.setViewport(options),
802
+ "onError",
803
+ { operation: "setViewport", options }
804
+ );
805
+ };
806
+
807
+ globalThis.resetDefaultViewport = (options) => {
808
+ executeWithErrorHandling(
809
+ () => window._mapView?.resetDefaultViewport(options),
810
+ "onError",
811
+ { operation: "resetDefaultViewport", options }
812
+ );
813
+ };
814
+
815
+ // Search Methods
816
+ globalThis.searchForLocations = (q, callbackId) => {
817
+ if (!window._mapView?.searchForLocations) {
818
+ notifyNative("onSearchForLocations", {
819
+ callbackId,
820
+ results: [],
821
+ error: "Search method not available"
822
+ });
823
+ return;
824
+ }
825
+
826
+ window._mapView.searchForLocations(q, (matches) => {
827
+ notifyNative("onSearchForLocations", {
828
+ callbackId,
829
+ results: matches?.map(m => safeToJSON(m)) || []
830
+ });
831
+ });
832
+ };
833
+
834
+ globalThis.searchForCategories = (q, callbackId) => {
835
+ if (!window._mapView?.searchForCategories) {
836
+ notifyNative("onSearchForCategories", {
837
+ callbackId,
838
+ results: [],
839
+ error: "Search method not available"
840
+ });
841
+ return;
842
+ }
843
+
844
+ window._mapView.searchForCategories(q, (matches) => {
845
+ notifyNative("onSearchForCategories", {
846
+ callbackId,
847
+ results: matches?.map(m => safeToJSON(m)) || []
848
+ });
849
+ });
850
+ };
851
+
852
+ // ============================================================================
853
+ // ROUTE CONTROLLER METHODS
854
+ // ============================================================================
855
+
856
+ globalThis.getRoute = (startID, goalID, waypoints = [], routeOptions) => {
857
+ try {
858
+ const routes = window.becomap.getRouteById(startID, goalID, waypoints, routeOptions);
859
+ notifyNative("onGetRoute", routes?.map(route => safeToJSON(route)) || []);
860
+ } catch (error) {
861
+ notifyNative("onError", {
862
+ message: error.message,
863
+ timestamp: Date.now(),
864
+ operation: "getRoute",
865
+ startID,
866
+ goalID,
867
+ waypoints,
868
+ routeOptions
869
+ });
870
+ }
871
+ };
872
+
873
+ globalThis.showRoute = (segmentOrderIndex) => {
874
+ executeWithErrorHandling(
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 }
890
+ );
891
+ };
892
+
893
+ globalThis.showStep = (step) => {
894
+ executeWithErrorHandling(
895
+ () => window._mapView?.routeController?.showStepByOrderIndex(step),
896
+ "onError",
897
+ { operation: "showStep", step }
898
+ );
899
+ };
900
+
901
+ globalThis.clearAllRoutes = () => {
902
+ executeWithErrorHandling(
903
+ () => window._mapView?.routeController?.clearAllRoutes(),
904
+ "onError",
905
+ { operation: "clearAllRoutes" }
906
+ );
907
+ };
908
+
909
+ // ============================================================================
910
+ // CLEANUP AND EXPORTS
911
+ // ============================================================================
912
+
913
+ /**
914
+ * Enhanced cleanup function with proper resource management
915
+ */
916
+ globalThis.cleanup = () => {
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;
1005
+ };
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
+
1088
+ // Export init function
1089
+ globalThis.init = init;</script>
1090
+ </body>