![]() 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 Map - EXACT COPY of working advanced map logic
*/
(function() {
'use strict';
const CONFIG = {
quebecBounds: {
minLat: 45.0,
maxLat: 51.0,
minLng: -80.0,
maxLng: -66.0
},
zoom: {
min: 0.5,
max: 8,
default: 1.2,
step: 1.3
},
cityRadius: {
major: 12,
medium: 8,
small: 5,
tiny: 3
}
};
let state = {
cities: [],
villages: [],
selectedCity: null,
selectedVillage: null,
hoveredCity: null,
hoveredVillage: null,
zoom: CONFIG.zoom.default,
panX: 0,
panY: 0,
isDragging: false,
dragStart: { x: 0, y: 0 },
hasDragged: false
};
let canvas, ctx, container;
let villagesData = [];
let quebecMunicipalities = {};
function init() {
console.log('π Homepage Map: Starting init...');
container = document.getElementById('homepageInteractiveMap');
if (!container) {
console.error('π Homepage Map: Container not found');
return;
}
canvas = document.getElementById('homepageMapCanvas');
if (!canvas) {
console.error('π Homepage Map: Canvas not found');
return;
}
ctx = canvas.getContext('2d');
if (!ctx) {
console.error('π Homepage Map: Could not get context');
return;
}
console.log('π Homepage Map: Elements found, setting up...');
resize();
setupUI();
setupEvents();
loadData();
animate();
}
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;
}
updateCityPositions();
updateVillagePositions();
console.log('π Homepage Map: Canvas sized', canvas.width, 'x', canvas.height);
}
function setupUI() {
const zoomIn = document.getElementById('mapZoomIn');
const zoomOut = document.getElementById('mapZoomOut');
const reset = document.getElementById('resetMapView');
if (zoomIn) zoomIn.addEventListener('click', () => zoom(1));
if (zoomOut) zoomOut.addEventListener('click', () => zoom(-1));
if (reset) reset.addEventListener('click', resetMapView);
}
function setupEvents() {
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('click', handleClick);
canvas.addEventListener('wheel', handleWheel);
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);
window.addEventListener('resize', resize);
// Close panel button
const closeBtn = document.getElementById('closeVillagePanel');
if (closeBtn) {
closeBtn.addEventListener('click', closeDetailsPanel);
}
}
async function loadData() {
try {
console.log('π Homepage Map: Loading data from API...');
const res = await fetch('/api/map');
const data = await res.json();
villagesData = data.villages || data.cities || [];
console.log('π Homepage Map: Loaded', villagesData.length, 'villages from database');
quebecMunicipalities = window.quebecMunicipalities || {};
if (Object.keys(quebecMunicipalities).length === 0) {
console.log('π Homepage Map: Waiting for quebecMunicipalities...');
let attempts = 0;
const waitForData = setInterval(() => {
attempts++;
if (window.quebecMunicipalities && Object.keys(window.quebecMunicipalities).length > 0) {
clearInterval(waitForData);
quebecMunicipalities = window.quebecMunicipalities;
console.log('π Homepage Map: Found quebecMunicipalities after', attempts, 'attempts');
processCities();
} else if (attempts > 100) {
clearInterval(waitForData);
console.error('π Homepage Map: quebecMunicipalities not found');
}
}, 100);
} else {
console.log('π Homepage Map: quebecMunicipalities already loaded');
processCities();
}
} catch (err) {
console.error('π Homepage Map: Failed to load data', err);
}
}
function processCities() {
const bounds = CONFIG.quebecBounds;
console.log('π Homepage Map: Processing cities from', Object.keys(quebecMunicipalities).length, 'municipalities');
state.cities = Object.keys(quebecMunicipalities).map(cityName => {
const data = quebecMunicipalities[cityName];
return {
name: cityName,
lat: data.lat,
lng: data.lng,
region: data.region,
population: data.population || 0,
member_count: 0,
post_count: 0,
event_count: 0,
villages: [],
x: 0,
y: 0,
radius: getCityRadius(data.population || 0)
};
}).filter(city =>
city.lat >= bounds.minLat && city.lat <= bounds.maxLat &&
city.lng >= bounds.minLng && city.lng <= bounds.maxLng
);
console.log('π Homepage Map: Filtered to', state.cities.length, 'cities in bounds');
console.log('π Homepage Map: Sample cities:', state.cities.slice(0, 3).map(c => c.name));
// Process villages
state.villages = villagesData.map(village => {
const vLat = parseFloat(village.location_lat);
const vLng = parseFloat(village.location_lng);
if (isNaN(vLat) || isNaN(vLng)) return null;
return {
id: village.id,
name: village.name,
name_fr: village.name_fr,
slug: village.slug,
lat: vLat,
lng: vLng,
region: village.region,
member_count: village.member_count || 0,
post_count: village.post_count || 0,
event_count: village.event_count || 0,
status: village.status,
x: 0,
y: 0,
radius: Math.max(8, Math.min(20, 8 + (village.member_count || 0) * 0.5))
};
}).filter(v => v !== null &&
v.lat >= bounds.minLat && v.lat <= bounds.maxLat &&
v.lng >= bounds.minLng && v.lng <= bounds.maxLng
);
console.log('π Homepage Map: Processed', state.villages.length, 'villages');
// Merge village data into cities
villagesData.forEach(village => {
if (!village.location_lat || !village.location_lng) return;
const vLat = parseFloat(village.location_lat);
const vLng = parseFloat(village.location_lng);
if (isNaN(vLat) || isNaN(vLng)) return;
let closest = null;
let minDist = Infinity;
state.cities.forEach(city => {
const dist = Math.sqrt(Math.pow(city.lat - vLat, 2) + Math.pow(city.lng - vLng, 2));
if (dist < minDist && dist < 0.1) {
minDist = dist;
closest = city;
}
});
if (closest) {
closest.member_count += village.member_count || 0;
closest.post_count += village.post_count || 0;
closest.event_count += village.event_count || 0;
closest.villages.push(village);
}
});
updateCityPositions();
updateVillagePositions();
}
function updateCityPositions() {
const bounds = CONFIG.quebecBounds;
const width = canvas.width;
const height = canvas.height;
state.cities.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);
});
}
function updateVillagePositions() {
const bounds = CONFIG.quebecBounds;
const width = canvas.width;
const height = canvas.height;
state.villages.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 getCityRadius(population) {
if (population > 100000) return CONFIG.cityRadius.major;
if (population > 50000) return CONFIG.cityRadius.medium;
if (population > 10000) return CONFIG.cityRadius.small;
return CONFIG.cityRadius.tiny;
}
function zoom(direction) {
const oldZoom = state.zoom;
state.zoom = Math.max(CONFIG.zoom.min, Math.min(CONFIG.zoom.max,
state.zoom * (direction > 0 ? CONFIG.zoom.step : 1 / CONFIG.zoom.step)));
const zoomFactor = state.zoom / oldZoom;
state.panX = canvas.width / 2 - (canvas.width / 2 - state.panX) * zoomFactor;
state.panY = canvas.height / 2 - (canvas.height / 2 - state.panY) * zoomFactor;
updateCityPositions();
updateVillagePositions();
}
function resetMapView() {
state.zoom = CONFIG.zoom.default;
state.panX = canvas.width / 2;
state.panY = canvas.height / 2;
updateCityPositions();
updateVillagePositions();
}
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
state.isDragging = true;
state.hasDragged = false;
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) {
const dx = x - state.dragStart.x;
const dy = y - state.dragStart.y;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
state.hasDragged = true;
}
state.panX += dx;
state.panY += dy;
state.dragStart.x = x;
state.dragStart.y = y;
updateCityPositions();
updateVillagePositions();
} else {
// Check hover
checkHover(x, y);
}
}
function handleMouseUp() {
state.isDragging = false;
canvas.style.cursor = state.hoveredCity || state.hoveredVillage ? 'pointer' : 'grab';
}
function handleClick(e) {
if (state.hasDragged) {
state.hasDragged = false;
return;
}
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check villages first (on top)
const village = getVillageAtPosition(x, y);
if (village) {
state.selectedVillage = village;
state.selectedCity = null;
showVillageDetails(village);
return;
}
// Check cities
const city = getCityAtPosition(x, y);
if (city) {
state.selectedCity = city;
state.selectedVillage = null;
showCityDetails(city);
}
}
function checkHover(x, y) {
const village = getVillageAtPosition(x, y);
if (village !== state.hoveredVillage) {
state.hoveredVillage = village;
}
if (!village) {
const city = getCityAtPosition(x, y);
if (city !== state.hoveredCity) {
state.hoveredCity = city;
}
} else {
state.hoveredCity = null;
}
canvas.style.cursor = (village || city) ? 'pointer' : (state.isDragging ? 'grabbing' : 'grab');
}
function getCityAtPosition(x, y) {
for (let i = state.cities.length - 1; i >= 0; i--) {
const city = state.cities[i];
const distance = Math.sqrt(Math.pow(x - city.x, 2) + Math.pow(y - city.y, 2));
if (distance <= city.radius + 5) {
return city;
}
}
return null;
}
function getVillageAtPosition(x, y) {
for (let i = state.villages.length - 1; i >= 0; i--) {
const village = state.villages[i];
const distance = Math.sqrt(Math.pow(x - village.x, 2) + Math.pow(y - village.y, 2));
if (distance <= village.radius + 5) {
return village;
}
}
return null;
}
function showCityDetails(city) {
const panel = document.getElementById('villageDetailsPanel');
const content = document.getElementById('villageDetailsContent');
if (!panel || !content) return;
const lang = document.documentElement.lang || 'en';
const isFr = lang === 'fr';
const hasMembers = (city.member_count || 0) > 0;
content.innerHTML = `
<h3>${city.name}</h3>
<div style="margin: 1rem 0; color: var(--color-text-secondary);">
<div>π ${city.region || 'Quebec'}</div>
<div>π₯ ${city.population.toLocaleString()} ${isFr ? 'habitants' : 'residents'}</div>
${hasMembers ? `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
<div>π₯ <strong>${city.member_count}</strong> ${isFr ? 'membres du rΓ©seau' : 'network members'}</div>
<div>π¬ ${city.post_count || 0} posts</div>
<div>π
${city.event_count || 0} ${isFr ? 'Γ©vΓ©nements' : 'events'}</div>
</div>
` : `
<div style="margin-top: 1rem; color: var(--color-text-secondary);">${isFr ? 'Aucun membre dans cette ville' : 'No members in this city yet'}</div>
`}
${city.villages && city.villages.length > 0 ? `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
<strong>${isFr ? 'Villages:' : 'Villages:'}</strong>
${city.villages.map(v => `
<div style="margin-top: 0.5rem;">
<a href="/land/village/${v.slug}" style="color: var(--color-accent); text-decoration: none;">
${isFr && v.name_fr ? v.name_fr : v.name}
</a>
</div>
`).join('')}
</div>
` : ''}
</div>
<div style="margin-top: 1.5rem;">
<a href="/city?city=${encodeURIComponent(city.name)}" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-primary); color: white; text-decoration: none; border-radius: 8px; font-weight: 600;">
${isFr ? 'Voir la page de la ville' : 'View City Page'} β
</a>
</div>
`;
panel.style.display = 'block';
}
function showVillageDetails(village) {
const panel = document.getElementById('villageDetailsPanel');
const content = document.getElementById('villageDetailsContent');
if (!panel || !content) return;
const lang = document.documentElement.lang || 'en';
const isFr = lang === 'fr';
const name = (isFr && village.name_fr) ? village.name_fr : village.name;
content.innerHTML = `
<h3>${name}</h3>
<div style="margin: 1rem 0; color: var(--color-text-secondary);">
<div>π ${village.region || 'Quebec'}</div>
<div>π₯ ${village.member_count || 0} ${isFr ? 'membres' : 'members'}</div>
<div>π¬ ${village.post_count || 0} posts</div>
<div>π
${village.event_count || 0} ${isFr ? 'Γ©vΓ©nements' : 'events'}</div>
<div>${isFr ? 'Statut' : 'Status'}: <strong>${village.status || 'forming'}</strong></div>
</div>
<div style="margin-top: 1.5rem;">
<a href="/land/village/${village.slug}" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-primary); color: white; text-decoration: none; border-radius: 8px; font-weight: 600;">
${isFr ? 'Voir le Village' : 'View Village'} β
</a>
</div>
`;
panel.style.display = 'block';
}
function closeDetailsPanel() {
const panel = document.getElementById('villageDetailsPanel');
if (panel) {
panel.style.display = 'none';
state.selectedCity = null;
state.selectedVillage = null;
}
}
function handleWheel(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const oldZoom = state.zoom;
const zoomFactor = e.deltaY > 0 ? 1 / CONFIG.zoom.step : CONFIG.zoom.step;
state.zoom = Math.max(CONFIG.zoom.min, Math.min(CONFIG.zoom.max, state.zoom * zoomFactor));
const zoomChange = state.zoom / oldZoom;
state.panX = x - (x - state.panX) * zoomChange;
state.panY = y - (y - state.panY) * zoomChange;
updateCityPositions();
updateVillagePositions();
}
function handleTouchStart(e) {
if (e.touches.length === 1) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
state.isDragging = true;
state.dragStart.x = touch.clientX - rect.left;
state.dragStart.y = touch.clientY - rect.top;
}
}
function handleTouchMove(e) {
if (e.touches.length === 1 && state.isDragging) {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
state.panX += x - state.dragStart.x;
state.panY += y - state.dragStart.y;
state.dragStart.x = x;
state.dragStart.y = y;
updateCityPositions();
updateVillagePositions();
}
}
function handleTouchEnd() {
state.isDragging = false;
}
function draw() {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Background
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-light').trim() || '#1a1a1a';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Grid
ctx.strokeStyle = 'rgba(212, 165, 116, 0.2)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
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();
}
ctx.setLineDash([]);
// Draw cities
state.cities.forEach(city => {
const isHovered = state.hoveredCity === city;
const isSelected = state.selectedCity === city;
const isMajor = city.population > 100000;
const isMedium = city.population > 50000;
const hasMembers = (city.member_count || 0) > 0;
// Glow effect
if (hasMembers || isHovered || isSelected) {
const gradient = ctx.createRadialGradient(
city.x, city.y, 0,
city.x, city.y, city.radius * 3
);
gradient.addColorStop(0, 'rgba(212, 165, 116, 0.5)');
gradient.addColorStop(1, 'rgba(212, 165, 116, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(city.x, city.y, city.radius * 3, 0, Math.PI * 2);
ctx.fill();
}
// City marker
const color = hasMembers ? '212, 165, 116' : (isMajor ? '212, 165, 116' : isMedium ? '139, 195, 74' : '100, 150, 200');
const opacity = isSelected ? 1 : isHovered ? 0.9 : (hasMembers ? 0.9 : 0.7);
const radius = isSelected ? city.radius * 1.3 : isHovered ? city.radius * 1.1 : city.radius;
ctx.fillStyle = `rgba(${color}, ${opacity})`;
ctx.beginPath();
ctx.arc(city.x, city.y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(${color}, ${opacity + 0.2})`;
ctx.lineWidth = (hasMembers || isSelected) ? 3 : 2;
ctx.stroke();
// Member indicator
if (hasMembers) {
ctx.fillStyle = '#10b981';
ctx.beginPath();
ctx.arc(city.x + radius * 0.6, city.y - radius * 0.6, radius * 0.3, 0, Math.PI * 2);
ctx.fill();
}
// Label
if ((isMajor || hasMembers || state.zoom > 1.8) && (isHovered || isSelected || hasMembers || state.zoom > 1.5)) {
const labelText = city.name + (hasMembers ? ` (${city.member_count})` : '');
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.font = `${isSelected ? 'bold ' : ''}12px sans-serif`;
const metrics = ctx.measureText(labelText);
const pad = 6;
ctx.fillRect(city.x - metrics.width / 2 - pad, city.y + radius + 2, metrics.width + pad * 2, 18);
ctx.fillStyle = `rgba(${color}, 1)`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(labelText, city.x, city.y + radius + 5);
}
});
// Draw villages
state.villages.forEach(village => {
const hasMembers = (village.member_count || 0) > 0;
const color = '212, 165, 116';
const opacity = hasMembers ? 0.9 : 0.7;
// Glow
if (hasMembers) {
const gradient = ctx.createRadialGradient(
village.x, village.y, 0,
village.x, village.y, village.radius * 4
);
gradient.addColorStop(0, 'rgba(212, 165, 116, 0.8)');
gradient.addColorStop(1, 'rgba(212, 165, 116, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(village.x, village.y, village.radius * 4, 0, Math.PI * 2);
ctx.fill();
}
// Outer ring
ctx.strokeStyle = `rgba(${color}, ${opacity + 0.3})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(village.x, village.y, village.radius + 2, 0, Math.PI * 2);
ctx.stroke();
// Village marker
ctx.fillStyle = `rgba(${color}, ${opacity})`;
ctx.beginPath();
ctx.arc(village.x, village.y, village.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(${color}, ${opacity + 0.2})`;
ctx.lineWidth = 2;
ctx.stroke();
// Label
if (state.zoom > 1.5 || hasMembers) {
const name = (document.documentElement.lang === 'fr' && village.name_fr) ? village.name_fr : village.name;
const labelText = name + (hasMembers ? ` (${village.member_count})` : '');
ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
ctx.font = '13px sans-serif';
const metrics = ctx.measureText(labelText);
const pad = 8;
ctx.fillRect(village.x - metrics.width / 2 - pad, village.y + village.radius + 3, metrics.width + pad * 2, 20);
ctx.fillStyle = `rgba(${color}, 1)`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(labelText, village.x, village.y + village.radius + 5);
}
});
}
function animate() {
draw();
requestAnimationFrame(animate);
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 200);
}
})();