/** * MapGeo Safety System * Prevents MapGeo and third-party map plugins from crashing the page * Works in all browsers including Safari and Chrome * * @package HVAC_Community_Events * @since 2.0.0 */ (function() { 'use strict'; // Safety configuration const config = window.HVAC_MapGeo_Config || { maxRetries: 3, retryDelay: 2000, timeout: 10000, fallbackEnabled: true, debugMode: false }; const log = config.debugMode ? console.log.bind(console) : () => {}; const error = config.debugMode ? console.error.bind(console) : () => {}; log('[MapGeo Safety] Initializing protection system'); /** * Resource Load Monitor * Tracks and manages external script loading */ class ResourceLoadMonitor { constructor() { this.resources = new Map(); this.criticalResources = [ 'amcharts', 'mapgeo', 'interactive-geo-maps', 'map-widget' ]; this.setupMonitoring(); } setupMonitoring() { // Monitor script loading const originalAppendChild = Element.prototype.appendChild; const self = this; Element.prototype.appendChild = function(element) { if (element.tagName === 'SCRIPT' && element.src) { self.monitorScript(element); } return originalAppendChild.call(this, element); }; // Monitor existing scripts document.querySelectorAll('script[src]').forEach(script => { this.monitorScript(script); }); } monitorScript(script) { const src = script.src; const isCritical = this.criticalResources.some(resource => src.toLowerCase().includes(resource) ); if (isCritical) { log('[MapGeo Safety] Monitoring critical resource:', src); const timeoutId = setTimeout(() => { error('[MapGeo Safety] Resource timeout:', src); this.handleResourceFailure(src); }, config.timeout); script.addEventListener('load', () => { clearTimeout(timeoutId); log('[MapGeo Safety] Resource loaded:', src); this.resources.set(src, 'loaded'); }); script.addEventListener('error', () => { clearTimeout(timeoutId); error('[MapGeo Safety] Resource failed:', src); this.handleResourceFailure(src); }); } } handleResourceFailure(src) { this.resources.set(src, 'failed'); // Check if this is a MapGeo CDN resource if (src.includes('cdn') || src.includes('amcharts')) { log('[MapGeo Safety] CDN resource failed, activating fallback'); this.activateFallback(); } } activateFallback() { // Hide map container const mapContainers = document.querySelectorAll( '.igm-map-container, [class*="mapgeo"], [id*="map-"], .map-widget-container' ); mapContainers.forEach(container => { container.style.display = 'none'; }); // Show fallback content const fallback = document.getElementById('hvac-map-fallback'); if (fallback) { fallback.style.display = 'block'; } // Dispatch custom event window.dispatchEvent(new CustomEvent('hvac:mapgeo:fallback', { detail: { reason: 'resource_failure' } })); } } /** * MapGeo API Wrapper * Safely wraps MapGeo API calls */ class MapGeoAPIWrapper { constructor() { this.wrapAPIs(); } wrapAPIs() { // Wrap potential MapGeo global functions const mapGeoAPIs = [ 'MapGeoWidget', 'InteractiveGeoMaps', 'IGM', 'mapWidget' ]; mapGeoAPIs.forEach(api => { if (typeof window[api] !== 'undefined') { this.wrapAPI(api); } // Set up getter to wrap when loaded Object.defineProperty(window, `_original_${api}`, { value: window[api], writable: true }); Object.defineProperty(window, api, { get() { return window[`_wrapped_${api}`] || window[`_original_${api}`]; }, set(value) { window[`_original_${api}`] = value; window[`_wrapped_${api}`] = new Proxy(value, { construct(target, args) { try { return new target(...args); } catch (e) { error('[MapGeo Safety] Construction error:', e); return {}; } }, apply(target, thisArg, args) { try { return target.apply(thisArg, args); } catch (e) { error('[MapGeo Safety] Execution error:', e); return null; } } }); } }); }); } wrapAPI(apiName) { const original = window[apiName]; window[apiName] = new Proxy(original, { construct(target, args) { try { log(`[MapGeo Safety] Creating ${apiName} instance`); return new target(...args); } catch (e) { error(`[MapGeo Safety] Failed to create ${apiName}:`, e); return {}; } }, apply(target, thisArg, args) { try { log(`[MapGeo Safety] Calling ${apiName}`); return target.apply(thisArg, args); } catch (e) { error(`[MapGeo Safety] Failed to call ${apiName}:`, e); return null; } } }); } } /** * DOM Ready Safety * Ensures MapGeo only runs when DOM is safe */ class DOMReadySafety { constructor() { this.setupSafety(); } setupSafety() { // Intercept jQuery ready calls that might contain MapGeo code if (typeof jQuery !== 'undefined') { const originalReady = jQuery.fn.ready; jQuery.fn.ready = function(callback) { const wrappedCallback = function() { try { // Check if MapGeo elements exist before running const hasMapElements = document.querySelector( '.igm-map-container, [class*="mapgeo"], [id*="map-"]' ); if (hasMapElements || !callback.toString().includes('map')) { return callback.apply(this, arguments); } else { log('[MapGeo Safety] Skipping map-related ready callback - no map elements found'); } } catch (e) { error('[MapGeo Safety] Error in ready callback:', e); } }; return originalReady.call(this, wrappedCallback); }; } } } /** * CDN Health Checker * Proactively checks AmCharts CDN availability before MapGeo initialization */ class CDNHealthChecker { constructor() { this.criticalCDNs = [ 'https://cdn.amcharts.com/lib/version/4.10.29/core.js', 'https://cdn.amcharts.com/lib/version/4.10.29/maps.js', 'https://cdn.amcharts.com/lib/4/geodata/usaLow.js' ]; this.timeout = 5000; // 5 second timeout this.cacheKey = 'hvac_cdn_health'; this.cacheExpiry = 10 * 60 * 1000; // 10 minutes } async checkCDNHealth() { log('[MapGeo Safety] Checking AmCharts CDN health...'); // Check cached result first const cached = this.getCachedResult(); if (cached !== null) { log('[MapGeo Safety] Using cached CDN status:', cached); return cached; } // Test primary CDN endpoints const results = await Promise.allSettled( this.criticalCDNs.map(url => this.testCDNEndpoint(url)) ); // Consider CDN healthy if at least 2 out of 3 endpoints work const successCount = results.filter(r => r.status === 'fulfilled' && r.value).length; const isHealthy = successCount >= 2; log(`[MapGeo Safety] CDN health check: ${successCount}/${this.criticalCDNs.length} endpoints available`); // Cache result this.setCachedResult(isHealthy); return isHealthy; } async testCDNEndpoint(url) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(url, { method: 'HEAD', mode: 'no-cors', // Allow cross-origin requests signal: controller.signal }); clearTimeout(timeoutId); return true; // If we get here, endpoint is reachable } catch (e) { log(`[MapGeo Safety] CDN endpoint failed: ${url} - ${e.message}`); return false; } } getCachedResult() { try { const cached = sessionStorage.getItem(this.cacheKey); if (cached) { const data = JSON.parse(cached); if (Date.now() - data.timestamp < this.cacheExpiry) { return data.healthy; } } } catch (e) { log('[MapGeo Safety] Error reading CDN cache:', e.message); } return null; } setCachedResult(healthy) { try { const data = { healthy: healthy, timestamp: Date.now() }; sessionStorage.setItem(this.cacheKey, JSON.stringify(data)); } catch (e) { log('[MapGeo Safety] Error caching CDN status:', e.message); } } } /** * Initialize all safety systems with proactive CDN checking */ async function initializeSafetySystems() { // Only initialize on pages with potential maps if (!document.querySelector('[class*="map"], [id*="map"]')) { log('[MapGeo Safety] No map elements detected, skipping initialization'); return; } // CRITICAL: Check CDN health before allowing MapGeo to initialize const cdnChecker = new CDNHealthChecker(); const cdnHealthy = await cdnChecker.checkCDNHealth(); if (!cdnHealthy) { error('[MapGeo Safety] AmCharts CDN unavailable - activating immediate fallback'); // Show fallback state UIManager.showFallbackState(); // Dispatch event to notify other systems window.dispatchEvent(new CustomEvent('hvac:mapgeo:cdn_unavailable', { detail: { reason: 'amcharts_cdn_timeout' } })); log('[MapGeo Safety] Immediate fallback activated due to CDN unavailability'); return; } log('[MapGeo Safety] AmCharts CDN healthy - proceeding with MapGeo initialization'); // Show map state since CDN is healthy UIManager.showMapState(); // Initialize monitors new ResourceLoadMonitor(); new MapGeoAPIWrapper(); new DOMReadySafety(); // Set up periodic health check with shorter timeout now that we pre-checked CDN let healthCheckCount = 0; const healthCheckInterval = setInterval(() => { healthCheckCount++; // Check if map loaded successfully const mapLoaded = document.querySelector('.igm-map-loaded, .mapgeo-loaded, .map-initialized'); if (mapLoaded) { log('[MapGeo Safety] Map loaded successfully'); clearInterval(healthCheckInterval); } else if (healthCheckCount >= 6) { // Reduced to 6 seconds since we already verified CDN error('[MapGeo Safety] Map failed to load after 6 seconds (CDN was healthy)'); clearInterval(healthCheckInterval); // Activate fallback if configured if (config.fallbackEnabled) { const monitor = new ResourceLoadMonitor(); monitor.activateFallback(); } } }, 1000); log('[MapGeo Safety] All safety systems initialized with CDN pre-check'); } /** * Enhanced UI Management for CDN fallbacks */ class UIManager { static showLoadingState() { const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); if (loading) loading.style.display = 'block'; if (fallback) fallback.style.display = 'none'; if (mapWrapper) mapWrapper.style.display = 'none'; log('[MapGeo Safety] Loading state activated'); } static showFallbackState() { const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); if (loading) loading.style.display = 'none'; if (fallback) fallback.style.display = 'block'; if (mapWrapper) mapWrapper.style.display = 'none'; log('[MapGeo Safety] Fallback state activated'); } static showMapState() { const loading = document.getElementById('hvac-map-loading'); const fallback = document.getElementById('hvac-map-fallback'); const mapWrapper = document.querySelector('.hvac-mapgeo-wrapper'); if (loading) loading.style.display = 'none'; if (fallback) fallback.style.display = 'none'; if (mapWrapper) mapWrapper.style.display = 'block'; log('[MapGeo Safety] Map state activated'); } static setupRetryButton() { const retryButton = document.querySelector('.hvac-retry-map'); if (retryButton) { retryButton.addEventListener('click', async () => { retryButton.disabled = true; retryButton.textContent = 'Checking...'; try { UIManager.showLoadingState(); // Clear CDN health cache const cdnChecker = new CDNHealthChecker(); sessionStorage.removeItem(cdnChecker.cacheKey); // Re-check CDN health const isHealthy = await cdnChecker.checkCDNHealth(); if (isHealthy) { log('[MapGeo Safety] CDN healthy on retry - reloading page'); window.location.reload(); } else { UIManager.showFallbackState(); retryButton.textContent = 'Still Unavailable'; setTimeout(() => { retryButton.textContent = 'Try Loading Map Again'; retryButton.disabled = false; }, 3000); } } catch (e) { error('[MapGeo Safety] Error during retry:', e.message); UIManager.showFallbackState(); retryButton.textContent = 'Error - Try Again'; retryButton.disabled = false; } }); } } } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { UIManager.showLoadingState(); UIManager.setupRetryButton(); initializeSafetySystems(); }); } else { // DOM already loaded UIManager.showLoadingState(); UIManager.setupRetryButton(); initializeSafetySystems(); } // Enhanced safety API for debugging and manual control window.HVACMapGeoSafety = { config: config, reinitialize: initializeSafetySystems, activateFallback: () => { const monitor = new ResourceLoadMonitor(); monitor.activateFallback(); }, ui: UIManager, checkCDN: async () => { const checker = new CDNHealthChecker(); return await checker.checkCDNHealth(); }, clearCDNCache: () => { const checker = new CDNHealthChecker(); sessionStorage.removeItem(checker.cacheKey); log('[MapGeo Safety] CDN cache cleared'); } }; })();