Librería: Mapbox

Descripción: El usuario agrega varios puntos sobre el mapa y el sistema captura los puntos, genera la ruta y muestra la distancia y el tiempo estimado.

image.png

Archivo CSS

#map {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 100%;
}
.control-panel {
    position: absolute;
    top: 10px;
    right: 10px;
    padding: 10px;
    background: white;
    border-radius: 4px;
    max-width: 300px;
    z-index: 1;
}
#points-list {
    margin-top: 10px;
    max-height: 200px;
    overflow-y: auto;
}
.point-item {
    padding: 5px;
    border-bottom: 1px solid #eee;
}
button {
    margin-top: 10px;
    padding: 8px;
    background: #3887be;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}
button:hover {
    background: #2a65a0;
}

.marker-label {
    position: absolute;
    top: -25px; /* Ajusta esta distancia si es necesario */
    left: 50%;
    transform: translateX(-50%);
    background-color: #e02a46;
    color: white;
    padding: 2px 5px;
    border-radius: 5px;
    font-size: 12px;
    font-weight: bold;
    white-space: nowrap;
}

Archivo HMTL:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Analisis de Rutas Optimizadas</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
    <link href="<https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css>" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="mapa.css">
</head>

<body>
    <div id="map"></div>
    <div class="control-panel">
        <h3>Analisis de Rutas Optimizadas</h3>
        <p>Haz clic en el mapa para agregar puntos de parada.</p>
        <div id="points-list"></div>
        <button id="calculate-route">Calcular Ruta Óptima</button>
        <button id="clear-points">Limpiar Puntos</button>
        <div id="route-info"></div>
    </div>
    <script src="<https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js>"></script>
    <script src="mapa.js"></script>
</body>
</html>

Archivo JavaScript:

// Agrega tu token
mapboxgl.accessToken = 'TU TOKEN XXXXXX';

// Inicializar el mapa en las coordenadas y zoom requeridas
const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [-74.082, 4.639], // Ajustar a la ubicación deseada 
    zoom: 14
});

// Variables para almacenar los datos de los puntos
const markers = [];
const waypoints = [];
let routeSource = null;

// Función para agregar un marcador al hacer clic en el mapa
map.on('click', (e) => {       
    const coordinates = [e.lngLat.lng, e.lngLat.lat];
    const marker = new mapboxgl.Marker({          // Crear un marcador y agregarlo al mapa
        color: 'blue',
        draggable: true
    })
    .setLngLat(coordinates)
    .addTo(map);    
    
    const label = document.createElement('div');   // Crear un contenedor para el número de la parada
    label.className = 'marker-label';
    label.textContent = `Punto. ${markers.length + 1}`; // Número de la parada (parada 1, parada 2, etc.)
    
    marker.getElement().appendChild(label); // Añadir el número de la parada al marcador

    marker.on('dragend', () => {                // Actualizar coordenadas al arrastrar el marcador
        const newCoords = marker.getLngLat();
        const index = markers.indexOf(marker);
        if (index !== -1) {
            waypoints[index] = [newCoords.lng, newCoords.lat];
            updatePointsList();
        }
    });    

    markers.push(marker);         // Almacenar el marcador y las coordenadas
    waypoints.push(coordinates);
    
    updatePointsList();         // Actualizar la lista de puntos
});

// Función para actualizar la lista de puntos en el panel
function updatePointsList() {
    const pointsList = document.getElementById('points-list');
    pointsList.innerHTML = '';
    
    waypoints.forEach((point, index) => {
        const pointItem = document.createElement('div');
        pointItem.className = 'point-item';
        pointItem.innerHTML = `Punto ${index + 1}: [${point[0].toFixed(4)}, ${point[1].toFixed(4)}]
            <button class="remove-point" data-index="${index}">Eliminar</button>`;
        pointsList.appendChild(pointItem);
    });
    
    document.querySelectorAll('.remove-point').forEach(button => {       // Agregar event listeners a botones de eliminar
        button.addEventListener('click', (e) => {
            const index = parseInt(e.target.getAttribute('data-index'));
            removePoint(index);
        });
    });
}

// Función para eliminar un punto
function removePoint(index) {
    if (index >= 0 && index < markers.length) {
        markers[index].remove(); // Eliminar marcador del mapa
        markers.splice(index, 1); // Eliminar del array de marcadores
        waypoints.splice(index, 1); // Eliminar del array de waypoints
        updatePointsList(); // Actualizar lista
    }
}

// Botón para calcular la ruta
document.getElementById('calculate-route').addEventListener('click', () => {
    calculateOptimizedRoute();
});

