/** * Algorithmes de calcul de plus court chemin pour les voyages */ /** * Classe pour le calcul de plus court chemin (algorithme de Dijkstra) */ export class PathFinder { /** * Construit un graphe à partir des données de voyage * @param {Array} travelData - Données de voyage * @param {String|Array} modes - Mode(s) de transport ('road', 'river', 'sea', ['road', 'river', 'sea'], 'water') * @returns {Object} Graphe avec adjacence et poids */ static buildGraph(travelData, modes = 'road') { const graph = {}; // Normaliser modes en tableau let modeList = []; if (modes === 'water') { modeList = ['river', 'sea']; } else if (modes === 'mixed') { modeList = ['road', 'river', 'sea']; } else if (Array.isArray(modes)) { modeList = modes; } else { modeList = [modes]; } for (const route of travelData) { const from = route.from; const to = route.to; // Initialiser les nœuds if (!graph[from]) { graph[from] = []; } if (!graph[to]) { graph[to] = []; } // Pour chaque mode de transport disponible for (const mode of modeList) { const distanceKey = `${mode}_distance`; const daysKey = `${mode}_days`; const distance = route[distanceKey]; const days = route[daysKey]; // Ignorer les routes sans ce mode de transport if (!distance || distance === "" || !days || days === "") { continue; } // Ajouter les arêtes (bidirectionnelles) graph[from].push({ destination: to, distance: parseFloat(distance), days: parseFloat(days), danger: route[`${mode}_danger`] || "", mode: mode }); graph[to].push({ destination: from, distance: parseFloat(distance), days: parseFloat(days), danger: route[`${mode}_danger`] || "", mode: mode }); } } return graph; } /** * Algorithme de Dijkstra pour trouver le plus court chemin * @param {Object} graph - Graphe d'adjacence * @param {String} start - Ville de départ * @param {String} end - Ville d'arrivée * @param {String} metric - Métrique à minimiser ('distance' ou 'days') * @returns {Object|null} Chemin trouvé avec détails ou null si pas de chemin */ static dijkstra(graph, start, end, metric = 'days') { // Normaliser les noms de villes const normalizedStart = this.findCityInGraph(graph, start); const normalizedEnd = this.findCityInGraph(graph, end); if (!normalizedStart || !normalizedEnd) { console.warn(`PathFinder: Ville non trouvée - start: ${start}, end: ${end}`); return null; } // Initialisation const distances = {}; const previous = {}; const visited = new Set(); const queue = []; // Initialiser toutes les distances à l'infini for (const node in graph) { distances[node] = Infinity; previous[node] = null; } distances[normalizedStart] = 0; queue.push({ node: normalizedStart, distance: 0 }); while (queue.length > 0) { // Trouver le nœud avec la plus petite distance queue.sort((a, b) => a.distance - b.distance); const { node: current } = queue.shift(); if (visited.has(current)) continue; visited.add(current); // Si on a atteint la destination if (current === normalizedEnd) { return this.reconstructPath(previous, normalizedStart, normalizedEnd, graph, metric); } // Explorer les voisins const neighbors = graph[current] || []; for (const neighbor of neighbors) { if (visited.has(neighbor.destination)) continue; const weight = neighbor[metric]; // 'distance' ou 'days' const newDistance = distances[current] + weight; if (newDistance < distances[neighbor.destination]) { distances[neighbor.destination] = newDistance; previous[neighbor.destination] = { from: current, edge: neighbor }; queue.push({ node: neighbor.destination, distance: newDistance }); } } } // Pas de chemin trouvé console.warn(`PathFinder: Aucun chemin trouvé entre ${start} et ${end}`); return null; } /** * Trouve une ville dans le graphe (insensible à la casse) * @param {Object} graph - Graphe * @param {String} cityName - Nom de la ville * @returns {String|null} Nom normalisé de la ville ou null */ static findCityInGraph(graph, cityName) { const lowerName = cityName.toLowerCase(); for (const city in graph) { if (city.toLowerCase() === lowerName) { return city; } } return null; } /** * Reconstruit le chemin à partir des prédécesseurs * @param {Object} previous - Map des prédécesseurs * @param {String} start - Ville de départ * @param {String} end - Ville d'arrivée * @param {Object} graph - Graphe d'adjacence * @param {String} metric - Métrique utilisée * @returns {Object} Détails du chemin */ static reconstructPath(previous, start, end, graph, metric) { const path = []; let current = end; let totalDistance = 0; let totalDays = 0; let maxDanger = ""; const modesUsed = new Set(); // Reconstruire le chemin en remontant while (current !== start) { const prev = previous[current]; if (!prev) break; const mode = prev.edge.mode || 'road'; modesUsed.add(mode); path.unshift({ from: prev.from, to: current, distance: prev.edge.distance, days: prev.edge.days, danger: prev.edge.danger, mode: mode }); totalDistance += prev.edge.distance; totalDays += prev.edge.days; // Calculer le danger maximum const dangerLevel = (prev.edge.danger || "").length; const currentMaxLevel = maxDanger.length; if (dangerLevel > currentMaxLevel) { maxDanger = prev.edge.danger; } current = prev.from; } return { path: path, totalDistance: totalDistance, totalDays: totalDays, maxDanger: maxDanger, steps: path.length, modesUsed: Array.from(modesUsed) }; } }