/ src / lib / maps / utils / routing-engine.ts
routing-engine.ts
  1  /**
  2   * Motor de cálculo de rutas 100% offline
  3   * Implementa algoritmos de búsqueda de caminos para calcular rutas sin acceso a Internet
  4   */
  5  import { browser } from '$app/environment';
  6  import type { Place, RoutePoint, RouteStep, RouteResult } from '../models';
  7  
  8  // Now using interfaces imported from models.ts
  9  
 10  // Interfaz para nodos del grafo de la red de carreteras
 11  interface Node {
 12    id: string;
 13    lat: number;
 14    lng: number;
 15    connections: Connection[];
 16  }
 17  
 18  // Interfaz para conexiones entre nodos
 19  interface Connection {
 20    targetId: string;
 21    distance: number;  // Metros
 22    duration: number;  // Segundos
 23    wayName: string;
 24    wayType: string;
 25  }
 26  
 27  // Interfaz para grafo de carreteras
 28  interface RoadGraph {
 29    nodes: Record<string, Node>;
 30    version: string;
 31    lastUpdated: number;
 32  }
 33  
 34  // Datos de muestra para un grafo de carreteras simplificado
 35  // En una implementación real, estos datos se cargarían de un archivo generado
 36  // a partir de datos OSM (OpenStreetMap)
 37  const SAMPLE_GRAPH: RoadGraph = {
 38    version: "1.0",
 39    lastUpdated: Date.now(),
 40    nodes: {
 41      // Madrid - Puerta del Sol
 42      "n1": {
 43        id: "n1",
 44        lat: 40.416705, 
 45        lng: -3.703582,
 46        connections: [
 47          { 
 48            targetId: "n2", 
 49            distance: 380, 
 50            duration: 300, 
 51            wayName: "Calle Mayor", 
 52            wayType: "street" 
 53          },
 54          { 
 55            targetId: "n5", 
 56            distance: 450, 
 57            duration: 360, 
 58            wayName: "Calle Alcalá", 
 59            wayType: "street" 
 60          }
 61        ]
 62      },
 63      // Plaza Mayor
 64      "n2": {
 65        id: "n2",
 66        lat: 40.415557, 
 67        lng: -3.707233,
 68        connections: [
 69          { 
 70            targetId: "n1", 
 71            distance: 380, 
 72            duration: 300, 
 73            wayName: "Calle Mayor", 
 74            wayType: "street" 
 75          },
 76          { 
 77            targetId: "n3", 
 78            distance: 900, 
 79            duration: 720, 
 80            wayName: "Calle Toledo", 
 81            wayType: "street" 
 82          }
 83        ]
 84      },
 85      // Plaza de Cascorro
 86      "n3": {
 87        id: "n3",
 88        lat: 40.410996, 
 89        lng: -3.707416,
 90        connections: [
 91          { 
 92            targetId: "n2", 
 93            distance: 900, 
 94            duration: 720, 
 95            wayName: "Calle Toledo", 
 96            wayType: "street" 
 97          },
 98          { 
 99            targetId: "n4", 
100            distance: 650, 
101            duration: 520, 
102            wayName: "Calle Embajadores", 
103            wayType: "street" 
104          }
105        ]
106      },
107      // Glorieta de Embajadores
108      "n4": {
109        id: "n4",
110        lat: 40.405936, 
111        lng: -3.703595,
112        connections: [
113          { 
114            targetId: "n3", 
115            distance: 650, 
116            duration: 520, 
117            wayName: "Calle Embajadores", 
118            wayType: "street" 
119          },
120          { 
121            targetId: "n6", 
122            distance: 950, 
123            duration: 760, 
124            wayName: "Paseo de las Delicias", 
125            wayType: "avenue" 
126          }
127        ]
128      },
129      // Museo del Prado
130      "n5": {
131        id: "n5",
132        lat: 40.413561, 
133        lng: -3.692479,
134        connections: [
135          { 
136            targetId: "n1", 
137            distance: 450, 
138            duration: 360, 
139            wayName: "Calle Alcalá", 
140            wayType: "street" 
141          },
142          { 
143            targetId: "n7", 
144            distance: 1200, 
145            duration: 960, 
146            wayName: "Paseo del Prado", 
147            wayType: "avenue" 
148          }
149        ]
150      },
151      // Estación de Atocha
152      "n6": {
153        id: "n6",
154        lat: 40.406514, 
155        lng: -3.691481,
156        connections: [
157          { 
158            targetId: "n4", 
159            distance: 950, 
160            duration: 760, 
161            wayName: "Paseo de las Delicias", 
162            wayType: "avenue" 
163          },
164          { 
165            targetId: "n7", 
166            distance: 500, 
167            duration: 400, 
168            wayName: "Paseo de la Infanta Isabel", 
169            wayType: "avenue" 
170          }
171        ]
172      },
173      // Parque del Retiro (Puerta de Alcalá)
174      "n7": {
175        id: "n7",
176        lat: 40.420512, 
177        lng: -3.688133,
178        connections: [
179          { 
180            targetId: "n5", 
181            distance: 1200, 
182            duration: 960, 
183            wayName: "Paseo del Prado", 
184            wayType: "avenue" 
185          },
186          { 
187            targetId: "n6", 
188            distance: 500, 
189            duration: 400, 
190            wayName: "Paseo de la Infanta Isabel", 
191            wayType: "avenue" 
192          }
193        ]
194      }
195    }
196  };
197  
198  // Cargar el grafo de carreteras
199  export async function loadRoadGraph(): Promise<RoadGraph> {
200    if (!browser) {
201      return SAMPLE_GRAPH;
202    }
203    
204    try {
205      // En una implementación real, cargaríamos desde un archivo o IndexedDB
206      // Para esta demostración, usamos el grafo estático
207      return SAMPLE_GRAPH;
208    } catch (error) {
209      console.error('[routing-engine] Error al cargar grafo de carreteras:', error);
210      return SAMPLE_GRAPH;
211    }
212  }
213  
214  // Calcular una ruta entre dos puntos
215  export async function calculateOfflineRoute(
216    startPoint: RoutePoint | Place,
217    endPoint: RoutePoint | Place,
218    viaPoints: (RoutePoint | Place)[] = []
219  ): Promise<RouteResult> {
220    if (!browser) {
221      throw new Error('Solo disponible en el navegador');
222    }
223    
224    try {
225      // Cargar grafo de carreteras
226      const roadGraph = await loadRoadGraph();
227      
228      // Encontrar nodos más cercanos a los puntos
229      const startNode = findNearestNode(roadGraph, startPoint.lat, startPoint.lng);
230      const endNode = findNearestNode(roadGraph, endPoint.lat, endPoint.lng);
231      
232      // Convertir viaPoints a nodos
233      const viaNodes = viaPoints.map(point => 
234        findNearestNode(roadGraph, point.lat, point.lng)
235      );
236      
237      // Si no hay puntos intermedios, calculamos la ruta directa
238      if (viaNodes.length === 0) {
239        const route = findPath(roadGraph, startNode.id, endNode.id);
240        return formatRouteResult(roadGraph, route, startPoint, endPoint, []);
241      }
242      
243      // Si hay puntos intermedios, calculamos la ruta por partes
244      const routeParts = [];
245      let currentStart = startNode;
246      
247      // Para cada punto intermedio
248      for (const viaNode of viaNodes) {
249        const routePart = findPath(roadGraph, currentStart.id, viaNode.id);
250        routeParts.push(routePart);
251        currentStart = viaNode;
252      }
253      
254      // Última parte de la ruta
255      const lastPart = findPath(roadGraph, currentStart.id, endNode.id);
256      routeParts.push(lastPart);
257      
258      // Unir todas las partes
259      const combinedRoute = combineRoutes(routeParts);
260      
261      // Formatear resultado
262      return formatRouteResult(roadGraph, combinedRoute, startPoint, endPoint, viaPoints);
263    } catch (error) {
264      console.error('[routing-engine] Error al calcular ruta:', error);
265      throw error;
266    }
267  }
268  
269  // Encontrar el nodo más cercano a unas coordenadas
270  function findNearestNode(graph: RoadGraph, lat: number, lng: number): Node {
271    const nodes = Object.values(graph.nodes);
272    
273    // Encontrar el nodo más cercano usando la distancia euclidiana
274    let nearestNode = nodes[0];
275    let minDistance = calculateDistance(lat, lng, nearestNode.lat, nearestNode.lng);
276    
277    for (const node of nodes) {
278      const distance = calculateDistance(lat, lng, node.lat, node.lng);
279      
280      if (distance < minDistance) {
281        minDistance = distance;
282        nearestNode = node;
283      }
284    }
285    
286    return nearestNode;
287  }
288  
289  // Calcular distancia en metros entre dos puntos
290  function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
291    const R = 6371000; // Radio de la Tierra en metros
292    const dLat = toRadians(lat2 - lat1);
293    const dLng = toRadians(lng2 - lng1);
294    
295    const a = 
296      Math.sin(dLat/2) * Math.sin(dLat/2) +
297      Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * 
298      Math.sin(dLng/2) * Math.sin(dLng/2);
299    
300    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
301    const distance = R * c;
302    
303    return distance;
304  }
305  
306  // Convertir grados a radianes
307  function toRadians(degrees: number): number {
308    return degrees * Math.PI / 180;
309  }
310  
311  // Interfaz para representar un camino
312  interface Path {
313    nodes: string[];
314    totalDistance: number;
315    totalDuration: number;
316    steps: {
317      fromId: string;
318      toId: string;
319      connection: Connection;
320    }[];
321  }
322  
323  // Algoritmo de Dijkstra para encontrar la ruta más corta entre dos nodos
324  function findPath(graph: RoadGraph, startId: string, endId: string): Path {
325    // Inicializar distancias y nodos previos
326    const distances: Record<string, number> = {};
327    const durations: Record<string, number> = {};
328    const previous: Record<string, string | null> = {};
329    const connections: Record<string, Connection> = {};
330    
331    // Inicializar con valores infinitos
332    Object.keys(graph.nodes).forEach(nodeId => {
333      distances[nodeId] = Infinity;
334      durations[nodeId] = Infinity;
335      previous[nodeId] = null;
336    });
337    
338    // Distancia y duración desde el inicio a sí mismo es 0
339    distances[startId] = 0;
340    durations[startId] = 0;
341    
342    // Conjunto de nodos no visitados
343    const unvisited = new Set(Object.keys(graph.nodes));
344    
345    // Mientras haya nodos no visitados
346    while (unvisited.size > 0) {
347      // Encontrar el nodo no visitado con la menor distancia
348      let current: string | null = null;
349      let minDistance = Infinity;
350      
351      for (const nodeId of unvisited) {
352        if (distances[nodeId] < minDistance) {
353          minDistance = distances[nodeId];
354          current = nodeId;
355        }
356      }
357      
358      // Si no hay nodos alcanzables, terminar
359      if (current === null || distances[current] === Infinity) {
360        break;
361      }
362      
363      // Si hemos llegado al destino, terminar
364      if (current === endId) {
365        break;
366      }
367      
368      // Marcar como visitado
369      unvisited.delete(current);
370      
371      // Actualizar distancias de los vecinos
372      const node = graph.nodes[current];
373      
374      for (const connection of node.connections) {
375        // Si el nodo ya está visitado, ignorar
376        if (!unvisited.has(connection.targetId)) {
377          continue;
378        }
379        
380        // Calcular nueva distancia
381        const newDistance = distances[current] + connection.distance;
382        const newDuration = durations[current] + connection.duration;
383        
384        // Si la nueva distancia es menor, actualizar
385        if (newDistance < distances[connection.targetId]) {
386          distances[connection.targetId] = newDistance;
387          durations[connection.targetId] = newDuration;
388          previous[connection.targetId] = current;
389          connections[connection.targetId] = connection;
390        }
391      }
392    }
393    
394    // Reconstruir el camino
395    const path: string[] = [];
396    const steps: { fromId: string; toId: string; connection: Connection }[] = [];
397    let current = endId;
398    
399    // Si no hay camino al destino
400    if (previous[current] === null && current !== startId) {
401      throw new Error(`No se pudo encontrar una ruta entre ${startId} y ${endId}`);
402    }
403    
404    // Reconstruir el camino desde el final hasta el inicio
405    while (current) {
406      path.unshift(current);
407      
408      if (previous[current] !== null) {
409        steps.unshift({
410          fromId: previous[current]!,
411          toId: current,
412          connection: connections[current]
413        });
414      }
415      
416      current = previous[current]!;
417      
418      if (current === null || current === startId) {
419        path.unshift(startId);
420        break;
421      }
422    }
423    
424    // Retornar el camino completo
425    return {
426      nodes: path,
427      totalDistance: distances[endId],
428      totalDuration: durations[endId],
429      steps
430    };
431  }
432  
433  // Combinar varias rutas en una sola
434  function combineRoutes(routes: Path[]): Path {
435    if (routes.length === 0) {
436      throw new Error('No hay rutas para combinar');
437    }
438    
439    if (routes.length === 1) {
440      return routes[0];
441    }
442    
443    // Inicializar con la primera ruta
444    const combinedRoute: Path = {
445      nodes: [...routes[0].nodes],
446      totalDistance: routes[0].totalDistance,
447      totalDuration: routes[0].totalDuration,
448      steps: [...routes[0].steps]
449    };
450    
451    // Añadir el resto de rutas
452    for (let i = 1; i < routes.length; i++) {
453      const route = routes[i];
454      
455      // Añadir nodos (excepto el primero que ya está incluido)
456      combinedRoute.nodes.push(...route.nodes.slice(1));
457      
458      // Añadir pasos
459      combinedRoute.steps.push(...route.steps);
460      
461      // Actualizar distancia y duración totales
462      combinedRoute.totalDistance += route.totalDistance;
463      combinedRoute.totalDuration += route.totalDuration;
464    }
465    
466    return combinedRoute;
467  }
468  
469  // Formatear resultado de la ruta
470  function formatRouteResult(
471    graph: RoadGraph,
472    path: Path,
473    startPoint: RoutePoint | Place,
474    endPoint: RoutePoint | Place,
475    viaPoints: (RoutePoint | Place)[]
476  ): RouteResult {
477    // Crear pasos de la ruta
478    const steps: RouteStep[] = path.steps.map(step => {
479      const fromNode = graph.nodes[step.fromId];
480      const toNode = graph.nodes[step.toId];
481      
482      return {
483        distance: step.connection.distance,
484        duration: step.connection.duration,
485        name: step.connection.wayName,
486        type: step.connection.wayType,
487        instruction: generateInstruction(step.connection.wayName, step.connection.wayType),
488        startPoint: {
489          lat: fromNode.lat,
490          lng: fromNode.lng
491        },
492        endPoint: {
493          lat: toNode.lat,
494          lng: toNode.lng
495        }
496      };
497    });
498    
499    // Crear geometría GeoJSON para la ruta
500    const coordinates = path.nodes.map(nodeId => {
501      const node = graph.nodes[nodeId];
502      return [node.lng, node.lat]; // GeoJSON usa [longitud, latitud]
503    });
504    
505    const geometry = {
506      type: "LineString",
507      coordinates
508    };
509    
510    // Asegurar que startPoint y endPoint tienen el formato correcto
511    const start: RoutePoint = {
512      lat: startPoint.lat,
513      lng: startPoint.lng,
514      name: 'name' in startPoint ? startPoint.name : 'Inicio'
515    };
516    
517    const end: RoutePoint = {
518      lat: endPoint.lat,
519      lng: endPoint.lng,
520      name: 'name' in endPoint ? endPoint.name : 'Destino'
521    };
522    
523    // Formatear puntos intermedios
524    const via: RoutePoint[] = viaPoints.map(point => ({
525      lat: point.lat,
526      lng: point.lng,
527      name: 'name' in point ? point.name : 'Punto intermedio'
528    }));
529    
530    // Crear resultado final
531    return {
532      distance: path.totalDistance,
533      duration: path.totalDuration,
534      startPoint: start,
535      endPoint: end,
536      viaPoints: via,
537      steps,
538      geometry: {
539        type: "Feature",
540        properties: {},
541        geometry
542      }
543    };
544  }
545  
546  // Generar instrucción de navegación basada en el nombre y tipo de vía
547  function generateInstruction(wayName: string, wayType: string): string {
548    const typeText = wayType === 'avenue' ? 'el paseo' : 'la calle';
549    return `Continúa por ${typeText} ${wayName}`;
550  }