becomap 1.6.1 → 1.6.8

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,1374 @@
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
+ if (!skipQueue && window._bridgeHealth.connectionAttempts < window._bridgeHealth.maxRetries) {
80
+ queueOperation(() => notifyNative(type, payload, true));
81
+ return;
82
+ }
83
+
84
+ // Fallback: store in localStorage for debugging
85
+ try {
86
+ const failedMessages = JSON.parse(localStorage.getItem('becomap_failed_messages') || '[]');
87
+ failedMessages.push(message);
88
+ localStorage.setItem('becomap_failed_messages', JSON.stringify(failedMessages.slice(-50))); // Keep last 50
89
+ } catch (e) {
90
+ // Silent fail for localStorage errors in production
91
+ }
92
+ return;
93
+ }
94
+
95
+ try {
96
+ const messageStr = JSON.stringify(message);
97
+
98
+ if (window.webkit?.messageHandlers?.jsHandler) {
99
+ // iOS
100
+ window.webkit.messageHandlers.jsHandler.postMessage(messageStr);
101
+ } else if (window.jsHandler?.postMessage) {
102
+ // Android
103
+ window.jsHandler.postMessage(messageStr);
104
+ }
105
+
106
+ updateBridgeHealth(true);
107
+ } catch (error) {
108
+ updateBridgeHealth(false);
109
+
110
+ if (!skipQueue) {
111
+ queueOperation(() => notifyNative(type, payload, true));
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Queues an operation for later execution
118
+ * @param {Function} operation - Operation to queue
119
+ */
120
+ function queueOperation(operation) {
121
+ window._operationQueue.push({
122
+ operation,
123
+ timestamp: Date.now(),
124
+ retries: 0
125
+ });
126
+
127
+ // Process queue after a short delay
128
+ setTimeout(processOperationQueue, 100);
129
+ }
130
+
131
+ /**
132
+ * Processes queued operations
133
+ */
134
+ function processOperationQueue() {
135
+ if (window._isProcessingQueue || !isNativeBridgeAvailable()) {
136
+ return;
137
+ }
138
+
139
+ window._isProcessingQueue = true;
140
+
141
+ try {
142
+ const now = Date.now();
143
+ const maxAge = 30000; // 30 seconds
144
+
145
+ // Remove expired operations
146
+ window._operationQueue = window._operationQueue.filter(item =>
147
+ now - item.timestamp < maxAge
148
+ );
149
+
150
+ // Process remaining operations
151
+ while (window._operationQueue.length > 0 && isNativeBridgeAvailable()) {
152
+ const item = window._operationQueue.shift();
153
+
154
+ try {
155
+ item.operation();
156
+ } catch (error) {
157
+ // Retry failed operations up to 3 times
158
+ if (item.retries < 3) {
159
+ item.retries++;
160
+ window._operationQueue.unshift(item);
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ } finally {
166
+ window._isProcessingQueue = false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Throttle function to prevent event flooding
172
+ * @param {Function} func - Function to throttle
173
+ * @param {number} delay - Delay in milliseconds
174
+ * @returns {Function} - Throttled function
175
+ */
176
+ function throttle(func, delay) {
177
+ let timeoutId;
178
+ let lastExecTime = 0;
179
+
180
+ return function (...args) {
181
+ const currentTime = Date.now();
182
+
183
+ if (currentTime - lastExecTime > delay) {
184
+ func.apply(this, args);
185
+ lastExecTime = currentTime;
186
+ } else {
187
+ clearTimeout(timeoutId);
188
+ timeoutId = setTimeout(() => {
189
+ func.apply(this, args);
190
+ lastExecTime = Date.now();
191
+ }, delay - (currentTime - lastExecTime));
192
+ }
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Safely converts objects to JSON with error handling
198
+ * @param {any} obj - Object to convert
199
+ * @returns {any} - JSON object or original object if conversion fails
200
+ */
201
+ function safeToJSON(obj) {
202
+ try {
203
+ return obj?.toJSON ? obj.toJSON() : obj;
204
+ } catch (error) {
205
+ return obj;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Executes function with enhanced error handling, timeout, and retry mechanism
211
+ * @param {Function} fn - Function to execute
212
+ * @param {string} errorType - Error type for native notification
213
+ * @param {any} errorDetails - Additional error details to include in the payload
214
+ * @param {any} fallbackValue - Fallback value if function fails
215
+ * @param {number} timeout - Timeout in milliseconds (default: 5000)
216
+ * @param {number} maxRetries - Maximum retry attempts (default: 0)
217
+ */
218
+ function executeWithErrorHandling(fn, errorType, errorDetails = null, fallbackValue = null, timeout = 5000, maxRetries = 0) {
219
+ return new Promise((resolve) => {
220
+ let retryCount = 0;
221
+
222
+ function attemptExecution() {
223
+ try {
224
+ // Validate pre-conditions
225
+ if (window._appState === 'destroyed') {
226
+ throw new Error('Application has been destroyed');
227
+ }
228
+
229
+ if (!window._mapView && errorDetails?.operation !== 'init') {
230
+ throw new Error('MapView not initialized');
231
+ }
232
+
233
+ // Set up timeout
234
+ const timeoutId = setTimeout(() => {
235
+ throw new Error(`Operation timed out after ${timeout}ms`);
236
+ }, timeout);
237
+
238
+ const result = fn();
239
+ clearTimeout(timeoutId);
240
+
241
+ // Handle promises
242
+ if (result && typeof result.then === 'function') {
243
+ result
244
+ .then(res => resolve(res !== undefined ? res : fallbackValue))
245
+ .catch(handleError);
246
+ } else {
247
+ resolve(result !== undefined ? result : fallbackValue);
248
+ }
249
+
250
+ } catch (error) {
251
+ handleError(error);
252
+ }
253
+ }
254
+
255
+ function handleError(error) {
256
+ // Retry logic for transient errors
257
+ if (retryCount < maxRetries && isRetriableError(error)) {
258
+ retryCount++;
259
+ const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000); // Exponential backoff
260
+ setTimeout(attemptExecution, delay);
261
+ return;
262
+ }
263
+
264
+ // Simple error payload for mobile
265
+ const errorPayload = {
266
+ operation: errorDetails?.operation || 'unknown',
267
+ error: getSimpleErrorMessage(error)
268
+ };
269
+
270
+ notifyNative(errorType, errorPayload);
271
+ resolve(fallbackValue);
272
+ }
273
+
274
+ attemptExecution();
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Determines if an error is retriable
280
+ * @param {Error} error - The error to check
281
+ * @returns {boolean} - True if error is retriable
282
+ */
283
+ function isRetriableError(error) {
284
+ const retriableMessages = [
285
+ 'network error',
286
+ 'timeout',
287
+ 'connection failed',
288
+ 'temporary failure'
289
+ ];
290
+
291
+ return retriableMessages.some(msg =>
292
+ error.message.toLowerCase().includes(msg)
293
+ );
294
+ }
295
+
296
+ /**
297
+ * Validates route parameters for mobile SDK consumption
298
+ * @param {any} startID - Starting location ID
299
+ * @param {any} goalID - Goal location ID
300
+ * @param {any} waypoints - Array of waypoint IDs
301
+ * @param {any} routeOptions - Route calculation options
302
+ * @returns {string|null} - Error message or null if valid
303
+ */
304
+ function validateRouteParameters(startID, goalID, waypoints, routeOptions) {
305
+ if (!startID || (typeof startID !== 'string' && typeof startID !== 'number')) {
306
+ return "Invalid start location ID";
307
+ }
308
+
309
+ if (!goalID || (typeof goalID !== 'string' && typeof goalID !== 'number')) {
310
+ return "Invalid goal location ID";
311
+ }
312
+
313
+ if (startID === goalID) {
314
+ return "Start and goal locations cannot be the same";
315
+ }
316
+
317
+ if (waypoints && !Array.isArray(waypoints)) {
318
+ return "Waypoints must be an array";
319
+ }
320
+
321
+ if (waypoints && waypoints.length > 10) {
322
+ return "Too many waypoints (maximum 10 allowed)";
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ /**
329
+ * Validates route data integrity
330
+ * @param {any} route - Route object to validate
331
+ * @returns {boolean} - True if route is valid
332
+ */
333
+ function isValidRoute(route) {
334
+ return route &&
335
+ route.segments &&
336
+ Array.isArray(route.segments) &&
337
+ route.segments.length > 0;
338
+ }
339
+
340
+ /**
341
+ * Validates segment index for route display
342
+ * @param {any} segmentIndex - Segment order index
343
+ * @param {any} routeController - Route controller instance
344
+ * @returns {string|null} - Error message or null if valid
345
+ */
346
+ function validateSegmentIndex(segmentIndex, routeController) {
347
+ if (typeof segmentIndex !== 'number' || segmentIndex < 0) {
348
+ return "Segment index must be a non-negative number";
349
+ }
350
+
351
+ const segments = routeController.segments;
352
+ if (!segments || segments.length === 0) {
353
+ return "No route segments available";
354
+ }
355
+
356
+ if (segmentIndex >= segments.length) {
357
+ return `Segment index ${segmentIndex} exceeds available segments (${segments.length})`;
358
+ }
359
+
360
+ return null;
361
+ }
362
+
363
+ /**
364
+ * Converts error objects to simple messages for mobile SDK
365
+ * @param {Error} error - Error object
366
+ * @returns {string} - Simplified error message
367
+ */
368
+ function getSimpleErrorMessage(error) {
369
+ if (!error) return "Unknown error occurred";
370
+
371
+ // Map common technical errors to user-friendly messages
372
+ const errorMappings = {
373
+ 'TypeError': 'Invalid data provided',
374
+ 'ReferenceError': 'Required component not available',
375
+ 'NetworkError': 'Network connection failed',
376
+ 'TimeoutError': 'Operation timed out'
377
+ };
378
+
379
+ const errorType = error.constructor.name;
380
+ return errorMappings[errorType] || error.message || "Operation failed";
381
+ }
382
+
383
+
384
+
385
+ /**
386
+ * Creates standardized parameter validation for mobile functions
387
+ * @param {Object} params - Parameters to validate
388
+ * @param {Object} schema - Validation schema
389
+ * @returns {string|null} - Error message or null if valid
390
+ */
391
+ function validateMobileParameters(params, schema) {
392
+ for (const [key, rules] of Object.entries(schema)) {
393
+ const value = params[key];
394
+
395
+ if (rules.required && (value === null || value === undefined)) {
396
+ return `Missing required parameter: ${key}`;
397
+ }
398
+
399
+ if (value !== undefined) {
400
+ if (rules.type && typeof value !== rules.type) {
401
+ return `Invalid ${key}: expected ${rules.type}`;
402
+ }
403
+
404
+ if (rules.range && (value < rules.range[0] || value > rules.range[1])) {
405
+ return `${key} out of valid range`;
406
+ }
407
+
408
+ if (rules.maxLength && value.length > rules.maxLength) {
409
+ return `${key} exceeds maximum length`;
410
+ }
411
+ }
412
+ }
413
+
414
+ return null;
415
+ }
416
+
417
+ /**
418
+ * Advanced error handling with circuit breaker pattern
419
+ */
420
+ const errorCircuitBreaker = {
421
+ failures: new Map(),
422
+ thresholds: {
423
+ maxFailures: 5,
424
+ resetTimeout: 30000 // 30 seconds
425
+ },
426
+
427
+ isCircuitOpen(operation) {
428
+ const failure = this.failures.get(operation);
429
+ if (!failure) return false;
430
+
431
+ if (failure.count >= this.thresholds.maxFailures) {
432
+ if (Date.now() - failure.lastFailure < this.thresholds.resetTimeout) {
433
+ return true;
434
+ } else {
435
+ // Reset circuit
436
+ this.failures.delete(operation);
437
+ return false;
438
+ }
439
+ }
440
+ return false;
441
+ },
442
+
443
+ recordFailure(operation) {
444
+ const failure = this.failures.get(operation) || { count: 0, lastFailure: 0 };
445
+ failure.count++;
446
+ failure.lastFailure = Date.now();
447
+ this.failures.set(operation, failure);
448
+ },
449
+
450
+ recordSuccess(operation) {
451
+ this.failures.delete(operation);
452
+ }
453
+ };
454
+
455
+ /**
456
+ * Enhanced error handling with circuit breaker and caching
457
+ */
458
+ function executeWithCircuitBreaker(operation, fn, errorCallback) {
459
+ if (errorCircuitBreaker.isCircuitOpen(operation)) {
460
+ errorCallback({
461
+ operation,
462
+ error: "Service temporarily unavailable"
463
+ });
464
+ return;
465
+ }
466
+
467
+ try {
468
+ const result = fn();
469
+ errorCircuitBreaker.recordSuccess(operation);
470
+ return result;
471
+ } catch (error) {
472
+ errorCircuitBreaker.recordFailure(operation);
473
+ errorCallback({
474
+ operation,
475
+ error: getSimpleErrorMessage(error)
476
+ });
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Debounced error reporting to prevent spam
482
+ */
483
+ const errorDebouncer = {
484
+ timers: new Map(),
485
+ delay: 1000, // 1 second
486
+
487
+ debounce(key, callback) {
488
+ if (this.timers.has(key)) {
489
+ clearTimeout(this.timers.get(key));
490
+ }
491
+
492
+ const timer = setTimeout(() => {
493
+ callback();
494
+ this.timers.delete(key);
495
+ }, this.delay);
496
+
497
+ this.timers.set(key, timer);
498
+ }
499
+ };
500
+
501
+ /**
502
+ * Memory-efficient error caching
503
+ */
504
+ const errorCache = {
505
+ cache: new Map(),
506
+ maxSize: 50,
507
+
508
+ set(key, value) {
509
+ if (this.cache.size >= this.maxSize) {
510
+ const firstKey = this.cache.keys().next().value;
511
+ this.cache.delete(firstKey);
512
+ }
513
+ this.cache.set(key, value);
514
+ },
515
+
516
+ get(key) {
517
+ return this.cache.get(key);
518
+ },
519
+
520
+ has(key) {
521
+ return this.cache.has(key);
522
+ }
523
+ };
524
+
525
+ // ============================================================================
526
+ // BCMapViewEvents CALLBACKS
527
+ // ============================================================================
528
+
529
+ /**
530
+ * Sets up event listeners for BCMapViewEvents
531
+ * @param {Object} mapView - The map view instance
532
+ */
533
+ function setupMapViewEventListeners(mapView) {
534
+ if (!mapView || !mapView.eventsHandler) {
535
+ console.warn('MapView does not support event listeners');
536
+ return;
537
+ }
538
+
539
+ // Clear existing listeners
540
+ clearMapViewEventListeners();
541
+
542
+ // Map load event
543
+ const loadListenerId = mapView.eventsHandler.on('load', () => {
544
+ try {
545
+ window._appState = 'ready';
546
+ notifyNative("onRenderComplete", {
547
+ site: safeToJSON(window._site)
548
+ });
549
+ } catch (error) {
550
+ notifyNative("onError", {
551
+ operation: 'load',
552
+ error: getSimpleErrorMessage(error)
553
+ });
554
+ }
555
+ });
556
+
557
+ // View change event (throttled to prevent flooding)
558
+ const throttledViewChange = throttle((args) => {
559
+ try {
560
+ notifyNative("onViewChange", {
561
+ viewOptions: safeToJSON(args.viewOptions)
562
+ });
563
+ } catch (error) {
564
+ // Silent fail for view change errors to prevent spam
565
+ }
566
+ }, 100); // Throttle to max 10 events per second
567
+
568
+ const viewChangeListenerId = mapView.eventsHandler.on('viewChange', throttledViewChange);
569
+
570
+ // Location selection event
571
+ const selectListenerId = mapView.eventsHandler.on('select', (args) => {
572
+ try {
573
+ notifyNative("onLocationSelect", {
574
+ locations: args.locations?.map(loc => safeToJSON(loc)) || []
575
+ });
576
+ } catch (error) {
577
+ notifyNative("onError", {
578
+ operation: 'select',
579
+ error: getSimpleErrorMessage(error)
580
+ });
581
+ }
582
+ });
583
+
584
+ // Floor switch event
585
+ const switchToFloorListenerId = mapView.eventsHandler.on('switchToFloor', (args) => {
586
+ try {
587
+ notifyNative("onFloorSwitch", {
588
+ floor: safeToJSON(args.floor)
589
+ });
590
+ } catch (error) {
591
+ console.error('Error in switchToFloor event handler:', error);
592
+ notifyNative("onError", {
593
+ operation: "switchToFloor",
594
+ error: getSimpleErrorMessage(error)
595
+ });
596
+ }
597
+ });
598
+
599
+ // Route step load event
600
+ const stepLoadListenerId = mapView.eventsHandler.on('stepLoad', (args) => {
601
+ try {
602
+ notifyNative("onStepLoad", {
603
+ step: safeToJSON(args.step)
604
+ });
605
+ } catch (error) {
606
+ console.error('Error in stepLoad event handler:', error);
607
+ notifyNative("onError", {
608
+ operation: "stepLoad",
609
+ error: getSimpleErrorMessage(error)
610
+ });
611
+ }
612
+ });
613
+
614
+ // Walkthrough end event
615
+ const walkthroughEndListenerId = mapView.eventsHandler.on('walkthroughEnd', () => {
616
+ try {
617
+ notifyNative("onWalkthroughEnd", {});
618
+ } catch (error) {
619
+ console.error('Error in walkthroughEnd event handler:', error);
620
+ notifyNative("onError", {
621
+ operation: "walkthroughEnd",
622
+ error: getSimpleErrorMessage(error)
623
+ });
624
+ }
625
+ });
626
+
627
+ // Store listener IDs for cleanup
628
+ window._eventListeners.set('load', loadListenerId);
629
+ window._eventListeners.set('viewChange', viewChangeListenerId);
630
+ window._eventListeners.set('select', selectListenerId);
631
+ window._eventListeners.set('switchToFloor', switchToFloorListenerId);
632
+ window._eventListeners.set('stepLoad', stepLoadListenerId);
633
+ window._eventListeners.set('walkthroughEnd', walkthroughEndListenerId);
634
+ }
635
+
636
+ /**
637
+ * Clears all map view event listeners
638
+ */
639
+ function clearMapViewEventListeners() {
640
+ if (!window._mapView || !window._mapView.eventsHandler) {
641
+ return;
642
+ }
643
+
644
+ window._eventListeners.forEach((listenerId, eventName) => {
645
+ try {
646
+ window._mapView.eventsHandler.off(eventName, listenerId);
647
+ } catch (error) {
648
+ console.warn(`Error removing ${eventName} listener:`, error);
649
+ }
650
+ });
651
+
652
+ window._eventListeners.clear();
653
+ }
654
+
655
+ // ============================================================================
656
+ // INITIALIZATION
657
+ // ============================================================================
658
+
659
+ /**
660
+ * Initializes the map with site options and enhanced error handling
661
+ * @param {Object} siteOptions - Site configuration options
662
+ */
663
+ function init(siteOptions) {
664
+ // Validate input parameters
665
+ if (!siteOptions || typeof siteOptions !== 'object') {
666
+ notifyNative("onError", {
667
+ operation: "init",
668
+ error: "Invalid siteOptions provided"
669
+ });
670
+ return;
671
+ }
672
+
673
+ // Check if already initialized
674
+ if (window._appState === 'ready' && window._mapView) {
675
+ notifyNative("onRenderComplete", {
676
+ site: safeToJSON(window._site)
677
+ });
678
+ return;
679
+ }
680
+
681
+ window._appState = 'initializing';
682
+
683
+ try {
684
+ const container = document.getElementById('mapContainer');
685
+ if (!container) {
686
+ throw new Error('Map container not found');
687
+ }
688
+
689
+ // Check if Becomap UMD is loaded with timeout
690
+ const checkBecomapLoaded = () => {
691
+ return new Promise((resolve, reject) => {
692
+ const maxAttempts = 50; // 5 seconds with 100ms intervals
693
+ let attempts = 0;
694
+
695
+ const checkInterval = setInterval(() => {
696
+ attempts++;
697
+
698
+ if (window.becomap?.getSite && window.becomap?.getMapView) {
699
+ clearInterval(checkInterval);
700
+ resolve();
701
+ } else if (attempts >= maxAttempts) {
702
+ clearInterval(checkInterval);
703
+ reject(new Error('Becomap UMD failed to load within timeout'));
704
+ }
705
+ }, 100);
706
+ });
707
+ };
708
+
709
+ // Initialize with proper error handling and timeouts
710
+ checkBecomapLoaded()
711
+ .then(() => {
712
+ const mapOptions = { zoom: 18.5 };
713
+
714
+ // Add timeout to getSite
715
+ const getSiteWithTimeout = Promise.race([
716
+ window.becomap.getSite(siteOptions),
717
+ new Promise((_, reject) =>
718
+ setTimeout(() => reject(new Error('getSite timeout')), 10000)
719
+ )
720
+ ]);
721
+
722
+ return getSiteWithTimeout;
723
+ })
724
+ .then(site => {
725
+ if (!site) {
726
+ throw new Error('Failed to load site data');
727
+ }
728
+
729
+ window._site = site;
730
+
731
+ // Add timeout to getMapView
732
+ const getMapViewWithTimeout = Promise.race([
733
+ window.becomap.getMapView(container, site, { zoom: 18.5 }),
734
+ new Promise((_, reject) =>
735
+ setTimeout(() => reject(new Error('getMapView timeout')), 15000)
736
+ )
737
+ ]);
738
+
739
+ return getMapViewWithTimeout;
740
+ })
741
+ .then(mapView => {
742
+ if (!mapView) {
743
+ throw new Error('Failed to create map view');
744
+ }
745
+
746
+ window._mapView = mapView;
747
+
748
+ // Setup event listeners
749
+ setupMapViewEventListeners(mapView);
750
+
751
+ // Initialize bridge health monitoring
752
+ updateBridgeHealth(isNativeBridgeAvailable());
753
+ })
754
+ .catch(err => {
755
+ window._appState = 'error';
756
+
757
+ notifyNative("onError", {
758
+ operation: "init",
759
+ error: getSimpleErrorMessage(err)
760
+ });
761
+ });
762
+
763
+ } catch (err) {
764
+ window._appState = 'error';
765
+
766
+ notifyNative("onError", {
767
+ operation: "init",
768
+ error: getSimpleErrorMessage(err)
769
+ });
770
+ }
771
+ }
772
+
773
+ // ============================================================================
774
+ // MAP VIEW METHODS
775
+ // ============================================================================
776
+
777
+ // Floor and Location Methods
778
+ globalThis.getCurrentFloor = () => {
779
+ executeWithErrorHandling(
780
+ () => window._mapView?.currentFloor,
781
+ "onError",
782
+ { operation: "getCurrentFloor" }
783
+ ).then(floor => {
784
+ notifyNative("onGetCurrentFloor", safeToJSON(floor));
785
+ }).catch(error => {
786
+ console.error('Error in getCurrentFloor:', error);
787
+ notifyNative("onGetCurrentFloor", null);
788
+ });
789
+ };
790
+
791
+ globalThis.selectFloorWithId = (floor) => {
792
+ executeWithErrorHandling(
793
+ () => window._mapView?.selectFloorWithId(floor),
794
+ "onError",
795
+ { operation: "selectFloorWithId", floor }
796
+ );
797
+ };
798
+
799
+ globalThis.selectLocationWithId = (location) => {
800
+ executeWithErrorHandling(
801
+ () => window._mapView?.selectLocationWithId(location),
802
+ "onError",
803
+ { operation: "selectLocationWithId", location }
804
+ );
805
+ };
806
+
807
+ // Data Retrieval Methods
808
+ globalThis.getCategories = () => {
809
+ executeWithErrorHandling(
810
+ () => window._mapView?.getCategories(),
811
+ "onError",
812
+ { operation: "getCategories" },
813
+ []
814
+ ).then(categories => {
815
+ // Ensure categories is an array before mapping
816
+ const categoriesArray = Array.isArray(categories) ? categories : [];
817
+ notifyNative("onGetCategories", categoriesArray.map(cat => safeToJSON(cat)));
818
+ }).catch(error => {
819
+ console.error('Error in getCategories:', error);
820
+ notifyNative("onGetCategories", []);
821
+ });
822
+ };
823
+
824
+ globalThis.getLocations = () => {
825
+ executeWithErrorHandling(
826
+ () => window._mapView?.getLocations(),
827
+ "onError",
828
+ { operation: "getLocations" },
829
+ []
830
+ ).then(locations => {
831
+ // Ensure locations is an array before mapping
832
+ const locationsArray = Array.isArray(locations) ? locations : [];
833
+ notifyNative("onGetLocations", locationsArray.map(loc => safeToJSON(loc)));
834
+ }).catch(error => {
835
+ console.error('Error in getLocations:', error);
836
+ notifyNative("onGetLocations", []);
837
+ });
838
+ };
839
+
840
+ globalThis.getAmenities = () => {
841
+ executeWithErrorHandling(
842
+ () => window._mapView?.getAllAminityLocations(),
843
+ "onError",
844
+ { operation: "getAmenities" },
845
+ []
846
+ ).then(amenities => {
847
+ // Ensure amenities is an array before mapping
848
+ const amenitiesArray = Array.isArray(amenities) ? amenities : [];
849
+ notifyNative("onGetAmenities", amenitiesArray.map(amenity => safeToJSON(amenity)));
850
+ }).catch(error => {
851
+ console.error('Error in getAmenities:', error);
852
+ notifyNative("onGetAmenities", []);
853
+ });
854
+ };
855
+
856
+ globalThis.getAmenityTypes = () => {
857
+ executeWithErrorHandling(
858
+ () => window._mapView?.getAmenities(),
859
+ "onError",
860
+ { operation: "getAmenityTypes" },
861
+ []
862
+ ).then(amenities => {
863
+ // Ensure amenities is an array before mapping
864
+ const amenitiesArray = Array.isArray(amenities) ? amenities : [];
865
+ notifyNative("onGetAmenityTypes", amenitiesArray.map(amenity => safeToJSON(amenity)));
866
+ }).catch(error => {
867
+ console.error('Error in getAmenityTypes:', error);
868
+ notifyNative("onGetAmenityTypes", []);
869
+ });
870
+ };
871
+
872
+ globalThis.selectAmenities = (type) => {
873
+ executeWithErrorHandling(
874
+ () => window._mapView?.selectAmenities(type),
875
+ "onError",
876
+ { operation: "selectAmenities", type }
877
+ );
878
+ };
879
+
880
+ // Session and Event Methods
881
+ globalThis.getSessionId = async () => {
882
+ try {
883
+ const sessionId = await window._mapView?.getSessionId();
884
+ notifyNative("onGetSessionId", sessionId);
885
+ } catch (err) {
886
+ notifyNative("onError", {
887
+ operation: "getSessionId",
888
+ error: getSimpleErrorMessage(err)
889
+ });
890
+ }
891
+ };
892
+
893
+ globalThis.getHappenings = (type) => {
894
+ executeWithErrorHandling(
895
+ () => window._mapView?.getHappenings(type),
896
+ "onError",
897
+ { operation: "getHappenings", type },
898
+ []
899
+ ).then(happenings => {
900
+ // Ensure happenings is an array before mapping
901
+ const happeningsArray = Array.isArray(happenings) ? happenings : [];
902
+ notifyNative("onGetHappenings", happeningsArray.map(h => safeToJSON(h)));
903
+ }).catch(error => {
904
+ console.error('Error in getHappenings:', error);
905
+ notifyNative("onGetHappenings", []);
906
+ });
907
+ };
908
+
909
+ globalThis.getEventSuggestions = async (sessionId, answers) => {
910
+ try {
911
+ const suggestions = await window._mapView?.getEventSuggestions(sessionId, answers);
912
+ notifyNative("onGetEventSuggestions", suggestions?.map(s => safeToJSON(s)) || []);
913
+ } catch (err) {
914
+ notifyNative("onError", {
915
+ operation: "getEventSuggestions",
916
+ error: getSimpleErrorMessage(err)
917
+ });
918
+ }
919
+ };
920
+
921
+ // Viewport and Camera Methods
922
+ globalThis.focusTo = (locationId, zoom, bearing, pitch) => {
923
+ // Validate parameters for mobile SDK
924
+ const validationError = validateMobileParameters(
925
+ { locationId, zoom, bearing, pitch },
926
+ {
927
+ locationId: { required: true, type: 'string' },
928
+ zoom: { type: 'number', range: [1, 25] },
929
+ bearing: { type: 'number', range: [0, 360] },
930
+ pitch: { type: 'number', range: [0, 60] }
931
+ }
932
+ );
933
+
934
+ if (validationError) {
935
+ notifyNative("onError", {
936
+ operation: "focusTo",
937
+ error: validationError
938
+ });
939
+ return;
940
+ }
941
+
942
+ // Get location object from ID using mapView
943
+ if (!window._mapView) {
944
+ notifyNative("onError", {
945
+ operation: "focusTo",
946
+ error: "MapView not initialized"
947
+ });
948
+ return;
949
+ }
950
+
951
+ const locations = window._mapView.getLocations();
952
+ const location = locations?.find(loc => loc.id === locationId);
953
+ if (!location) {
954
+ notifyNative("onError", {
955
+ operation: "focusTo",
956
+ error: `Location not found for ID: ${locationId}`
957
+ });
958
+ return;
959
+ }
960
+
961
+ executeWithErrorHandling(
962
+ () => window._mapView?.focusTo(location, zoom, bearing, pitch),
963
+ "onError",
964
+ { operation: "focusTo" }
965
+ );
966
+ };
967
+
968
+ globalThis.clearSelection = () => {
969
+ executeWithErrorHandling(
970
+ () => window._mapView?.clearSelection(),
971
+ "onError",
972
+ { operation: "clearSelection" }
973
+ );
974
+ };
975
+
976
+ globalThis.updateZoom = (zoom) => {
977
+ executeWithErrorHandling(
978
+ () => window._mapView?.updateZoom(zoom),
979
+ "onError",
980
+ { operation: "updateZoom", zoom }
981
+ );
982
+ };
983
+
984
+ globalThis.updatePitch = (pitch) => {
985
+ executeWithErrorHandling(
986
+ () => window._mapView?.updatePitch(pitch),
987
+ "onError",
988
+ { operation: "updatePitch", pitch }
989
+ );
990
+ };
991
+
992
+ globalThis.updateBearing = (bearing) => {
993
+ executeWithErrorHandling(
994
+ () => window._mapView?.updateBearing(bearing),
995
+ "onError",
996
+ { operation: "updateBearing", bearing }
997
+ );
998
+ };
999
+
1000
+ globalThis.enableMultiSelection = (val) => {
1001
+ executeWithErrorHandling(
1002
+ () => window._mapView?.enableMultiSelection(val),
1003
+ "onError",
1004
+ { operation: "enableMultiSelection", value: val }
1005
+ );
1006
+ };
1007
+
1008
+ globalThis.setBounds = (sw, ne) => {
1009
+ executeWithErrorHandling(
1010
+ () => window._mapView?.setBounds(sw, ne),
1011
+ "onError",
1012
+ { operation: "setBounds", southwest: sw, northeast: ne }
1013
+ );
1014
+ };
1015
+
1016
+ globalThis.setViewport = (options) => {
1017
+ executeWithErrorHandling(
1018
+ () => window._mapView?.setViewport(options),
1019
+ "onError",
1020
+ { operation: "setViewport", options }
1021
+ );
1022
+ };
1023
+
1024
+ globalThis.resetDefaultViewport = (options) => {
1025
+ executeWithErrorHandling(
1026
+ () => window._mapView?.resetDefaultViewport(options),
1027
+ "onError",
1028
+ { operation: "resetDefaultViewport", options }
1029
+ );
1030
+ };
1031
+
1032
+ // Search Methods
1033
+ globalThis.searchForLocations = (q, callbackId) => {
1034
+ // Validate search parameters
1035
+ const validationError = validateMobileParameters(
1036
+ { q, callbackId },
1037
+ {
1038
+ q: { required: true, type: 'string', maxLength: 100 },
1039
+ callbackId: { required: true, type: 'string' }
1040
+ }
1041
+ );
1042
+
1043
+ if (validationError) {
1044
+ notifyNative("onSearchForLocations", {
1045
+ callbackId,
1046
+ results: [],
1047
+ error: validationError
1048
+ });
1049
+ return;
1050
+ }
1051
+
1052
+ if (!window._mapView?.searchForLocations) {
1053
+ notifyNative("onSearchForLocations", {
1054
+ callbackId,
1055
+ results: [],
1056
+ error: "Search method not available"
1057
+ });
1058
+ return;
1059
+ }
1060
+
1061
+ try {
1062
+ window._mapView.searchForLocations(q, (matches) => {
1063
+ notifyNative("onSearchForLocations", {
1064
+ callbackId,
1065
+ results: matches?.map(m => safeToJSON(m)) || []
1066
+ });
1067
+ });
1068
+ } catch (error) {
1069
+ notifyNative("onSearchForLocations", {
1070
+ callbackId,
1071
+ results: [],
1072
+ error: getSimpleErrorMessage(error)
1073
+ });
1074
+ }
1075
+ };
1076
+
1077
+ globalThis.searchForCategories = (q, callbackId) => {
1078
+ if (!window._mapView?.searchForCategories) {
1079
+ notifyNative("onSearchForCategories", {
1080
+ callbackId,
1081
+ results: [],
1082
+ error: "Search method not available"
1083
+ });
1084
+ return;
1085
+ }
1086
+
1087
+ window._mapView.searchForCategories(q, (matches) => {
1088
+ notifyNative("onSearchForCategories", {
1089
+ callbackId,
1090
+ results: matches?.map(m => safeToJSON(m)) || []
1091
+ });
1092
+ });
1093
+ };
1094
+
1095
+ // ============================================================================
1096
+ // ROUTE CONTROLLER METHODS
1097
+ // ============================================================================
1098
+
1099
+ globalThis.getRoute = (startID, goalID, waypoints = [], routeOptions) => {
1100
+ // Parameter validation
1101
+ const validationError = validateRouteParameters(startID, goalID, waypoints, routeOptions);
1102
+ if (validationError) {
1103
+ notifyNative("onError", {
1104
+ operation: "getRoute",
1105
+ error: validationError
1106
+ });
1107
+ return;
1108
+ }
1109
+
1110
+ // // Defensive checks for becomap and getRouteById
1111
+ // if (!window.becomap) {
1112
+ // console.error("getRoute error: window.becomap is undefined", { startID, goalID, waypoints, routeOptions });
1113
+ // notifyNative("onError", {
1114
+ // operation: "getRoute",
1115
+ // error: "window.becomap is not available"
1116
+ // });
1117
+ // return;
1118
+ // }
1119
+ // if (typeof window.becomap.getRouteById !== 'function') {
1120
+ // console.error("getRoute error: window.becomap.getRouteById is not a function", { becomap: window.becomap });
1121
+ // notifyNative("onError", {
1122
+ // operation: "getRoute",
1123
+ // error: "getRouteById is not a function on becomap"
1124
+ // });
1125
+ // return;
1126
+ // }
1127
+
1128
+ try {
1129
+ const routes = window.becomap.getRouteById(startID, goalID, waypoints, routeOptions);
1130
+ console.log("getRouteById returned:", routes);
1131
+
1132
+ // Handle no route found scenario
1133
+ if (!routes || !Array.isArray(routes) || routes.length === 0) {
1134
+ notifyNative("onError", {
1135
+ operation: "getRoute",
1136
+ error: "No route found between specified locations",
1137
+ debug: { startID, goalID, waypoints, routeOptions, routes }
1138
+ });
1139
+ return;
1140
+ }
1141
+
1142
+ // No further validation, just return the segments
1143
+ try {
1144
+ const serializedRoutes = routes.map(route => {
1145
+ try {
1146
+ return safeToJSON(route);
1147
+ } catch (serializeError) {
1148
+ console.error("Error serializing route:", serializeError, route);
1149
+ return null;
1150
+ }
1151
+ }).filter(Boolean); // Remove any null entries from failed serialization
1152
+
1153
+ notifyNative("onGetRoute", serializedRoutes);
1154
+ } catch (serializeError) {
1155
+ console.error("Error in route serialization:", serializeError);
1156
+ notifyNative("onError", {
1157
+ operation: "getRoute",
1158
+ error: "Failed to serialize route data",
1159
+ debug: { serializeError: serializeError?.message }
1160
+ });
1161
+ }
1162
+ } catch (error) {
1163
+ console.error("getRoute exception:", error, { startID, goalID, waypoints, routeOptions });
1164
+ notifyNative("onError", {
1165
+ operation: "getRoute",
1166
+ error: getSimpleErrorMessage(error),
1167
+ debug: { startID, goalID, waypoints, routeOptions, error: error?.message }
1168
+ });
1169
+ }
1170
+ };
1171
+
1172
+ globalThis.showRoute = (segmentOrderIndex) => {
1173
+ const routeController = window._mapView?.routeController;
1174
+ if (!routeController) {
1175
+ notifyNative("onError", {
1176
+ operation: "showRoute",
1177
+ error: "Route controller not available"
1178
+ });
1179
+ return;
1180
+ }
1181
+
1182
+ try {
1183
+ const validationError = validateSegmentIndex(segmentOrderIndex, routeController);
1184
+ if (validationError) {
1185
+ notifyNative("onError", {
1186
+ operation: "showRoute",
1187
+ error: validationError
1188
+ });
1189
+ return;
1190
+ }
1191
+ routeController.showSegmentByOrderIndex(segmentOrderIndex);
1192
+ } catch (error) {
1193
+ notifyNative("onError", {
1194
+ operation: "showRoute",
1195
+ error: getSimpleErrorMessage(error)
1196
+ });
1197
+ }
1198
+ };
1199
+
1200
+ globalThis.showStep = (step) => {
1201
+ executeWithErrorHandling(
1202
+ () => window._mapView?.routeController?.showStepByOrderIndex(step),
1203
+ "onError",
1204
+ { operation: "showStep", step }
1205
+ );
1206
+ };
1207
+
1208
+ globalThis.clearAllRoutes = () => {
1209
+ executeWithErrorHandling(
1210
+ () => window._mapView?.routeController?.clearAllRoutes(),
1211
+ "onError",
1212
+ { operation: "clearAllRoutes" }
1213
+ );
1214
+ };
1215
+
1216
+ // ============================================================================
1217
+ // CLEANUP AND EXPORTS
1218
+ // ============================================================================
1219
+
1220
+ /**
1221
+ * Enhanced cleanup function with proper resource management
1222
+ */
1223
+ globalThis.cleanup = () => {
1224
+ try {
1225
+ window._appState = 'destroyed';
1226
+
1227
+ // Clear event listeners
1228
+ clearMapViewEventListeners();
1229
+
1230
+ // Clear operation queue
1231
+ window._operationQueue = [];
1232
+ window._isProcessingQueue = false;
1233
+
1234
+ // Reset bridge health
1235
+ window._bridgeHealth = {
1236
+ isConnected: false,
1237
+ lastHeartbeat: null,
1238
+ connectionAttempts: 0,
1239
+ maxRetries: 3
1240
+ };
1241
+
1242
+ // Cleanup map view
1243
+ if (window._mapView && typeof window._mapView.destroy === 'function') {
1244
+ window._mapView.destroy();
1245
+ }
1246
+ window._mapView = null;
1247
+
1248
+ // Clear site data
1249
+ window._site = null;
1250
+
1251
+ // Clear any stored failed messages
1252
+ try {
1253
+ localStorage.removeItem('becomap_failed_messages');
1254
+ } catch (e) {
1255
+ console.warn('Failed to clear localStorage:', e);
1256
+ }
1257
+
1258
+ // Final notification to native
1259
+ notifyNative("onCleanupComplete", {}, true); // Skip queue for final message
1260
+
1261
+ } catch (error) {
1262
+ notifyNative("onError", {
1263
+ operation: "cleanup",
1264
+ error: getSimpleErrorMessage(error)
1265
+ }, true);
1266
+ }
1267
+ };
1268
+
1269
+ /**
1270
+ * Application state management
1271
+ */
1272
+ globalThis.getAppState = () => {
1273
+ const state = {
1274
+ appState: window._appState,
1275
+ hasMapView: !!window._mapView,
1276
+ hasSite: !!window._site
1277
+ };
1278
+
1279
+ notifyNative("onGetAppState", state);
1280
+ return state;
1281
+ };
1282
+
1283
+ /**
1284
+ * Health check function for native to verify bridge connectivity
1285
+ */
1286
+ globalThis.healthCheck = () => {
1287
+ const healthData = {
1288
+ appState: window._appState,
1289
+ bridgeConnected: isNativeBridgeAvailable(),
1290
+ mapViewReady: !!window._mapView,
1291
+ siteLoaded: !!window._site
1292
+ };
1293
+
1294
+ updateBridgeHealth(true); // Update heartbeat
1295
+ notifyNative("onHealthCheck", healthData);
1296
+
1297
+ return healthData;
1298
+ };
1299
+
1300
+ /**
1301
+ * Error recovery function
1302
+ */
1303
+ globalThis.recoverFromError = () => {
1304
+ try {
1305
+
1306
+
1307
+ // Reset app state if in error
1308
+ if (window._appState === 'error') {
1309
+ window._appState = 'initializing';
1310
+ }
1311
+
1312
+ // Clear failed operations
1313
+ window._operationQueue = [];
1314
+ window._isProcessingQueue = false;
1315
+
1316
+ // Reset bridge health
1317
+ updateBridgeHealth(isNativeBridgeAvailable());
1318
+
1319
+ // Process any pending operations
1320
+ processOperationQueue();
1321
+
1322
+ notifyNative("onErrorRecovery", {});
1323
+
1324
+ } catch (error) {
1325
+ notifyNative("onError", {
1326
+ operation: "recoverFromError",
1327
+ error: getSimpleErrorMessage(error)
1328
+ });
1329
+ }
1330
+ };
1331
+
1332
+ /**
1333
+ * Debug information function
1334
+ */
1335
+ globalThis.getDebugInfo = () => {
1336
+ const debugInfo = {
1337
+ appState: window._appState,
1338
+ hasMapView: !!window._mapView,
1339
+ hasSite: !!window._site,
1340
+ becomapLoaded: !!(window.becomap?.getSite && window.becomap?.getMapView)
1341
+ };
1342
+
1343
+ notifyNative("onGetDebugInfo", debugInfo);
1344
+ return debugInfo;
1345
+ };
1346
+
1347
+
1348
+
1349
+ /**
1350
+ * Global error handler for unhandled errors
1351
+ */
1352
+ window.addEventListener('error', (event) => {
1353
+ errorDebouncer.debounce('global-error', () => {
1354
+ notifyNative("onError", {
1355
+ operation: "global",
1356
+ error: "Unexpected error occurred"
1357
+ });
1358
+ });
1359
+ });
1360
+
1361
+
1362
+ // Initialize bridge health monitoring
1363
+ updateBridgeHealth(isNativeBridgeAvailable());
1364
+
1365
+ // Set up periodic health checks
1366
+ setInterval(() => {
1367
+ if (window._appState !== 'destroyed') {
1368
+ updateBridgeHealth(isNativeBridgeAvailable());
1369
+ }
1370
+ }, 5000); // Check every 5 seconds
1371
+
1372
+ // Export init function
1373
+ globalThis.init = init;</script>
1374
+ </body>