![]() 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/ |
/**
* Homepage Interactive Map - UPGRADED VERSION
* Enhanced with smooth animations, tooltips, labels, and better visuals
*/
(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
},
animation: {
speed: 0.15,
threshold: 0.01
}
};
let state = {
cities: [],
villages: [],
filteredCities: [],
filteredVillages: [],
selectedVillage: null,
hoveredCity: null,
hoveredVillage: null,
zoom: CONFIG.zoom.default,
targetZoom: CONFIG.zoom.default,
panX: 0,
panY: 0,
targetPanX: 0,
targetPanY: 0,
isDragging: false,
dragStart: { x: 0, y: 0 },
searchQuery: '',
regionFilter: 'all',
animationFrame: 0
};
let canvas, ctx, container, tooltip;
const lang = document.documentElement.lang || 'en';
function init() {
container = document.getElementById('homepageInteractiveMap');
if (!container) return;
canvas = document.getElementById('homepageMapCanvas');
if (!canvas) return;
ctx = canvas.getContext('2d');
if (!ctx) return;
createTooltip();
setupUI();
setupEvents();
resize();
loadData();
animate();
}
function createTooltip() {
tooltip = document.createElement('div');
tooltip.id = 'mapTooltip';
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: 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: 250px;
`;
container.appendChild(tooltip);
}
function showTooltip(x, y, text) {
if (!tooltip || !container) return;
tooltip.textContent = text;
// Position tooltip, keeping it within container bounds
const rect = container.getBoundingClientRect();
const tooltipX = Math.min(x + 15, rect.width - 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 searchInput = document.getElementById('mapSearch');
const regionFilter = document.getElementById('mapRegionFilter');
const closePanel = document.getElementById('closeVillagePanel');
if (zoomIn) zoomIn.addEventListener('click', () => zoom(1));
if (zoomOut) zoomOut.addEventListener('click', () => zoom(-1));
if (resetView) resetView.addEventListener('click', resetMapView);
if (searchInput) {
searchInput.addEventListener('input', handleSearch);
searchInput.addEventListener('focus', () => searchInput.style.borderColor = 'var(--color-accent)');
searchInput.addEventListener('blur', () => searchInput.style.borderColor = 'var(--color-border)');
}
if (regionFilter) regionFilter.addEventListener('change', handleRegionFilter);
if (closePanel) closePanel.addEventListener('click', closeVillagePanel);
}
function setupEvents() {
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', () => {
state.isDragging = false;
hideTooltip();
});
canvas.addEventListener('wheel', handleWheel);
canvas.addEventListener('click', handleClick);
window.addEventListener('resize', resize);
}
async function loadData() {
try {
const citiesResponse = await fetch('/api/endpoints/cities.php');
const text = await citiesResponse.text();
if (!text || text.trim() === '') {
useFallbackData();
return;
}
let citiesData;
try {
citiesData = JSON.parse(text);
} catch (e) {
useFallbackData();
return;
}
if (citiesData.success && 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];
const villagesResponse = await fetch('/api/endpoints/map-villages.php');
if (villagesResponse.ok) {
const text = await villagesResponse.text();
if (text && text.trim()) {
const villagesData = JSON.parse(text);
if (villagesData.success && 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
}));
}
}
}
state.filteredVillages = [...state.villages];
updateRegionFilter();
updatePositions();
} catch (error) {
// Silently fall back to JavaScript data
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();
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 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();
canvas.width = rect.width;
canvas.height = rect.height;
if (state.panX === 0 && state.panY === 0) {
state.panX = canvas.width / 2;
state.panY = canvas.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;
const height = canvas.height;
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);
});
}
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;
state.targetPanX = canvas.width / 2 - (canvas.width / 2 - state.targetPanX) * zoomFactor;
state.targetPanY = canvas.height / 2 - (canvas.height / 2 - state.targetPanY) * zoomFactor;
}
function resetMapView() {
state.targetZoom = CONFIG.zoom.default;
state.targetPanX = canvas.width / 2;
state.targetPanY = canvas.height / 2;
}
function handleSearch(e) {
state.searchQuery = e.target.value.toLowerCase();
applyFilters();
}
function handleRegionFilter(e) {
state.regionFilter = 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;
});
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);
// Check if it's a village (active villages work at any zoom)
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) {
// It's a city
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);
// Click on village (active villages work at any zoom, others need zoom)
if (item && item.slug) {
const isActive = item.status === 'active';
if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
state.selectedVillage = item;
showVillageDetails(item);
return;
}
}
// Click on city - zoom in to show villages
if (item && !item.slug) {
// Zoom in and center on city
state.targetZoom = CONFIG.zoom.showVillagesThreshold;
state.targetPanX = item.x;
state.targetPanY = item.y;
}
}
function getItemAtPosition(x, y) {
// Check active villages first (always visible)
for (let i = state.filteredVillages.length - 1; i >= 0; i--) {
const village = state.filteredVillages[i];
if (!village.x || !village.y) continue;
const isActive = village.status === 'active';
// Active villages always clickable, others only when zoomed in
if (isActive || state.zoom >= CONFIG.zoom.showVillagesThreshold) {
const dx = x - village.x;
const dy = y - village.y;
const distanceSq = dx * dx + dy * dy;
// Active villages have bigger radius, so use bigger hitbox
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: 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.style.right = '2rem';
}, 10);
}
function closeVillagePanel() {
const panel = document.getElementById('villageDetailsPanel');
if (panel) {
panel.style.right = '-400px';
setTimeout(() => {
panel.style.display = 'none';
}, 400);
state.selectedVillage = null;
}
}
function draw() {
if (!ctx || !canvas) return;
// 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();
// Enhanced background with gradient
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-light').trim() || '#1a1a1a';
const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
bgGradient.addColorStop(0, bgColor);
// Create slightly darker variant for gradient end
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, canvas.width, canvas.height);
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Enhanced grid with subtle animation
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 < canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// Draw connection lines between nearby villages
if (state.zoom >= CONFIG.zoom.showVillagesThreshold && state.filteredVillages.length > 1) {
drawVillageConnections();
}
// Draw cities
drawCities();
// ALWAYS draw active villages, even when zoomed out
// Draw all villages if zoomed in enough
if (state.zoom >= CONFIG.zoom.showVillagesThreshold) {
drawVillages();
} else {
// Still show active villages when zoomed out
drawActiveVillagesOnly();
}
// Draw legend
drawLegend();
state.animationFrame++;
}
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; // Avoid sqrt in loop
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;
// Quick distance check (squared to avoid sqrt)
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 = 100;
const visibleCities = state.filteredCities.filter(city =>
city.x >= -margin && city.x <= canvas.width + margin &&
city.y >= -margin && city.y <= canvas.height + margin
);
// Check which cities have active villages (cache this calculation)
const citiesWithActiveVillages = new Set(['Sainte-Émélie-de-l\'Énergie']); // Always mark as active
state.filteredVillages.forEach(village => {
if (village.status === 'active') {
// Find nearby city (optimized: only check if village has valid coords)
if (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);
// Quick check: within ~10km (0.1 degrees ≈ 11km)
if (latDiff < 0.1 && lngDiff < 0.1) {
citiesWithActiveVillages.add(city.name);
}
}
});
}
}
});
// Sort: active cities first
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;
// Active cities are MUCH bigger and brighter
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;
// Enhanced glow for active cities
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();
}
// Active cities get special color - bright gold
const population = city.population || 0;
let color;
if (isActive) {
color = '255, 215, 0'; // Bright gold for active
} else {
color = population > 100000 ? '212, 165, 116' : population > 50000 ? '139, 195, 74' : '100, 150, 200';
}
// Shadow - stronger for active
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();
// Always show label for active cities, or when zoomed in
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';
// Add star for active cities
const label = isActive ? `⭐ ${city.name}` : city.name;
ctx.fillText(label, city.x, city.y + finalRadius + 5);
}
});
}
function drawActiveVillagesOnly() {
// Draw only active villages when zoomed out
state.filteredVillages.filter(v => v.status === 'active').forEach(village => {
drawVillageMarker(village, true);
});
}
function drawVillages() {
state.filteredVillages.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';
// Active villages are MUCH bigger
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);
// Enhanced glow with pulse animation - stronger for active
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'; // Bright gold for active
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();
}
// Village marker with shadow - stronger for active
const color = isActive ? '255, 215, 0' : '139, 195, 74'; // Bright gold for active
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();
// Always show label for active villages, or when hovered/selected
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';
// Add star for active villages
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');
ctx.fillStyle = `rgba(0, 0, 0, 0.7)`;
const legendHeight = hasActiveVillages ? 160 : 100;
ctx.fillRect(x - 5, y - 5, 220, 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';
// Active cities/villages FIRST
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';
// Regular cities
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;
// Villages
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);
}
}
function animate() {
draw();
requestAnimationFrame(animate);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 100);
}
})();