becomap 1.6.2 → 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/lib/becomap.js +1 -1
- package/lib/index.d.ts +10 -0
- package/package.json +2 -2
- package/public/README.md +375 -0
- package/public/index.html +1032 -158
package/public/index.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
|
|
4
4
|
<head>
|
|
@@ -22,43 +22,177 @@
|
|
|
22
22
|
|
|
23
23
|
<body>
|
|
24
24
|
<div id="mapContainer"></div>
|
|
25
|
-
|
|
26
|
-
<!-- External libs -->
|
|
27
25
|
<script src="https://unpkg.com/maplibre-gl@4.4.1/dist/maplibre-gl.js"></script>
|
|
28
26
|
<script src="https://unpkg.com/@turf/turf@7.1.0/turf.min.js"></script>
|
|
29
|
-
|
|
30
|
-
<!-- Core Map SDK Integration -->
|
|
31
|
-
<script>
|
|
32
|
-
// ============================================================================
|
|
27
|
+
<script>// ============================================================================
|
|
33
28
|
// GLOBAL VARIABLES
|
|
34
29
|
// ============================================================================
|
|
35
30
|
window._mapView = null;
|
|
36
31
|
window._site = null;
|
|
37
32
|
window._eventListeners = new Map();
|
|
33
|
+
window._bridgeHealth = {
|
|
34
|
+
isConnected: false,
|
|
35
|
+
lastHeartbeat: null,
|
|
36
|
+
connectionAttempts: 0,
|
|
37
|
+
maxRetries: 3
|
|
38
|
+
};
|
|
39
|
+
window._operationQueue = [];
|
|
40
|
+
window._isProcessingQueue = false;
|
|
41
|
+
window._appState = 'initializing'; // 'initializing', 'ready', 'error', 'destroyed'
|
|
38
42
|
|
|
39
43
|
// ============================================================================
|
|
40
44
|
// UTILITY FUNCTIONS
|
|
41
45
|
// ============================================================================
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
|
-
*
|
|
48
|
+
* Checks if native bridge is available
|
|
49
|
+
* @returns {boolean} - True if native bridge is available
|
|
50
|
+
*/
|
|
51
|
+
function isNativeBridgeAvailable() {
|
|
52
|
+
return !!(window.webkit?.messageHandlers?.jsHandler || window.jsHandler?.postMessage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Updates bridge health status
|
|
57
|
+
* @param {boolean} isConnected - Connection status
|
|
58
|
+
*/
|
|
59
|
+
function updateBridgeHealth(isConnected) {
|
|
60
|
+
window._bridgeHealth.isConnected = isConnected;
|
|
61
|
+
window._bridgeHealth.lastHeartbeat = Date.now();
|
|
62
|
+
|
|
63
|
+
if (isConnected) {
|
|
64
|
+
window._bridgeHealth.connectionAttempts = 0;
|
|
65
|
+
processOperationQueue();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sends messages to native platform with retry mechanism
|
|
45
71
|
* @param {string} type - Message type
|
|
46
72
|
* @param {any} payload - Message payload
|
|
73
|
+
* @param {boolean} skipQueue - Skip queue and send immediately
|
|
74
|
+
*/
|
|
75
|
+
function notifyNative(type, payload = null, skipQueue = false) {
|
|
76
|
+
const message = { type, payload, timestamp: Date.now() };
|
|
77
|
+
|
|
78
|
+
if (!isNativeBridgeAvailable()) {
|
|
79
|
+
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
|
|
47
133
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
167
|
}
|
|
60
168
|
}
|
|
61
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
|
+
|
|
62
196
|
/**
|
|
63
197
|
* Safely converts objects to JSON with error handling
|
|
64
198
|
* @param {any} obj - Object to convert
|
|
@@ -68,28 +202,326 @@
|
|
|
68
202
|
try {
|
|
69
203
|
return obj?.toJSON ? obj.toJSON() : obj;
|
|
70
204
|
} catch (error) {
|
|
71
|
-
console.warn('Error converting to JSON:', error);
|
|
72
205
|
return obj;
|
|
73
206
|
}
|
|
74
207
|
}
|
|
75
208
|
|
|
76
209
|
/**
|
|
77
|
-
* Executes function with error handling and
|
|
210
|
+
* Executes function with enhanced error handling, timeout, and retry mechanism
|
|
78
211
|
* @param {Function} fn - Function to execute
|
|
79
212
|
* @param {string} errorType - Error type for native notification
|
|
213
|
+
* @param {any} errorDetails - Additional error details to include in the payload
|
|
80
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
|
|
81
303
|
*/
|
|
82
|
-
function
|
|
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
|
+
|
|
83
467
|
try {
|
|
84
468
|
const result = fn();
|
|
85
|
-
|
|
469
|
+
errorCircuitBreaker.recordSuccess(operation);
|
|
470
|
+
return result;
|
|
86
471
|
} catch (error) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
472
|
+
errorCircuitBreaker.recordFailure(operation);
|
|
473
|
+
errorCallback({
|
|
474
|
+
operation,
|
|
475
|
+
error: getSimpleErrorMessage(error)
|
|
476
|
+
});
|
|
90
477
|
}
|
|
91
478
|
}
|
|
92
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
|
+
|
|
93
525
|
// ============================================================================
|
|
94
526
|
// BCMapViewEvents CALLBACKS
|
|
95
527
|
// ============================================================================
|
|
@@ -109,44 +541,87 @@
|
|
|
109
541
|
|
|
110
542
|
// Map load event
|
|
111
543
|
const loadListenerId = mapView.eventsHandler.on('load', () => {
|
|
112
|
-
|
|
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
|
+
}
|
|
113
555
|
});
|
|
114
556
|
|
|
115
|
-
// View change event
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
569
|
|
|
123
570
|
// Location selection event
|
|
124
571
|
const selectListenerId = mapView.eventsHandler.on('select', (args) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
582
|
});
|
|
130
583
|
|
|
131
584
|
// Floor switch event
|
|
132
585
|
const switchToFloorListenerId = mapView.eventsHandler.on('switchToFloor', (args) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
597
|
});
|
|
138
598
|
|
|
139
599
|
// Route step load event
|
|
140
600
|
const stepLoadListenerId = mapView.eventsHandler.on('stepLoad', (args) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
612
|
});
|
|
146
613
|
|
|
147
614
|
// Walkthrough end event
|
|
148
615
|
const walkthroughEndListenerId = mapView.eventsHandler.on('walkthroughEnd', () => {
|
|
149
|
-
|
|
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
|
+
}
|
|
150
625
|
});
|
|
151
626
|
|
|
152
627
|
// Store listener IDs for cleanup
|
|
@@ -182,36 +657,115 @@
|
|
|
182
657
|
// ============================================================================
|
|
183
658
|
|
|
184
659
|
/**
|
|
185
|
-
* Initializes the map with site options
|
|
660
|
+
* Initializes the map with site options and enhanced error handling
|
|
186
661
|
* @param {Object} siteOptions - Site configuration options
|
|
187
662
|
*/
|
|
188
|
-
|
|
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
|
+
|
|
189
683
|
try {
|
|
190
684
|
const container = document.getElementById('mapContainer');
|
|
191
685
|
if (!container) {
|
|
192
686
|
throw new Error('Map container not found');
|
|
193
687
|
}
|
|
194
688
|
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
});
|
|
205
762
|
|
|
206
|
-
notifyNative("onRenderComplete", {
|
|
207
|
-
site: safeToJSON(window._site),
|
|
208
|
-
timestamp: Date.now()
|
|
209
|
-
});
|
|
210
763
|
} catch (err) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
764
|
+
window._appState = 'error';
|
|
765
|
+
|
|
766
|
+
notifyNative("onError", {
|
|
767
|
+
operation: "init",
|
|
768
|
+
error: getSimpleErrorMessage(err)
|
|
215
769
|
});
|
|
216
770
|
}
|
|
217
771
|
}
|
|
@@ -222,68 +776,104 @@
|
|
|
222
776
|
|
|
223
777
|
// Floor and Location Methods
|
|
224
778
|
globalThis.getCurrentFloor = () => {
|
|
225
|
-
|
|
779
|
+
executeWithErrorHandling(
|
|
226
780
|
() => window._mapView?.currentFloor,
|
|
227
|
-
"
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
});
|
|
230
789
|
};
|
|
231
790
|
|
|
232
791
|
globalThis.selectFloorWithId = (floor) => {
|
|
233
792
|
executeWithErrorHandling(
|
|
234
793
|
() => window._mapView?.selectFloorWithId(floor),
|
|
235
|
-
"
|
|
794
|
+
"onError",
|
|
795
|
+
{ operation: "selectFloorWithId", floor }
|
|
236
796
|
);
|
|
237
797
|
};
|
|
238
798
|
|
|
239
799
|
globalThis.selectLocationWithId = (location) => {
|
|
240
800
|
executeWithErrorHandling(
|
|
241
801
|
() => window._mapView?.selectLocationWithId(location),
|
|
242
|
-
"
|
|
802
|
+
"onError",
|
|
803
|
+
{ operation: "selectLocationWithId", location }
|
|
243
804
|
);
|
|
244
805
|
};
|
|
245
806
|
|
|
246
807
|
// Data Retrieval Methods
|
|
247
808
|
globalThis.getCategories = () => {
|
|
248
|
-
|
|
809
|
+
executeWithErrorHandling(
|
|
249
810
|
() => window._mapView?.getCategories(),
|
|
250
|
-
"
|
|
811
|
+
"onError",
|
|
812
|
+
{ operation: "getCategories" },
|
|
251
813
|
[]
|
|
252
|
-
)
|
|
253
|
-
|
|
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
|
+
});
|
|
254
822
|
};
|
|
255
823
|
|
|
256
824
|
globalThis.getLocations = () => {
|
|
257
|
-
|
|
825
|
+
executeWithErrorHandling(
|
|
258
826
|
() => window._mapView?.getLocations(),
|
|
259
|
-
"
|
|
827
|
+
"onError",
|
|
828
|
+
{ operation: "getLocations" },
|
|
260
829
|
[]
|
|
261
|
-
)
|
|
262
|
-
|
|
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
|
+
});
|
|
263
838
|
};
|
|
264
839
|
|
|
265
|
-
globalThis.
|
|
266
|
-
|
|
840
|
+
globalThis.getAmenities = () => {
|
|
841
|
+
executeWithErrorHandling(
|
|
267
842
|
() => window._mapView?.getAllAminityLocations(),
|
|
268
|
-
"
|
|
843
|
+
"onError",
|
|
844
|
+
{ operation: "getAmenities" },
|
|
269
845
|
[]
|
|
270
|
-
)
|
|
271
|
-
|
|
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
|
+
});
|
|
272
854
|
};
|
|
273
855
|
|
|
274
|
-
globalThis.
|
|
275
|
-
|
|
856
|
+
globalThis.getAmenityTypes = () => {
|
|
857
|
+
executeWithErrorHandling(
|
|
276
858
|
() => window._mapView?.getAmenities(),
|
|
277
|
-
"
|
|
859
|
+
"onError",
|
|
860
|
+
{ operation: "getAmenityTypes" },
|
|
278
861
|
[]
|
|
279
|
-
)
|
|
280
|
-
|
|
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
|
+
});
|
|
281
870
|
};
|
|
282
871
|
|
|
283
872
|
globalThis.selectAmenities = (type) => {
|
|
284
873
|
executeWithErrorHandling(
|
|
285
874
|
() => window._mapView?.selectAmenities(type),
|
|
286
|
-
"
|
|
875
|
+
"onError",
|
|
876
|
+
{ operation: "selectAmenities", type }
|
|
287
877
|
);
|
|
288
878
|
};
|
|
289
879
|
|
|
@@ -291,113 +881,176 @@
|
|
|
291
881
|
globalThis.getSessionId = async () => {
|
|
292
882
|
try {
|
|
293
883
|
const sessionId = await window._mapView?.getSessionId();
|
|
294
|
-
notifyNative("
|
|
884
|
+
notifyNative("onGetSessionId", sessionId);
|
|
295
885
|
} catch (err) {
|
|
296
|
-
notifyNative("
|
|
297
|
-
|
|
298
|
-
|
|
886
|
+
notifyNative("onError", {
|
|
887
|
+
operation: "getSessionId",
|
|
888
|
+
error: getSimpleErrorMessage(err)
|
|
299
889
|
});
|
|
300
890
|
}
|
|
301
891
|
};
|
|
302
892
|
|
|
303
|
-
globalThis.getQuestions = () => {
|
|
304
|
-
const questions = executeWithErrorHandling(
|
|
305
|
-
() => window._mapView?.getQuestions(),
|
|
306
|
-
"getQuestionsError",
|
|
307
|
-
[]
|
|
308
|
-
);
|
|
309
|
-
notifyNative("getQuestions", questions?.map(q => safeToJSON(q)) || []);
|
|
310
|
-
};
|
|
311
|
-
|
|
312
893
|
globalThis.getHappenings = (type) => {
|
|
313
|
-
|
|
894
|
+
executeWithErrorHandling(
|
|
314
895
|
() => window._mapView?.getHappenings(type),
|
|
315
|
-
"
|
|
896
|
+
"onError",
|
|
897
|
+
{ operation: "getHappenings", type },
|
|
316
898
|
[]
|
|
317
|
-
)
|
|
318
|
-
|
|
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
|
+
});
|
|
319
907
|
};
|
|
320
908
|
|
|
321
909
|
globalThis.getEventSuggestions = async (sessionId, answers) => {
|
|
322
910
|
try {
|
|
323
911
|
const suggestions = await window._mapView?.getEventSuggestions(sessionId, answers);
|
|
324
|
-
notifyNative("
|
|
912
|
+
notifyNative("onGetEventSuggestions", suggestions?.map(s => safeToJSON(s)) || []);
|
|
325
913
|
} catch (err) {
|
|
326
|
-
notifyNative("
|
|
327
|
-
|
|
328
|
-
|
|
914
|
+
notifyNative("onError", {
|
|
915
|
+
operation: "getEventSuggestions",
|
|
916
|
+
error: getSimpleErrorMessage(err)
|
|
329
917
|
});
|
|
330
918
|
}
|
|
331
919
|
};
|
|
332
920
|
|
|
333
921
|
// Viewport and Camera Methods
|
|
334
|
-
globalThis.focusTo = (
|
|
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
|
+
|
|
335
961
|
executeWithErrorHandling(
|
|
336
962
|
() => window._mapView?.focusTo(location, zoom, bearing, pitch),
|
|
337
|
-
"
|
|
963
|
+
"onError",
|
|
964
|
+
{ operation: "focusTo" }
|
|
338
965
|
);
|
|
339
966
|
};
|
|
340
967
|
|
|
341
968
|
globalThis.clearSelection = () => {
|
|
342
969
|
executeWithErrorHandling(
|
|
343
970
|
() => window._mapView?.clearSelection(),
|
|
344
|
-
"
|
|
971
|
+
"onError",
|
|
972
|
+
{ operation: "clearSelection" }
|
|
345
973
|
);
|
|
346
974
|
};
|
|
347
975
|
|
|
348
976
|
globalThis.updateZoom = (zoom) => {
|
|
349
977
|
executeWithErrorHandling(
|
|
350
978
|
() => window._mapView?.updateZoom(zoom),
|
|
351
|
-
"
|
|
979
|
+
"onError",
|
|
980
|
+
{ operation: "updateZoom", zoom }
|
|
352
981
|
);
|
|
353
982
|
};
|
|
354
983
|
|
|
355
984
|
globalThis.updatePitch = (pitch) => {
|
|
356
985
|
executeWithErrorHandling(
|
|
357
986
|
() => window._mapView?.updatePitch(pitch),
|
|
358
|
-
"
|
|
987
|
+
"onError",
|
|
988
|
+
{ operation: "updatePitch", pitch }
|
|
359
989
|
);
|
|
360
990
|
};
|
|
361
991
|
|
|
362
992
|
globalThis.updateBearing = (bearing) => {
|
|
363
993
|
executeWithErrorHandling(
|
|
364
994
|
() => window._mapView?.updateBearing(bearing),
|
|
365
|
-
"
|
|
995
|
+
"onError",
|
|
996
|
+
{ operation: "updateBearing", bearing }
|
|
366
997
|
);
|
|
367
998
|
};
|
|
368
999
|
|
|
369
1000
|
globalThis.enableMultiSelection = (val) => {
|
|
370
1001
|
executeWithErrorHandling(
|
|
371
1002
|
() => window._mapView?.enableMultiSelection(val),
|
|
372
|
-
"
|
|
1003
|
+
"onError",
|
|
1004
|
+
{ operation: "enableMultiSelection", value: val }
|
|
373
1005
|
);
|
|
374
1006
|
};
|
|
375
1007
|
|
|
376
1008
|
globalThis.setBounds = (sw, ne) => {
|
|
377
1009
|
executeWithErrorHandling(
|
|
378
1010
|
() => window._mapView?.setBounds(sw, ne),
|
|
379
|
-
"
|
|
1011
|
+
"onError",
|
|
1012
|
+
{ operation: "setBounds", southwest: sw, northeast: ne }
|
|
380
1013
|
);
|
|
381
1014
|
};
|
|
382
1015
|
|
|
383
1016
|
globalThis.setViewport = (options) => {
|
|
384
1017
|
executeWithErrorHandling(
|
|
385
1018
|
() => window._mapView?.setViewport(options),
|
|
386
|
-
"
|
|
1019
|
+
"onError",
|
|
1020
|
+
{ operation: "setViewport", options }
|
|
387
1021
|
);
|
|
388
1022
|
};
|
|
389
1023
|
|
|
390
1024
|
globalThis.resetDefaultViewport = (options) => {
|
|
391
1025
|
executeWithErrorHandling(
|
|
392
1026
|
() => window._mapView?.resetDefaultViewport(options),
|
|
393
|
-
"
|
|
1027
|
+
"onError",
|
|
1028
|
+
{ operation: "resetDefaultViewport", options }
|
|
394
1029
|
);
|
|
395
1030
|
};
|
|
396
1031
|
|
|
397
1032
|
// Search Methods
|
|
398
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
|
+
|
|
399
1052
|
if (!window._mapView?.searchForLocations) {
|
|
400
|
-
notifyNative("
|
|
1053
|
+
notifyNative("onSearchForLocations", {
|
|
401
1054
|
callbackId,
|
|
402
1055
|
results: [],
|
|
403
1056
|
error: "Search method not available"
|
|
@@ -405,17 +1058,25 @@
|
|
|
405
1058
|
return;
|
|
406
1059
|
}
|
|
407
1060
|
|
|
408
|
-
|
|
409
|
-
|
|
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", {
|
|
410
1070
|
callbackId,
|
|
411
|
-
results:
|
|
1071
|
+
results: [],
|
|
1072
|
+
error: getSimpleErrorMessage(error)
|
|
412
1073
|
});
|
|
413
|
-
}
|
|
1074
|
+
}
|
|
414
1075
|
};
|
|
415
1076
|
|
|
416
1077
|
globalThis.searchForCategories = (q, callbackId) => {
|
|
417
1078
|
if (!window._mapView?.searchForCategories) {
|
|
418
|
-
notifyNative("
|
|
1079
|
+
notifyNative("onSearchForCategories", {
|
|
419
1080
|
callbackId,
|
|
420
1081
|
results: [],
|
|
421
1082
|
error: "Search method not available"
|
|
@@ -424,7 +1085,7 @@
|
|
|
424
1085
|
}
|
|
425
1086
|
|
|
426
1087
|
window._mapView.searchForCategories(q, (matches) => {
|
|
427
|
-
notifyNative("
|
|
1088
|
+
notifyNative("onSearchForCategories", {
|
|
428
1089
|
callbackId,
|
|
429
1090
|
results: matches?.map(m => safeToJSON(m)) || []
|
|
430
1091
|
});
|
|
@@ -436,35 +1097,119 @@
|
|
|
436
1097
|
// ============================================================================
|
|
437
1098
|
|
|
438
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
|
+
|
|
439
1128
|
try {
|
|
440
1129
|
const routes = window.becomap.getRouteById(startID, goalID, waypoints, routeOptions);
|
|
441
|
-
|
|
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
|
+
}
|
|
442
1162
|
} catch (error) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 }
|
|
446
1168
|
});
|
|
447
1169
|
}
|
|
448
1170
|
};
|
|
449
1171
|
|
|
450
|
-
globalThis.showRoute = () => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
"
|
|
454
|
-
|
|
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
|
+
}
|
|
455
1198
|
};
|
|
456
1199
|
|
|
457
1200
|
globalThis.showStep = (step) => {
|
|
458
1201
|
executeWithErrorHandling(
|
|
459
1202
|
() => window._mapView?.routeController?.showStepByOrderIndex(step),
|
|
460
|
-
"
|
|
1203
|
+
"onError",
|
|
1204
|
+
{ operation: "showStep", step }
|
|
461
1205
|
);
|
|
462
1206
|
};
|
|
463
1207
|
|
|
464
1208
|
globalThis.clearAllRoutes = () => {
|
|
465
1209
|
executeWithErrorHandling(
|
|
466
1210
|
() => window._mapView?.routeController?.clearAllRoutes(),
|
|
467
|
-
"
|
|
1211
|
+
"onError",
|
|
1212
|
+
{ operation: "clearAllRoutes" }
|
|
468
1213
|
);
|
|
469
1214
|
};
|
|
470
1215
|
|
|
@@ -473,28 +1218,157 @@
|
|
|
473
1218
|
// ============================================================================
|
|
474
1219
|
|
|
475
1220
|
/**
|
|
476
|
-
*
|
|
1221
|
+
* Enhanced cleanup function with proper resource management
|
|
477
1222
|
*/
|
|
478
1223
|
globalThis.cleanup = () => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
}
|
|
482
1267
|
};
|
|
483
1268
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
};
|
|
487
1282
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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;
|
|
493
1298
|
};
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
});
|
|
496
1359
|
});
|
|
497
|
-
</script> -->
|
|
498
|
-
</body>
|
|
499
1360
|
|
|
500
|
-
|
|
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>
|