T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/.cursor-server/data/User/History/6b963d87/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/.cursor-server/data/User/History/6b963d87/Z0bb.js
/**
 * SUPER ADVANCED INTERACTIVE MAP
 * Full mobile + desktop support with touch gestures, clustering, geolocation, and more
 */

(function() {
    'use strict';

    const CONFIG = {
        quebecBounds: {
            minLat: 45.0,
            maxLat: 51.0,
            minLng: -80.0,
            maxLng: -66.0
        },
        zoom: {
            min: 0.5,
            max: 6,
            default: 1.2,
            step: 1.3,
            showVillagesThreshold: 2.5,
            showLabelsThreshold: 2.0,
            pinchSensitivity: 0.01
        },
        animation: {
            speed: 0.15,
            threshold: 0.01,
            fps: 60
        },
        clustering: {
            enabled: true,
            maxZoom: 2.0,
            radius: 50
        },
        mobile: {
            touchRadius: 20,
            minTapTime: 200,
            maxTapDistance: 10
        },
        performance: {
            viewportMargin: 200,
            maxRenderItems: 1000,
            throttleDelay: 16
        }
    };

    let state = {
        cities: [],
        villages: [],
        filteredCities: [],
        filteredVillages: [],
        clusters: [],
        selectedVillage: null,
        hoveredCity: null,
        hoveredVillage: null,
        zoom: CONFIG.zoom.default,
        targetZoom: CONFIG.zoom.default,
        panX: 0,
        panY: 0,
        targetPanX: 0,
        targetPanY: 0,
        isDragging: false,
        isPinching: false,
        dragStart: { x: 0, y: 0 },
        touchStart: { x: 0, y: 0, time: 0, distance: 0 },
        pinchStart: { distance: 0, center: { x: 0, y: 0 } },
        searchQuery: '',
        regionFilter: 'all',
        statusFilter: 'all',
        animationFrame: 0,
        lastFrameTime: 0,
        isMobile: false,
        userLocation: null,
        showUserLocation: false
    };

    let canvas, ctx, container, tooltip, searchInput, searchResults;
    const lang = document.documentElement.lang || 'en';

    // Detect mobile device
    function detectMobile() {
        return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
               (window.innerWidth <= 768);
    }

    function init() {
        container = document.getElementById('homepageInteractiveMap');
        if (!container) return;

        canvas = document.getElementById('homepageMapCanvas');
        if (!canvas) return;

        ctx = canvas.getContext('2d');
        if (!ctx) return;

        state.isMobile = detectMobile();
        
        createTooltip();
        createSearchUI();
        setupUI();
        setupEvents();
        resize();
        loadData();
        requestGeolocation();
        animate();
    }

    function createTooltip() {
        tooltip = document.createElement('div');
        tooltip.id = 'mapTooltip';
        tooltip.className = 'map-tooltip';
        tooltip.style.cssText = `
            position: absolute;
            pointer-events: none;
            background: var(--color-bg-card);
            border: 2px solid var(--color-border);
            border-radius: 12px;
            padding: 0.75rem 1rem;
            color: var(--color-text);
            font-size: ${state.isMobile ? '0.85rem' : '0.9rem'};
            font-weight: 600;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
            z-index: 1000;
            opacity: 0;
            transition: opacity 0.2s ease;
            max-width: ${state.isMobile ? '200px' : '250px'};
        `;
        container.appendChild(tooltip);
    }

    function createSearchUI() {
        // Enhanced search with autocomplete
        searchInput = document.getElementById('mapSearch');
        if (!searchInput) return;

        searchResults = document.createElement('div');
        searchResults.id = 'mapSearchResults';
        searchResults.className = 'map-search-results';
        searchResults.style.cssText = `
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            margin-top: 0.5rem;
            background: var(--color-bg-card);
            border: 2px solid var(--color-border);
            border-radius: 12px;
            max-height: 300px;
            overflow-y: auto;
            display: none;
            z-index: 1000;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
        `;
        searchInput.parentElement.style.position = 'relative';
        searchInput.parentElement.appendChild(searchResults);

        searchInput.addEventListener('input', handleSearchInput);
        searchInput.addEventListener('focus', () => {
            if (state.searchQuery) updateSearchResults();
        });
        document.addEventListener('click', (e) => {
            if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
                searchResults.style.display = 'none';
            }
        });
    }

    function handleSearchInput(e) {
        state.searchQuery = e.target.value.toLowerCase();
        if (state.searchQuery.length > 0) {
            updateSearchResults();
            searchResults.style.display = 'block';
        } else {
            searchResults.style.display = 'none';
            applyFilters();
        }
    }

    function updateSearchResults() {
        if (!searchResults) return;
        
        const query = state.searchQuery.toLowerCase();
        const matches = [];
        
        // Search cities
        state.cities.forEach(city => {
            if (city.name.toLowerCase().includes(query) || 
                (city.region && city.region.toLowerCase().includes(query))) {
                matches.push({ type: 'city', ...city });
            }
        });
        
        // Search villages
        state.villages.forEach(village => {
            const name = (lang === 'fr' && village.name_fr) ? village.name_fr : village.name;
            if (name.toLowerCase().includes(query) ||
                (village.region && village.region.toLowerCase().includes(query))) {
                matches.push({ type: 'village', ...village });
            }
        });
        
        // Limit results
        const limitedMatches = matches.slice(0, 10);
        
        searchResults.innerHTML = limitedMatches.map(item => {
            const name = item.type === 'village' 
                ? ((lang === 'fr' && item.name_fr) ? item.name_fr : item.name)
                : item.name;
            const subtitle = item.type === 'village' 
                ? `${item.region || ''} • ${item.member_count || 0} ${lang === 'fr' ? 'membres' : 'members'}`
                : `${item.region || ''} • ${(item.population || 0).toLocaleString()} ${lang === 'fr' ? 'habitants' : 'people'}`;
            
            return `
                <div class="search-result-item" data-type="${item.type}" data-id="${item.id || item.slug}" style="
                    padding: 1rem;
                    cursor: pointer;
                    border-bottom: 1px solid var(--color-border);
                    transition: background 0.2s;
                " onmouseover="this.style.background='var(--color-bg-light)'" onmouseout="this.style.background='transparent'">
                    <div style="font-weight: 600; color: var(--color-text); margin-bottom: 0.25rem;">${name}</div>
                    <div style="font-size: 0.85rem; color: var(--color-text-secondary);">${subtitle}</div>
                </div>
            `;
        }).join('');
        
        // Add click handlers
        searchResults.querySelectorAll('.search-result-item').forEach(item => {
            item.addEventListener('click', () => {
                const type = item.dataset.type;
                const id = item.dataset.id;
                
                if (type === 'village') {
                    const village = state.villages.find(v => v.slug === id);
                    if (village) {
                        state.selectedVillage = village;
                        showVillageDetails(village);
                        searchInput.value = '';
                        state.searchQuery = '';
                        searchResults.style.display = 'none';
                    }
                } else {
                    const city = state.cities.find(c => c.id == id || c.name === id);
                    if (city) {
                        // Zoom to city
                        state.targetZoom = CONFIG.zoom.showVillagesThreshold;
                        state.targetPanX = city.x;
                        state.targetPanY = city.y;
                        searchInput.value = '';
                        state.searchQuery = '';
                        searchResults.style.display = 'none';
                    }
                }
            });
        });
    }

    function showTooltip(x, y, text) {
        if (!tooltip || !container) return;
        tooltip.textContent = text;
        
        const rect = container.getBoundingClientRect();
        const tooltipX = Math.min(x + 15, rect.width - (state.isMobile ? 200 : 250));
        const tooltipY = Math.max(y - 10, 10);
        
        tooltip.style.left = tooltipX + 'px';
        tooltip.style.top = tooltipY + 'px';
        tooltip.style.opacity = '1';
    }

    function hideTooltip() {
        if (!tooltip) return;
        tooltip.style.opacity = '0';
    }

    function setupUI() {
        const zoomIn = document.getElementById('mapZoomIn');
        const zoomOut = document.getElementById('mapZoomOut');
        const resetView = document.getElementById('resetMapView');
        const regionFilter = document.getElementById('mapRegionFilter');
        const statusFilter = document.getElementById('mapStatusFilter');
        const closePanel = document.getElementById('closeVillagePanel');
        const locateBtn = document.getElementById('mapLocateBtn');

        if (zoomIn) zoomIn.addEventListener('click', () => zoom(1));
        if (zoomOut) zoomOut.addEventListener('click', () => zoom(-1));
        if (resetView) resetView.addEventListener('click', resetMapView);
        if (regionFilter) regionFilter.addEventListener('change', handleRegionFilter);
        if (statusFilter) statusFilter.addEventListener('change', handleStatusFilter);
        if (closePanel) closePanel.addEventListener('click', closeVillagePanel);
        if (locateBtn) locateBtn.addEventListener('click', centerOnUserLocation);
    }

    function setupEvents() {
        // Mouse events
        canvas.addEventListener('mousedown', handleMouseDown);
        canvas.addEventListener('mousemove', handleMouseMove);
        canvas.addEventListener('mouseup', handleMouseUp);
        canvas.addEventListener('mouseleave', () => {
            state.isDragging = false;
            hideTooltip();
        });
        canvas.addEventListener('wheel', handleWheel, { passive: false });
        canvas.addEventListener('click', handleClick);

        // Touch events
        canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
        canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
        canvas.addEventListener('touchend', handleTouchEnd, { passive: false });
        canvas.addEventListener('touchcancel', handleTouchEnd, { passive: false });

        // Keyboard events
        canvas.setAttribute('tabindex', '0');
        canvas.addEventListener('keydown', handleKeyDown);

        window.addEventListener('resize', debounce(resize, 250));
    }

    function handleTouchStart(e) {
        e.preventDefault();
        const touches = e.touches;
        
        if (touches.length === 1) {
            // Single touch - start drag
            const touch = touches[0];
            const rect = canvas.getBoundingClientRect();
            state.isDragging = true;
            state.touchStart.x = touch.clientX - rect.left;
            state.touchStart.y = touch.clientY - rect.top;
            state.touchStart.time = Date.now();
            state.dragStart.x = state.touchStart.x;
            state.dragStart.y = state.touchStart.y;
        } else if (touches.length === 2) {
            // Pinch zoom
            state.isDragging = false;
            state.isPinching = true;
            const touch1 = touches[0];
            const touch2 = touches[1];
            const rect = canvas.getBoundingClientRect();
            
            const x1 = touch1.clientX - rect.left;
            const y1 = touch1.clientY - rect.top;
            const x2 = touch2.clientX - rect.left;
            const y2 = touch2.clientY - rect.top;
            
            const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
            const centerX = (x1 + x2) / 2;
            const centerY = (y1 + y2) / 2;
            
            state.pinchStart.distance = distance;
            state.pinchStart.center = { x: centerX, y: centerY };
            state.pinchStart.zoom = state.zoom;
        }
    }

    function handleTouchMove(e) {
        e.preventDefault();
        const touches = e.touches;
        
        if (state.isPinching && touches.length === 2) {
            // Pinch zoom
            const touch1 = touches[0];
            const touch2 = touches[1];
            const rect = canvas.getBoundingClientRect();
            
            const x1 = touch1.clientX - rect.left;
            const y1 = touch1.clientY - rect.top;
            const x2 = touch2.clientX - rect.left;
            const y2 = touch2.clientY - rect.top;
            
            const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
            const scale = distance / state.pinchStart.distance;
            
            const newZoom = state.pinchStart.zoom * scale;
            state.targetZoom = Math.max(CONFIG.zoom.min, Math.min(CONFIG.zoom.max, newZoom));
            
            const zoomChange = state.targetZoom / state.pinchStart.zoom;
            const centerX = state.pinchStart.center.x;
            const centerY = state.pinchStart.center.y;
            
            state.targetPanX = centerX - (centerX - state.targetPanX) * zoomChange;
            state.targetPanY = centerY - (centerY - state.targetPanY) * zoomChange;
        } else if (state.isDragging && touches.length === 1) {
            // Pan
            const touch = touches[0];
            const rect = canvas.getBoundingClientRect();
            const x = touch.clientX - rect.left;
            const y = touch.clientY - rect.top;
            
            state.targetPanX += x - state.dragStart.x;
            state.targetPanY += y - state.dragStart.y;
            state.panX = state.targetPanX;
            state.panY = state.targetPanY;
            state.dragStart.x = x;
            state.dragStart.y = y;
            updatePositions();
        }
    }

    function handleTouchEnd(e) {
        e.preventDefault();
        const touches = e.touches;
        
        if (touches.length === 0) {
            // All touches ended
            if (state.isPinching) {
                state.isPinching = false;
            } else if (state.isDragging) {
                // Check if it was a tap
                const time = Date.now() - state.touchStart.time;
                const dx = state.dragStart.x - state.touchStart.x;
                const dy = state.dragStart.y - state.touchStart.y;
                const distance = Math.sqrt(dx * dx + dy * dy);
                
                if (time < CONFIG.mobile.minTapTime && distance < CONFIG.mobile.maxTapDistance) {
                    // It was a tap - handle click
                    handleTap(state.touchStart.x, state.touchStart.y);
                }
            }
            state.isDragging = false;
        } else if (touches.length === 1 && state.isPinching) {
            // Pinch ended, switch to drag
            state.isPinching = false;
            state.isDragging = true;
            const touch = touches[0];
            const rect = canvas.getBoundingClientRect();
            state.dragStart.x = touch.clientX - rect.left;
            state.dragStart.y = touch.clientY - rect.top;
        }
    }

    function handleTap(x, y) {
        const item = getItemAtPosition(x, y);
        
        if (item && item.slug) {
            const isActive = item.status === 'active';
            if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
                state.selectedVillage = item;
                showVillageDetails(item);
            }
        } else if (item && !item.slug) {
            // Zoom to city
            state.targetZoom = CONFIG.zoom.showVillagesThreshold;
            state.targetPanX = item.x;
            state.targetPanY = item.y;
        }
    }

    function handleKeyDown(e) {
        switch(e.key) {
            case '+':
            case '=':
                zoom(1);
                break;
            case '-':
            case '_':
                zoom(-1);
                break;
            case 'ArrowUp':
                state.targetPanY -= 50;
                break;
            case 'ArrowDown':
                state.targetPanY += 50;
                break;
            case 'ArrowLeft':
                state.targetPanX -= 50;
                break;
            case 'ArrowRight':
                state.targetPanX += 50;
                break;
            case 'Home':
                resetMapView();
                break;
        }
    }

    async function loadData() {
        try {
            const citiesResponse = await fetch('/api/endpoints/cities.php');
            
            if (!citiesResponse.ok) {
                useFallbackData();
                return;
            }
            
            const text = await citiesResponse.text();
            
            if (!text || text.trim() === '') {
                useFallbackData();
                return;
            }
            
            // Clean the text - remove any BOM or whitespace
            const cleanText = text.trim();
            
            if (!cleanText || cleanText.length === 0) {
                useFallbackData();
                return;
            }
            
            let citiesData;
            try {
                citiesData = JSON.parse(cleanText);
            } catch (e) {
                // Invalid JSON - use fallback silently
                useFallbackData();
                return;
            }
            
            if (citiesData && citiesData.success && citiesData.cities && Array.isArray(citiesData.cities) && citiesData.cities.length > 0) {
                const bounds = CONFIG.quebecBounds;
                state.cities = citiesData.cities.filter(c => {
                    const lat = parseFloat(c.lat);
                    const lng = parseFloat(c.lng);
                    return lat >= bounds.minLat && lat <= bounds.maxLat &&
                           lng >= bounds.minLng && lng <= bounds.maxLng;
                }).map(c => ({
                    ...c,
                    lat: parseFloat(c.lat),
                    lng: parseFloat(c.lng),
                    population: parseInt(c.population) || 0,
                    x: 0,
                    y: 0,
                    radius: getCityRadius(parseInt(c.population) || 0)
                }));
            } else {
                useFallbackData();
            }
            
            if (state.cities.length === 0) {
                useFallbackData();
            }

            state.filteredCities = [...state.cities];

            // Load villages
            try {
                const villagesResponse = await fetch('/api/endpoints/map-villages.php');
                if (villagesResponse.ok) {
                    const villagesText = await villagesResponse.text();
                    if (villagesText && villagesText.trim()) {
                        try {
                            const villagesData = JSON.parse(villagesText.trim());
                            if (villagesData && villagesData.success && villagesData.villages && Array.isArray(villagesData.villages)) {
                        const bounds = CONFIG.quebecBounds;
                        state.villages = villagesData.villages.filter(v => {
                            const lat = parseFloat(v.lat);
                            const lng = parseFloat(v.lng);
                            return lat >= bounds.minLat && lat <= bounds.maxLat &&
                                   lng >= bounds.minLng && lng <= bounds.maxLng;
                        }).map(v => ({
                            ...v,
                            lat: parseFloat(v.lat),
                            lng: parseFloat(v.lng),
                            x: 0,
                            y: 0,
                            radius: 8,
                            pulse: 0
                        }));
                            }
                        } catch (e) {
                            // Invalid JSON for villages - continue without villages
                        }
                    }
                }
            } catch (e) {
                // Failed to load villages - continue without villages
            }

            state.filteredVillages = [...state.villages];
            updateRegionFilter();
            updateStatusFilter();
            applyFilters();
            updatePositions();
        } catch (error) {
            useFallbackData();
        }
    }

    function useFallbackData() {
        if (window.quebecMunicipalities) {
            const bounds = CONFIG.quebecBounds;
            state.cities = Object.keys(window.quebecMunicipalities).map(name => {
                const data = window.quebecMunicipalities[name];
                return {
                    id: name,
                    name: name,
                    lat: parseFloat(data.lat),
                    lng: parseFloat(data.lng),
                    region: data.region,
                    population: parseInt(data.population) || 0,
                    is_active: 0,
                    x: 0,
                    y: 0,
                    radius: getCityRadius(parseInt(data.population) || 0)
                };
            }).filter(c => 
                c.lat >= bounds.minLat && c.lat <= bounds.maxLat &&
                c.lng >= bounds.minLng && c.lng <= bounds.maxLng
            );
            state.filteredCities = [...state.cities];
            updateRegionFilter();
            updateStatusFilter();
            applyFilters();
            updatePositions();
        }
    }

    function updateRegionFilter() {
        const regionFilter = document.getElementById('mapRegionFilter');
        if (!regionFilter) return;

        while (regionFilter.children.length > 1) {
            regionFilter.removeChild(regionFilter.lastChild);
        }

        const regions = [...new Set(state.cities.map(c => c.region))].sort();
        regions.forEach(region => {
            const option = document.createElement('option');
            option.value = region;
            option.textContent = region;
            regionFilter.appendChild(option);
        });
    }

    function updateStatusFilter() {
        const statusFilter = document.getElementById('mapStatusFilter');
        if (!statusFilter) return;

        // Status filter options are already in HTML, just ensure it exists
    }

    function getCityRadius(population) {
        if (population > 100000) return 12;
        if (population > 50000) return 8;
        if (population > 10000) return 5;
        return 3;
    }

    function resize() {
        if (!canvas || !container) return;
        const rect = container.getBoundingClientRect();
        const dpr = window.devicePixelRatio || 1;
        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        canvas.style.width = rect.width + 'px';
        canvas.style.height = rect.height + 'px';
        ctx.scale(dpr, dpr);
        
        if (state.panX === 0 && state.panY === 0) {
            state.panX = rect.width / 2;
            state.panY = rect.height / 2;
            state.targetPanX = state.panX;
            state.targetPanY = state.panY;
        }
        
        updatePositions();
    }

    function updatePositions() {
        if (!canvas || canvas.width === 0 || canvas.height === 0) return;

        const bounds = CONFIG.quebecBounds;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);

        state.filteredCities.forEach(city => {
            city.x = ((city.lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * width * state.zoom + (state.panX - (width * state.zoom) / 2);
            city.y = ((bounds.maxLat - city.lat) / (bounds.maxLat - bounds.minLat)) * height * state.zoom + (state.panY - (height * state.zoom) / 2);
        });

        state.filteredVillages.forEach(village => {
            village.x = ((village.lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * width * state.zoom + (state.panX - (width * state.zoom) / 2);
            village.y = ((bounds.maxLat - village.lat) / (bounds.maxLat - bounds.minLat)) * height * state.zoom + (state.panY - (height * state.zoom) / 2);
        });

        // Update clusters if enabled
        if (CONFIG.clustering.enabled && state.zoom <= CONFIG.clustering.maxZoom) {
            updateClusters();
        }
    }

    function updateClusters() {
        state.clusters = [];
        const clusterRadius = CONFIG.clustering.radius;
        const clusterRadiusSq = clusterRadius * clusterRadius;
        const clustered = new Set();

        state.filteredVillages.forEach((village, i) => {
            if (clustered.has(i) || !village.x || !village.y) return;

            const cluster = {
                villages: [village],
                x: village.x,
                y: village.y,
                count: 1
            };

            state.filteredVillages.forEach((other, j) => {
                if (i === j || clustered.has(j) || !other.x || !other.y) return;

                const dx = village.x - other.x;
                const dy = village.y - other.y;
                const distanceSq = dx * dx + dy * dy;

                if (distanceSq < clusterRadiusSq) {
                    cluster.villages.push(other);
                    cluster.x = (cluster.x * cluster.count + other.x) / (cluster.count + 1);
                    cluster.y = (cluster.y * cluster.count + other.y) / (cluster.count + 1);
                    cluster.count++;
                    clustered.add(j);
                }
            });

            if (cluster.count > 1) {
                clustered.add(i);
                state.clusters.push(cluster);
            }
        });
    }

    function zoom(direction) {
        const oldZoom = state.targetZoom;
        state.targetZoom = Math.max(CONFIG.zoom.min, Math.min(CONFIG.zoom.max, 
            state.targetZoom * (direction > 0 ? CONFIG.zoom.step : 1 / CONFIG.zoom.step)));
        
        const zoomFactor = state.targetZoom / oldZoom;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        state.targetPanX = width / 2 - (width / 2 - state.targetPanX) * zoomFactor;
        state.targetPanY = height / 2 - (height / 2 - state.targetPanY) * zoomFactor;
    }

    function resetMapView() {
        state.targetZoom = CONFIG.zoom.default;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        state.targetPanX = width / 2;
        state.targetPanY = height / 2;
    }

    function handleSearch(e) {
        state.searchQuery = e.target.value.toLowerCase();
        applyFilters();
    }

    function handleRegionFilter(e) {
        state.regionFilter = e.target.value;
        applyFilters();
    }

    function handleStatusFilter(e) {
        state.statusFilter = e.target.value;
        applyFilters();
    }

    function applyFilters() {
        state.filteredCities = state.cities.filter(city => {
            const matchesSearch = !state.searchQuery || 
                city.name.toLowerCase().includes(state.searchQuery) ||
                (city.region && city.region.toLowerCase().includes(state.searchQuery));
            const matchesRegion = state.regionFilter === 'all' || city.region === state.regionFilter;
            return matchesSearch && matchesRegion;
        });

        state.filteredVillages = state.villages.filter(village => {
            const matchesSearch = !state.searchQuery || 
                village.name.toLowerCase().includes(state.searchQuery) ||
                (village.name_fr && village.name_fr.toLowerCase().includes(state.searchQuery)) ||
                (village.region && village.region.toLowerCase().includes(state.searchQuery));
            const matchesRegion = state.regionFilter === 'all' || village.region === state.regionFilter;
            const matchesStatus = state.statusFilter === 'all' || village.status === state.statusFilter;
            return matchesSearch && matchesRegion && matchesStatus;
        });

        updatePositions();
    }

    function handleMouseDown(e) {
        const rect = canvas.getBoundingClientRect();
        state.isDragging = true;
        state.dragStart.x = e.clientX - rect.left;
        state.dragStart.y = e.clientY - rect.top;
        canvas.style.cursor = 'grabbing';
    }

    function handleMouseMove(e) {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        if (state.isDragging) {
            state.targetPanX += x - state.dragStart.x;
            state.targetPanY += y - state.dragStart.y;
            state.panX = state.targetPanX;
            state.panY = state.targetPanY;
            state.dragStart.x = x;
            state.dragStart.y = y;
            updatePositions();
        } else {
            const item = getItemAtPosition(x, y);
            
            if (item && item.slug) {
                const isActive = item.status === 'active';
                if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
                    state.hoveredVillage = item;
                    state.hoveredCity = null;
                    const isFr = lang === 'fr';
                    const name = isFr && item.name_fr ? item.name_fr : item.name;
                    showTooltip(x, y, `${name} (${item.member_count || 0} ${isFr ? 'membres' : 'members'})`);
                } else {
                    state.hoveredVillage = null;
                    state.hoveredCity = null;
                    hideTooltip();
                }
            } else if (item && !item.slug) {
                state.hoveredCity = item;
                state.hoveredVillage = null;
                const pop = item.population ? item.population.toLocaleString() : 'N/A';
                showTooltip(x, y, `${item.name} - ${pop} ${lang === 'fr' ? 'habitants' : 'people'}`);
            } else {
                state.hoveredCity = null;
                state.hoveredVillage = null;
                hideTooltip();
            }
        }
    }

    function handleMouseUp() {
        state.isDragging = false;
        canvas.style.cursor = 'grab';
    }

    function handleWheel(e) {
        e.preventDefault();
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        const oldZoom = state.targetZoom;
        const zoomFactor = e.deltaY > 0 ? 1 / CONFIG.zoom.step : CONFIG.zoom.step;
        state.targetZoom = Math.max(CONFIG.zoom.min, Math.min(CONFIG.zoom.max, state.targetZoom * zoomFactor));
        
        const zoomChange = state.targetZoom / oldZoom;
        state.targetPanX = x - (x - state.targetPanX) * zoomChange;
        state.targetPanY = y - (y - state.targetPanY) * zoomChange;
    }

    function handleClick(e) {
        if (state.isDragging) {
            state.isDragging = false;
            return;
        }
        
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        const item = getItemAtPosition(x, y);
        
        if (item && item.slug) {
            const isActive = item.status === 'active';
            if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
                state.selectedVillage = item;
                showVillageDetails(item);
                return;
            }
        }
        
        if (item && !item.slug) {
            state.targetZoom = CONFIG.zoom.showVillagesThreshold;
            state.targetPanX = item.x;
            state.targetPanY = item.y;
        }
    }

    function getItemAtPosition(x, y) {
        // Check clusters first
        if (state.clusters.length > 0) {
            for (let i = state.clusters.length - 1; i >= 0; i--) {
                const cluster = state.clusters[i];
                const dx = x - cluster.x;
                const dy = y - cluster.y;
                const distanceSq = dx * dx + dy * dy;
                const hitRadius = Math.sqrt(cluster.count) * 15;
                if (distanceSq <= hitRadius * hitRadius) {
                    // Return first village in cluster
                    return cluster.villages[0];
                }
            }
        }

        // Check active villages first
        for (let i = state.filteredVillages.length - 1; i >= 0; i--) {
            const village = state.filteredVillages[i];
            if (!village.x || !village.y) continue;
            
            // Skip if in a cluster
            if (state.clusters.some(c => c.villages.includes(village))) continue;
            
            const isActive = village.status === 'active';
            if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
                const dx = x - village.x;
                const dy = y - village.y;
                const distanceSq = dx * dx + dy * dy;
                const hitRadius = isActive ? village.radius + 10 : village.radius + 5;
                if (distanceSq <= hitRadius * hitRadius) return village;
            }
        }
        
        // Check cities
        for (let i = state.filteredCities.length - 1; i >= 0; i--) {
            const city = state.filteredCities[i];
            if (!city.x || !city.y) continue;
            
            const dx = x - city.x;
            const dy = y - city.y;
            const distanceSq = dx * dx + dy * dy;
            const hitRadius = city.radius + 5;
            if (distanceSq <= hitRadius * hitRadius) return city;
        }
        
        return null;
    }

    function showVillageDetails(village) {
        const panel = document.getElementById('villageDetailsPanel');
        const content = document.getElementById('villageDetailsContent');
        if (!panel || !content) return;

        const isFr = lang === 'fr';
        const villageName = isFr && village.name_fr ? village.name_fr : village.name;
        const villageDesc = isFr && village.description_fr ? village.description_fr : village.description;

        content.innerHTML = `
            <h3 style="font-size: ${state.isMobile ? '1.5rem' : '1.8rem'}; font-weight: 700; color: var(--color-accent); margin-bottom: 1.5rem; padding-right: 2rem;">${villageName}</h3>
            <div class="village-info" style="margin-bottom: 1.5rem;">
                <div class="info-item" style="display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--color-border);">
                    <span class="info-label" style="font-weight: 600; color: var(--color-text-secondary);">${isFr ? 'Statut' : 'Status'}:</span>
                    <span class="info-value" style="color: var(--color-text); font-weight: 500;">${village.status === 'active' ? (isFr ? 'Actif' : 'Active') : (isFr ? 'En Formation' : 'Forming')}</span>
                </div>
                <div class="info-item" style="display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--color-border);">
                    <span class="info-label" style="font-weight: 600; color: var(--color-text-secondary);">${isFr ? 'Région' : 'Region'}:</span>
                    <span class="info-value" style="color: var(--color-text); font-weight: 500;">${village.region || 'N/A'}</span>
                </div>
                <div class="info-item" style="display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--color-border);">
                    <span class="info-label" style="font-weight: 600; color: var(--color-text-secondary);">${isFr ? 'Membres' : 'Members'}:</span>
                    <span class="info-value" style="color: var(--color-text); font-weight: 500;">${village.member_count || 0}</span>
                </div>
            </div>
            ${villageDesc ? `<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem; line-height: 1.6;">${villageDesc.substring(0, 200)}${villageDesc.length > 200 ? '...' : ''}</p>` : ''}
            <div class="village-actions" style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 2px solid var(--color-border);">
                <a href="/land/village/${village.slug}" class="action-btn" style="display: block; width: 100%; padding: 1rem; background: var(--color-primary); color: var(--color-text); text-decoration: none; text-align: center; border-radius: 12px; font-weight: 600; margin-bottom: 0.5rem; transition: all 0.3s ease;">
                    ${isFr ? '👁️ Voir le Village' : '👁️ View Village'}
                </a>
                ${!village.is_member ? `<a href="/land/join?slug=${village.slug}" class="action-btn" style="display: block; width: 100%; padding: 1rem; background: var(--color-accent); color: var(--color-text); text-decoration: none; text-align: center; border-radius: 12px; font-weight: 600; transition: all 0.3s ease;">
                    ${isFr ? '➕ Rejoindre' : '➕ Join Village'}
                </a>` : `<span style="display: block; width: 100%; padding: 1rem; background: rgba(16, 185, 129, 0.2); color: #10b981; text-align: center; border-radius: 12px; font-weight: 600; border: 2px solid rgba(16, 185, 129, 0.5);">✓ ${isFr ? 'Membre' : 'Member'}</span>`}
            </div>
        `;
        
        panel.style.display = 'block';
        setTimeout(() => {
            panel.classList.add('active');
        }, 10);
    }

    function closeVillagePanel() {
        const panel = document.getElementById('villageDetailsPanel');
        if (panel) {
            panel.classList.remove('active');
            setTimeout(() => {
                panel.style.display = 'none';
            }, 400);
            state.selectedVillage = null;
        }
    }

    function requestGeolocation() {
        if (!navigator.geolocation) return;
        
        navigator.geolocation.getCurrentPosition(
            (position) => {
                state.userLocation = {
                    lat: position.coords.latitude,
                    lng: position.coords.longitude
                };
                // Check if within Quebec bounds
                const bounds = CONFIG.quebecBounds;
                if (state.userLocation.lat >= bounds.minLat && state.userLocation.lat <= bounds.maxLat &&
                    state.userLocation.lng >= bounds.minLng && state.userLocation.lng <= bounds.maxLng) {
                    state.showUserLocation = true;
                }
            },
            () => {
                // Geolocation denied or failed
            }
        );
    }

    function centerOnUserLocation() {
        if (!state.userLocation) {
            requestGeolocation();
            return;
        }
        
        const bounds = CONFIG.quebecBounds;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        
        state.targetZoom = 2.5;
        state.targetPanX = ((state.userLocation.lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * width * state.targetZoom + (width / 2 - (width * state.targetZoom) / 2);
        state.targetPanY = ((bounds.maxLat - state.userLocation.lat) / (bounds.maxLat - bounds.minLat)) * height * state.targetZoom + (height / 2 - (height * state.targetZoom) / 2);
    }

    function draw() {
        if (!ctx || !canvas) return;

        const now = performance.now();
        if (now - state.lastFrameTime < 1000 / CONFIG.animation.fps) {
            requestAnimationFrame(animate);
            return;
        }
        state.lastFrameTime = now;

        // Smooth zoom/pan interpolation
        const zoomDiff = state.targetZoom - state.zoom;
        const panXDiff = state.targetPanX - state.panX;
        const panYDiff = state.targetPanY - state.panY;

        if (Math.abs(zoomDiff) > CONFIG.animation.threshold) {
            state.zoom += zoomDiff * CONFIG.animation.speed;
        } else {
            state.zoom = state.targetZoom;
        }

        if (Math.abs(panXDiff) > 0.5) {
            state.panX += panXDiff * CONFIG.animation.speed;
        } else {
            state.panX = state.targetPanX;
        }

        if (Math.abs(panYDiff) > 0.5) {
            state.panY += panYDiff * CONFIG.animation.speed;
        } else {
            state.panY = state.targetPanY;
        }

        updatePositions();

        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);

        // Enhanced background
        const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-light').trim() || '#1a1a1a';
        const bgGradient = ctx.createLinearGradient(0, 0, width, height);
        bgGradient.addColorStop(0, bgColor);
        const darkerBg = bgColor.includes('#') ? bgColor.replace(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i, (_, r, g, b) => {
            const darken = (hex) => Math.max(0, parseInt(hex, 16) - 10).toString(16).padStart(2, '0');
            return '#' + darken(r) + darken(g) + darken(b);
        }) : bgColor;
        bgGradient.addColorStop(1, darkerBg);
        ctx.clearRect(0, 0, width, height);
        ctx.fillStyle = bgGradient;
        ctx.fillRect(0, 0, width, height);

        // Grid
        ctx.strokeStyle = `rgba(212, 165, 116, ${0.08 + Math.sin(state.animationFrame * 0.01) * 0.02})`;
        ctx.lineWidth = 1;
        const gridSize = 50;
        for (let x = 0; x < width; x += gridSize) {
            ctx.beginPath();
            ctx.moveTo(x, 0);
            ctx.lineTo(x, height);
            ctx.stroke();
        }
        for (let y = 0; y < height; y += gridSize) {
            ctx.beginPath();
            ctx.moveTo(0, y);
            ctx.lineTo(width, y);
            ctx.stroke();
        }

        // Draw connections
        if (state.zoom >= CONFIG.zoom.showVillagesThreshold && state.filteredVillages.length > 1) {
            drawVillageConnections();
        }

        // Draw cities
        drawCities();

        // Draw villages or clusters
        if (CONFIG.clustering.enabled && state.zoom <= CONFIG.clustering.maxZoom && state.clusters.length > 0) {
            drawClusters();
        } else {
            if (state.zoom >= CONFIG.zoom.showVillagesThreshold) {
                drawVillages();
            } else {
                drawActiveVillagesOnly();
            }
        }

        // Draw user location
        if (state.showUserLocation && state.userLocation) {
            drawUserLocation();
        }

        // Draw legend
        drawLegend();

        state.animationFrame++;
        requestAnimationFrame(animate);
    }

    function drawUserLocation() {
        const bounds = CONFIG.quebecBounds;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        
        const x = ((state.userLocation.lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * width * state.zoom + (state.panX - (width * state.zoom) / 2);
        const y = ((bounds.maxLat - state.userLocation.lat) / (bounds.maxLat - bounds.minLat)) * height * state.zoom + (state.panY - (height * state.zoom) / 2);
        
        // Pulse animation
        const pulse = Math.sin(state.animationFrame * 0.1) * 5;
        
        // Outer ring
        ctx.strokeStyle = 'rgba(66, 153, 225, 0.6)';
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(x, y, 15 + pulse, 0, Math.PI * 2);
        ctx.stroke();
        
        // Inner dot
        ctx.fillStyle = '#4299e1';
        ctx.beginPath();
        ctx.arc(x, y, 8, 0, Math.PI * 2);
        ctx.fill();
        
        // Center
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(x, y, 4, 0, Math.PI * 2);
        ctx.fill();
    }

    function drawClusters() {
        state.clusters.forEach(cluster => {
            const radius = Math.sqrt(cluster.count) * 12;
            const pulse = Math.sin(state.animationFrame * 0.1) * 2;
            const finalRadius = radius + pulse;
            
            // Glow
            const gradient = ctx.createRadialGradient(cluster.x, cluster.y, 0, cluster.x, cluster.y, finalRadius * 2);
            gradient.addColorStop(0, 'rgba(139, 195, 74, 0.6)');
            gradient.addColorStop(1, 'rgba(139, 195, 74, 0)');
            ctx.fillStyle = gradient;
            ctx.beginPath();
            ctx.arc(cluster.x, cluster.y, finalRadius * 2, 0, Math.PI * 2);
            ctx.fill();
            
            // Circle
            ctx.fillStyle = 'rgba(139, 195, 74, 0.8)';
            ctx.beginPath();
            ctx.arc(cluster.x, cluster.y, finalRadius, 0, Math.PI * 2);
            ctx.fill();
            
            // Border
            ctx.strokeStyle = 'rgba(139, 195, 74, 1)';
            ctx.lineWidth = 3;
            ctx.stroke();
            
            // Count label
            const textColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text').trim() || '#f5f5f5';
            ctx.fillStyle = textColor;
            ctx.font = `bold ${Math.max(12, 16 / state.zoom)}px sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(cluster.count.toString(), cluster.x, cluster.y);
        });
    }

    function drawVillageConnections() {
        if (state.filteredVillages.length < 2) return;
        
        ctx.strokeStyle = 'rgba(212, 165, 116, 0.2)';
        ctx.lineWidth = 1;
        ctx.setLineDash([3, 3]);

        const maxDistance = 200;
        const maxDistanceSq = maxDistance * maxDistance;

        for (let i = 0; i < state.filteredVillages.length; i++) {
            const v1 = state.filteredVillages[i];
            if (!v1.x || !v1.y) continue;
            
            for (let j = i + 1; j < state.filteredVillages.length; j++) {
                const v2 = state.filteredVillages[j];
                if (!v2.x || !v2.y) continue;
                
                const dx = v1.x - v2.x;
                const dy = v1.y - v2.y;
                const distanceSq = dx * dx + dy * dy;
                
                if (distanceSq < maxDistanceSq && v1.region === v2.region) {
                    ctx.beginPath();
                    ctx.moveTo(v1.x, v1.y);
                    ctx.lineTo(v2.x, v2.y);
                    ctx.stroke();
                }
            }
        }

        ctx.setLineDash([]);
    }

    function drawCities() {
        const margin = CONFIG.performance.viewportMargin;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        const visibleCities = state.filteredCities.filter(city => 
            city.x >= -margin && city.x <= width + margin &&
            city.y >= -margin && city.y <= height + margin
        ).slice(0, CONFIG.performance.maxRenderItems);

        const citiesWithActiveVillages = new Set(['Sainte-Émélie-de-l\'Énergie']);
        state.filteredVillages.forEach(village => {
            if (village.status === 'active' && village.lat && village.lng) {
                state.filteredCities.forEach(city => {
                    if (city.lat && city.lng) {
                        const latDiff = Math.abs(city.lat - village.lat);
                        const lngDiff = Math.abs(city.lng - village.lng);
                        if (latDiff < 0.1 && lngDiff < 0.1) {
                            citiesWithActiveVillages.add(city.name);
                        }
                    }
                });
            }
        });

        const sortedCities = [...visibleCities].sort((a, b) => {
            const aActive = citiesWithActiveVillages.has(a.name);
            const bActive = citiesWithActiveVillages.has(b.name);
            if (aActive && !bActive) return -1;
            if (!aActive && bActive) return 1;
            return 0;
        });

        sortedCities.forEach(city => {
            const isActive = citiesWithActiveVillages.has(city.name);
            const isHovered = state.hoveredCity === city;
            
            const baseRadius = isActive ? Math.max(city.radius * 2, 15) : city.radius;
            const radius = isHovered ? baseRadius * 1.4 : baseRadius;
            const pulse = isActive ? Math.sin(state.animationFrame * 0.1) * 3 : 0;
            const finalRadius = radius + pulse;

            if (isActive || isHovered) {
                const gradient = ctx.createRadialGradient(city.x, city.y, 0, city.x, city.y, finalRadius * 3);
                gradient.addColorStop(0, isActive ? 'rgba(255, 215, 0, 0.8)' : 'rgba(212, 165, 116, 0.5)');
                gradient.addColorStop(0.5, isActive ? 'rgba(255, 215, 0, 0.4)' : 'rgba(212, 165, 116, 0.2)');
                gradient.addColorStop(1, 'rgba(212, 165, 116, 0)');
                ctx.fillStyle = gradient;
                ctx.beginPath();
                ctx.arc(city.x, city.y, finalRadius * 3, 0, Math.PI * 2);
                ctx.fill();
            }

            const population = city.population || 0;
            let color;
            if (isActive) {
                color = '255, 215, 0';
            } else {
                color = population > 100000 ? '212, 165, 116' : population > 50000 ? '139, 195, 74' : '100, 150, 200';
            }
            
            ctx.shadowColor = `rgba(${color}, ${isActive ? 0.8 : 0.5})`;
            ctx.shadowBlur = isActive ? 25 : (isHovered ? 15 : 8);
            ctx.shadowOffsetX = 2;
            ctx.shadowOffsetY = 2;

            ctx.fillStyle = `rgba(${color}, ${isActive ? 1 : (isHovered ? 0.95 : 0.8)})`;
            ctx.beginPath();
            ctx.arc(city.x, city.y, finalRadius, 0, Math.PI * 2);
            ctx.fill();

            ctx.shadowBlur = 0;
            ctx.strokeStyle = `rgba(${color}, 1)`;
            ctx.lineWidth = isActive ? 4 : (isHovered ? 3 : 2);
            ctx.stroke();

            if (isActive || (state.zoom >= CONFIG.zoom.showLabelsThreshold && (isHovered || population > 50000))) {
                const textColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text').trim() || '#f5f5f5';
                ctx.fillStyle = textColor;
                ctx.font = `${isActive || isHovered ? 'bold ' : ''}${Math.max(11, (isActive ? 14 : 12) / state.zoom)}px sans-serif`;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'top';
                
                const label = isActive ? `⭐ ${city.name}` : city.name;
                ctx.fillText(label, city.x, city.y + finalRadius + 5);
            }
        });
    }

    function drawActiveVillagesOnly() {
        state.filteredVillages.filter(v => v.status === 'active').forEach(village => {
            drawVillageMarker(village, true);
        });
    }

    function drawVillages() {
        const margin = CONFIG.performance.viewportMargin;
        const width = canvas.width / (window.devicePixelRatio || 1);
        const height = canvas.height / (window.devicePixelRatio || 1);
        const visibleVillages = state.filteredVillages.filter(village => 
            village.x >= -margin && village.x <= width + margin &&
            village.y >= -margin && village.y <= height + margin
        ).slice(0, CONFIG.performance.maxRenderItems);

        visibleVillages.forEach(village => {
            drawVillageMarker(village, false);
        });
    }

    function drawVillageMarker(village, alwaysShowLabel) {
        village.pulse = (village.pulse || 0) + 0.05;
        const isHovered = state.hoveredVillage === village;
        const isSelected = state.selectedVillage === village;
        const isActive = village.status === 'active';
        
        const baseRadius = isActive ? 15 : 8;
        const radius = isSelected ? baseRadius + 4 : (isHovered ? baseRadius + 2 : baseRadius);
        const pulseRadius = radius + (isActive ? Math.sin(village.pulse) * 4 : Math.sin(village.pulse) * 2);

        if (isActive || isHovered || isSelected) {
            const gradient = ctx.createRadialGradient(village.x, village.y, 0, village.x, village.y, pulseRadius * 5);
            const glowColor = isActive ? '255, 215, 0' : '139, 195, 74';
            gradient.addColorStop(0, `rgba(${glowColor}, ${isActive ? 0.9 : (isSelected ? 0.7 : 0.5)})`);
            gradient.addColorStop(0.5, `rgba(${glowColor}, ${isActive ? 0.5 : (isSelected ? 0.4 : 0.2)})`);
            gradient.addColorStop(1, `rgba(${glowColor}, 0)`);
            ctx.fillStyle = gradient;
            ctx.beginPath();
            ctx.arc(village.x, village.y, pulseRadius * 5, 0, Math.PI * 2);
            ctx.fill();
        }

        const color = isActive ? '255, 215, 0' : '139, 195, 74';
        ctx.shadowColor = `rgba(${color}, ${isActive ? 0.9 : 0.6})`;
        ctx.shadowBlur = isActive ? 30 : (isSelected ? 20 : 12);
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;

        ctx.fillStyle = `rgba(${color}, ${isSelected ? 1 : isHovered ? 0.95 : (isActive ? 1 : 0.85)})`;
        ctx.beginPath();
        ctx.arc(village.x, village.y, pulseRadius, 0, Math.PI * 2);
        ctx.fill();

        ctx.shadowBlur = 0;
        ctx.strokeStyle = `rgba(${color}, 1)`;
        ctx.lineWidth = isActive ? 5 : (isSelected ? 4 : 2);
        ctx.stroke();

        if (isActive || alwaysShowLabel || isHovered || isSelected) {
            const isFr = lang === 'fr';
            const name = isFr && village.name_fr ? village.name_fr : village.name;
            const textColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text').trim() || '#f5f5f5';
            ctx.fillStyle = textColor;
            ctx.font = `${isActive || isSelected ? 'bold ' : ''}${Math.max(12, (isActive ? 16 : 13) / state.zoom)}px sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            
            const label = isActive ? `⭐ ${name}` : name;
            ctx.fillText(label, village.x, village.y + pulseRadius + 5);
        }
    }

    function drawLegend() {
        const textColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text').trim() || '#f5f5f5';
        const x = 15;
        let y = 15;
        const hasActiveVillages = state.filteredVillages.some(v => v.status === 'active');
        const width = canvas.width / (window.devicePixelRatio || 1);

        ctx.fillStyle = `rgba(0, 0, 0, 0.7)`;
        const legendHeight = hasActiveVillages ? 160 : 100;
        const legendWidth = state.isMobile ? Math.min(180, width - 30) : 220;
        ctx.fillRect(x - 5, y - 5, legendWidth, legendHeight);

        ctx.fillStyle = textColor;
        ctx.font = 'bold 13px sans-serif';
        ctx.textAlign = 'left';
        ctx.fillText(lang === 'fr' ? '⭐ ACTIF' : '⭐ ACTIVE', x, y);
        y += 20;

        ctx.font = '11px sans-serif';
        
        if (hasActiveVillages) {
            ctx.fillStyle = 'rgba(255, 215, 0, 1)';
            ctx.beginPath();
            ctx.arc(x + 5, y, 6, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = 'rgba(255, 215, 0, 1)';
            ctx.lineWidth = 2;
            ctx.stroke();
            ctx.fillStyle = textColor;
            ctx.font = 'bold 11px sans-serif';
            ctx.fillText(lang === 'fr' ? '⭐ Ville/Village actif' : '⭐ Active city/village', x + 15, y + 4);
            y += 22;
        }

        ctx.font = '10px sans-serif';
        
        ctx.fillStyle = 'rgba(212, 165, 116, 0.8)';
        ctx.beginPath();
        ctx.arc(x + 5, y, 4, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = textColor;
        ctx.fillText(lang === 'fr' ? 'Grande ville (>100k)' : 'Major city (>100k)', x + 15, y + 4);
        y += 18;

        ctx.fillStyle = 'rgba(139, 195, 74, 0.8)';
        ctx.beginPath();
        ctx.arc(x + 5, y, 3, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = textColor;
        ctx.fillText(lang === 'fr' ? 'Ville moyenne (>50k)' : 'Medium city (>50k)', x + 15, y + 4);
        y += 18;

        ctx.fillStyle = 'rgba(100, 150, 200, 0.8)';
        ctx.beginPath();
        ctx.arc(x + 5, y, 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = textColor;
        ctx.fillText(lang === 'fr' ? 'Petite ville' : 'Small city', x + 15, y + 4);
        y += 18;

        if (state.zoom >= CONFIG.zoom.showVillagesThreshold || hasActiveVillages) {
            ctx.fillStyle = 'rgba(139, 195, 74, 0.9)';
            ctx.beginPath();
            ctx.arc(x + 5, y, 4, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = textColor;
            ctx.fillText(lang === 'fr' ? 'Village en formation' : 'Forming village', x + 15, y + 4);
        }

        if (state.showUserLocation) {
            y += 18;
            ctx.fillStyle = '#4299e1';
            ctx.beginPath();
            ctx.arc(x + 5, y, 4, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = textColor;
            ctx.fillText(lang === 'fr' ? 'Votre position' : 'Your location', x + 15, y + 4);
        }
    }

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function animate() {
        draw();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 100);
    }
})();

CasperSecurity Mini