/** * postPriority.js * * Système de priorité et clustering pour afficher intelligemment les posts: * - Posts importants => cartes complètes (mini) * - Posts secondaires => marqueurs simples (icône + hover) */ /** * Calcule le score de priorité d'un post * Score plus élevé = plus important */ export function calculatePostPriority(post, viewCenter, allPosts) { let score = 0; // 1. Score intrinsèque du post (si disponible depuis reco-service) const recoScore = typeof post.score === 'number' ? post.score : 40; // Réduit de 50 à 40 pour base plus basse score += recoScore * 0.4; // Weight: 40% // 2. Catégorie (certaines plus importantes) const category = (post?.category || '').toString().toUpperCase(); const categoryBonus = { 'EVENT': 20, 'EVENTS': 20, 'NEWS': 15, 'MARKET': 10, 'FRIENDS': 5, }; score += categoryBonus[category] || 10; // Weight: 10-20 // 3. Fraîcheur (posts récents prioritaires) const createdAt = post?.created_at || post?.CreatedAt || post?.createdAt; if (createdAt) { const ageHours = (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60); if (ageHours < 1) score += 25; else if (ageHours < 6) score += 20; else if (ageHours < 24) score += 15; else if (ageHours < 72) score += 10; else score += 5; } // 4. Distance par rapport au centre de la vue if (viewCenter) { const lat = typeof post.lat === 'number' ? post.lat : post.latitude; const lon = typeof post.lon === 'number' ? post.lon : post.lng; if (lat != null && lon != null) { const dx = viewCenter.lng - lon; const dy = viewCenter.lat - lat; const dist = Math.sqrt(dx * dx + dy * dy); // Plus proche du centre = meilleur score const maxDist = 0.1; // ~10km const distScore = Math.max(0, 20 * (1 - dist / maxDist)); score += distScore; // Weight: 0-20 } } // 5. Engagement (si disponible) const views = post?.views || post?.view_count || 0; const likes = post?.likes || post?.like_count || 0; const comments = post?.comments || post?.comment_count || 0; const engagementScore = Math.min(20, (views / 100) + (likes * 2) + (comments * 5)); score += engagementScore; // Weight: 0-20 return Math.round(score); } /** * Détecte les clusters de posts proches * Retourne Map */ export function clusterPosts(posts, clusterRadius = 0.001) { const clusters = new Map(); const processed = new Set(); let clusterId = 0; for (const post of posts) { if (processed.has(post.id)) continue; const lat = typeof post.lat === 'number' ? post.lat : post.latitude; const lon = typeof post.lon === 'number' ? post.lon : post.lng; if (lat == null || lon == null) continue; // Trouve tous les posts dans le rayon const cluster = [post]; processed.add(post.id); for (const other of posts) { if (processed.has(other.id)) continue; const oLat = typeof other.lat === 'number' ? other.lat : other.latitude; const oLon = typeof other.lon === 'number' ? other.lon : other.lng; if (oLat == null || oLon == null) continue; const dx = lat - oLat; const dy = lon - oLon; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < clusterRadius) { cluster.push(other); processed.add(other.id); } } clusters.set(clusterId++, cluster); } return clusters; } /** * Décide quels posts afficher comme cartes complètes vs marqueurs simples * Retourne { fullCardPosts: [], simpleMarkerPosts: [] } */ export function prioritizePosts(posts, viewCenter, options = {}) { const { maxFullCards = 15, // Max de cartes complètes à afficher clusterRadius = 0.001, // Rayon de clustering (~100m) minScoreForCard = 40, // Score minimum pour carte complète } = options; // 1. Calculer les scores const postsWithScores = posts.map(post => ({ post, priority: calculatePostPriority(post, viewCenter, posts), })); // 2. Trier par priorité descendante postsWithScores.sort((a, b) => b.priority - a.priority); // 3. Détecter les clusters const clusters = clusterPosts(posts, clusterRadius); // 4. Sélectionner les posts pour cartes complètes const fullCardPosts = []; const simpleMarkerPosts = []; const clusterRepresentatives = new Set(); for (const [clusterId, clusterPosts] of clusters.entries()) { if (clusterPosts.length === 1) { // Pas de cluster, décision simple basée sur le score const item = postsWithScores.find(p => p.post.id === clusterPosts[0].id); if (item && item.priority >= minScoreForCard && fullCardPosts.length < maxFullCards) { fullCardPosts.push(item.post); } else { simpleMarkerPosts.push(clusterPosts[0]); } } else { // Cluster: prendre le meilleur post pour carte complète, autres en marqueurs simples const sortedCluster = clusterPosts .map(p => postsWithScores.find(ps => ps.post.id === p.id)) .filter(Boolean) .sort((a, b) => b.priority - a.priority); // Le meilleur du cluster devient carte si score suffisant if (sortedCluster[0] && sortedCluster[0].priority >= minScoreForCard && fullCardPosts.length < maxFullCards) { fullCardPosts.push(sortedCluster[0].post); clusterRepresentatives.add(sortedCluster[0].post.id); } // Les autres deviennent marqueurs simples for (let i = (clusterRepresentatives.has(sortedCluster[0]?.post.id) ? 1 : 0); i < sortedCluster.length; i++) { simpleMarkerPosts.push(sortedCluster[i].post); } } } // 5. Si pas assez de cartes complètes, ajouter les meilleurs marqueurs simples if (fullCardPosts.length < maxFullCards) { const remaining = postsWithScores .filter(p => !fullCardPosts.find(fp => fp.id === p.post.id) && !simpleMarkerPosts.find(sp => sp.id === p.post.id) ) .slice(0, maxFullCards - fullCardPosts.length); for (const item of remaining) { fullCardPosts.push(item.post); const idx = simpleMarkerPosts.findIndex(p => p.id === item.post.id); if (idx >= 0) simpleMarkerPosts.splice(idx, 1); } } return { fullCardPosts, simpleMarkerPosts, clusters, }; } /** * Génère une couleur pour un marqueur simple basée sur la catégorie */ export function getMarkerColorForCategory(category) { const cat = (category || '').toString().toUpperCase(); const colors = { 'EVENT': '#8b5cf6', // violet 'EVENTS': '#8b5cf6', 'NEWS': '#3b82f6', // bleu 'MARKET': '#10b981', // vert 'FRIENDS': '#f59e0b', // orange }; return colors[cat] || '#6b7280'; // gris par défaut }