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.
- package/README.md +15 -15
- package/lib/becomap.js +1 -1
- package/lib/index.d.ts +93 -75
- package/package.json +72 -70
- package/public/README.md +375 -0
- package/public/index.html +1374 -0
- package/webpack.umd.config.js +91 -0
|
@@ -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>
|