// Función para limpiar puntos
document.getElementById('clear-points').addEventListener('click', () => {
    clearAllPoints();
});

// Función para calcular la ruta optimizada
async function calculateOptimizedRoute() {    
    if (waypoints.length < 2) {    // Verificar que tenemos suficientes puntos
        alert('Se necesitan al menos 2 puntos para calcular una ruta.');
        return;
    }    
    try {        
        const coordinates = waypoints.map(point => point.join(','));    // Preparar los waypoints en formato correcto  
        
        const baseUrl = `https://api.mapbox.com/directions/v5/mapbox/driving-traffic/${coordinates.join(';')}`;   // Crear URL base para la petición a la API de Direcciones de Mapbox
  
        const params = new URLSearchParams({   // Iniciar con parámetros básicos            
            alternatives: 'true',
            geometries: 'geojson',
            annotations: 'distance',
            overview: 'full',
            steps: 'true',
            access_token: mapboxgl.accessToken,
        });

        const url = `${baseUrl}?${params.toString()}`;   // Construir URL completa
        console.log("URL de solicitud: " + url);        
        
        const response = await fetch(url);    // Realizar la petición
        const data = await response.json();
        
        if (data.code !== 'Ok') {
            console.error("Error de la API:", data.message);
            alert(`Error de la API: ${data.message}`);
        return;
        }

        console.log(data);  // Para ver la respuesta completa de la API
        
        displayRoute(data);    // Mostrar la ruta en el mapa        
        
        displayRouteInfo(data);    // Mostrar información de la ruta        
        
    } catch (error) {
        console.error('Error al calcular la ruta:', error);
        alert('Hubo un error al calcular la ruta. Verifica la consola para más detalles.');   

    }
}

// Función para mostrar la ruta en el mapa
function displayRoute(routeData) {
    
    if (map.getSource('route')) {   // Si ya existe una capa de ruta, eliminarla
        map.removeLayer('route-layer');
        map.removeSource('route');
    }    
    
    map.addSource('route', {    // Agregar la nueva ruta como fuente y capa
        type: 'geojson',
        data: {
            type: 'Feature',
            properties: {},
            geometry: routeData.routes[0].geometry
        }
    });
    
    map.addLayer({
        id: 'route-layer',
        type: 'line',
        source: 'route',
        layout: {
            'line-join': 'round',
            'line-cap': 'round'
        },
        paint: {
            'line-color': '#7525be',
            'line-width': 5,
            'line-opacity': 0.75
        }
    });    
    
    const coordinates = routeData.routes[0].geometry.coordinates;   // Ajustar el mapa para mostrar toda la ruta
    const bounds = coordinates.reduce((bounds, coord) => {
        return bounds.extend(coord);
    }, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));
    
    map.fitBounds(bounds, {
        padding: 50
    });
}

// Función para mostrar información de la ruta
function displayRouteInfo(routeData) {
    const route = routeData.routes[0];
    const distance = (route.distance / 1000).toFixed(2); // Convertir a km
    const duration = Math.floor(route.duration / 60); // Convertir a minutos    
    
    let waypointOrder = ''; // Crear un mapeo del orden optimizado de paradas
    
    if (routeData.waypoints && Array.isArray(routeData.waypoints)) {    
        waypointOrder = routeData.waypoints.map((wp, idx) => {
            return idx + 1;  // Usar el índice de los waypoints de la respuesta
        }).join(' → ');        
    } else {
        waypointOrder = 'No disponible';
    }

    const routeInfo = document.getElementById('route-info');
    routeInfo.innerHTML = `
        <h4>Información de la Ruta</h4>
        <p>Distancia total: ${distance} km</p>
        <p>Tiempo estimado: ${duration} minutos</p>
        <p>Orden de paradas optimizado:</p>
        <ul>
            ${waypointOrder.split(' → ').map(point => `<li>${point}</li>`).join('')}
        </ul>
    `;

    console.log(routeData)
}

// Función para limpiar todos los puntos
function clearAllPoints() {
    
    markers.forEach(marker => marker.remove());  // Eliminar todos los marcadores del mapa    
    
    markers.length = 0;  // Limpiar arrays
    waypoints.length = 0;    
    
    updatePointsList();  // Actualizar lista de puntos    
    
    if (map.getSource('route')) {  // Eliminar ruta si existe
        map.removeLayer('route-layer');
        map.removeSource('route');
    }    
    
    document.getElementById('route-info').innerHTML = '';   // Limpiar información de ruta
}