212 lines
6.6 KiB
JavaScript
212 lines
6.6 KiB
JavaScript
/**
|
|
* 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<clusterId, posts[]>
|
|
*/
|
|
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
|
|
}
|