Source: viewRoadtrip.js

/**
 * @file Roadtrip Visualization and Trip Data Management
 * @description
 * Handles the display of roadtrip routes on Leaflet maps, including:
 * - Drawing routes with numbered markers
 * - Mode of transport selection (car, bike, foot)
 * - ETA and duration calculation via OSRM
 * - Markdown rendering for descriptions
 * @requires Leaflet.js
 * @requires Leaflet.markercluster
 * @requires OSRM
 * @requires marked.js
 * @requires DOMPurify
 */
document.addEventListener('DOMContentLoaded', () => {
    initGlobalMap();
    setTimeout(calculateAllSegments, 1000);
});

/** * Stores instances of individual step maps indexed by ID
 * @type {Object.<string, L.Map>}
 */
const viewMapInstances = {};

/** * Color palette for distinct trip paths
 * @type {string[]}
 */
const colorsPalette = [
    '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
    '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
    '#469990', '#dcbeff', '#9A6324', '#fffac8', '#800000'
];

/**
 * Retrieves trip data from the global roadTripData array.
 * @function getTrajetData
 * @param {string|number} id - The ID of the trip
 * @returns {Object|null} The trip data object or null
 */

function getTrajetData(id) {
    if (typeof roadTripData === 'undefined' || !roadTripData) return null;
    return roadTripData.find(t => t.id == id);
}
/**
 * Initializes the main global map showing all trips.
 * @async
 * @function initGlobalMap
 * @returns {Promise<void>}
 */
async function initGlobalMap() {
    const mapDiv = document.getElementById('map-global');
    if (!mapDiv) return;

    mapDiv.style.display = 'block';

    const hasData = (typeof roadTripData !== 'undefined' && roadTripData && roadTripData.length > 0);
    const map = L.map('map-global');

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; OpenStreetMap'
    }).addTo(map);

    setTimeout(() => { map.invalidateSize(); }, 200);

    if (!hasData) {
        map.setView([46.6, 2.2], 5);
        return;
    }

    const markersCluster = L.markerClusterGroup({
        maxClusterRadius: 40,
        showCoverageOnHover: false
    });
    map.addLayer(markersCluster);

    const bounds = [];
    let colorIndex = 0;

    for (const data of roadTripData) {
        if (!data.depart || !data.arrivee) continue;

        data.color = colorsPalette[colorIndex % colorsPalette.length];

        try {
            await drawRoute(map, data, data.color, false, markersCluster);

            if(data.depart.lat) bounds.push([data.depart.lat, data.depart.lon]);
            if(data.arrivee.lat) bounds.push([data.arrivee.lat, data.arrivee.lon]);
            if(data.sousEtapes) {
                data.sousEtapes.forEach(s => { if(s.lat) bounds.push([s.lat, s.lon]); });
            }
        } catch (e) { console.error(e); }
        colorIndex++;
    }

    if (bounds.length > 0) {
        map.fitBounds(bounds, { padding: [50, 50] });
    } else {
        map.setView([46.6, 2.2], 6);
    }
}
/**
 * Initializes a specific map for a single trip step.
 * @async
 * @function initStepMap
 * @param {string|number} id - Trip ID
 * @returns {Promise<void>}
 */
async function initStepMap(id) {
    const data = getTrajetData(id);
    const divId = 'map-trajet-' + id;
    const divElement = document.getElementById(divId);

    if (!divElement) return;

    if (!data || !data.depart) {
        divElement.innerHTML = '<div style="display:flex; align-items:center; justify-content:center; height:100%; color:#888;">Données en attente...</div>';
        return;
    }

    if (viewMapInstances[id]) {
        setTimeout(() => {
            viewMapInstances[id].invalidateSize();
            if(data.layerBounds) viewMapInstances[id].fitBounds(data.layerBounds, { padding: [20, 20] });
        }, 200);
        return;
    }

    const map = L.map(divId);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; OSM' }).addTo(map);

    viewMapInstances[id] = map;

    setTimeout(async () => {
        map.invalidateSize();
        await drawRoute(map, data, data.color || '#3388ff', true, null);
    }, 100);
}
/**
 * Draws the route polyline and markers on a map using OSRM routing.
 * Falls back to straight lines if the API fails.
 * @async
 * @function drawRoute
 * @param {L.Map} map - Target Leaflet map
 * @param {Object} data - Trip data
 * @param {string} color - Polyline color
 * @param {boolean} fitBounds - Whether to zoom into the route
 * @param {L.MarkerClusterGroup|null} clusterGroup - Optional cluster group for markers
 * @returns {Promise<void>}
 */
async function drawRoute(map, data, color, fitBounds, clusterGroup) {
    let stepCounter = 1;

    createNumberedMarker(map, data.depart, stepCounter++, color, `<b>Départ:</b> ${data.depart.nom}`, 'left', clusterGroup);

    if(data.sousEtapes) {
        data.sousEtapes.forEach(s => {
            let popup = `<b>📍 ${s.nom}</b>`;
            if(s.heure && s.heure !== "00:00:00") popup += `<br>⏱️ Pause: ${s.heure}`;
            createNumberedMarker(map, s, stepCounter++, color, popup, null, clusterGroup);
        });
    }

    createNumberedMarker(map, data.arrivee, stepCounter, color, `<b>Arrivée:</b> ${data.arrivee.nom}`, 'right', clusterGroup);

    const servers = {
        'voiture': 'https://routing.openstreetmap.de/routed-car',
        'velo': 'https://routing.openstreetmap.de/routed-bike',
        'vélo': 'https://routing.openstreetmap.de/routed-bike',
        'marche': 'https://routing.openstreetmap.de/routed-foot',
        'à pied': 'https://routing.openstreetmap.de/routed-foot'
    };

    const mode = (data.mode || 'voiture').toLowerCase();
    const baseUrl = servers[mode] || servers['voiture'];

    let coords = `${data.depart.lon},${data.depart.lat}`;
    if(data.sousEtapes) {
        data.sousEtapes.forEach(s => {
            if(s.lon && s.lat) coords += `;${s.lon},${s.lat}`;
        });
    }
    coords += `;${data.arrivee.lon},${data.arrivee.lat}`;

    const url = `${baseUrl}/route/v1/driving/${coords}?overview=full&geometries=geojson`;

    try {
        const resp = await fetch(url);
        const json = await resp.json();

        if (json.code === 'Ok' && json.routes[0]) {
            const style = {
                color: color,
                weight: 5,
                opacity: 0.8,
                dashArray: (mode !== 'voiture') ? '10, 10' : null
            };

            const layer = L.geoJSON(json.routes[0].geometry, style).addTo(map);

            if(fitBounds) map.fitBounds(layer.getBounds(), { padding:[30,30] });
            data.layerBounds = layer.getBounds();
        }
    } catch(e) {
        console.warn("API Routage échouée, tracé ligne droite.", e);
        const points = [[data.depart.lat, data.depart.lon]];
        if(data.sousEtapes) data.sousEtapes.forEach(s => points.push([s.lat, s.lon]));
        points.push([data.arrivee.lat, data.arrivee.lon]);

        L.polyline(points, { color: color, dashArray: '5,10' }).addTo(map);
        if(fitBounds) map.fitBounds(L.latLngBounds(points));
    }
}
/**
 * Creates a circular numbered marker for route steps.
 * @function createNumberedMarker
 * @param {L.Map} map - Leaflet map instance
 * @param {Object} point - Latitude and Longitude object
 * @param {number} number - Step number to display
 * @param {string} color - Background color
 * @param {string} popupText - HTML content for the popup
 * @param {string|null} offset - CSS class for position offset
 * @param {L.MarkerClusterGroup|null} cluster - Cluster group to add to
 */
function createNumberedMarker(map, point, number, color, popupText, offset, cluster) {
    if(!point || !point.lat || !point.lon) return;

    const offsetClass = offset ? `marker-offset-${offset}` : '';

    const icon = L.divIcon({
        className: `custom-marker-number ${offsetClass}`,
        html: `<div class="marker-pin" style="background-color: ${color}; color: white; display:flex; justify-content:center; align-items:center; border-radius:50%; width:100%; height:100%; border:2px solid white; box-shadow:0 2px 5px rgba(0,0,0,0.3); font-weight:bold;">${number}</div>`,
        iconSize: [30, 30],
        iconAnchor: [15, 15],
        popupAnchor: [0, -15]
    });

    const marker = L.marker([point.lat, point.lon], { icon: icon }).bindPopup(popupText);

    if(cluster) cluster.addLayer(marker);
    else marker.addTo(map);
}
/**
 * Toggles the visibility of trip details and local maps.
 * Hides global map when a specific trip is active.
 * @function toggleTrajet
 * @param {string|number} id - Trip ID
 */
window.toggleTrajet = function(id) {
    const content = document.getElementById('sub-steps-' + id);
    const card = document.getElementById('card-' + id);
    const mapGlobal = document.getElementById('map-global');

    if (!content || !card) return;

    const isHidden = content.classList.contains('d-none');

    if (!isHidden) {
        content.classList.add('d-none');
        card.classList.remove('active');
        checkToggleGlobalMap();
    } else {
        content.classList.remove('d-none');
        card.classList.add('active');

        if(mapGlobal) mapGlobal.style.display = 'none';

        setTimeout(() => { initStepMap(id); }, 100);
    }
};

/**
 * Checks if any trip cards are active and toggles the global map accordingly.
 * @function checkToggleGlobalMap
 */
function checkToggleGlobalMap() {
    const active = document.querySelectorAll('.card-vu.active'); // card-vu est la classe que j'ai remise dans le PHP
    const mapGlobal = document.getElementById('map-global');

    if(mapGlobal) {
        if (active.length === 0) {
            mapGlobal.style.display = 'block';
            setTimeout(() => {
                const map = L.map('map-global');
                window.dispatchEvent(new Event('resize'));
            }, 100);
        } else {
            mapGlobal.style.display = 'none';
        }
    }
}
/**
 * Iterates through all cards to calculate segment distances and times.
 * @function calculateAllSegments
 */
function calculateAllSegments() {
    const cards = document.querySelectorAll('.card-vu');
    cards.forEach((card, i) => {
        setTimeout(() => processCardTimes(card), i * 600);
    });
}
/**
 * Processes OSRM routing data for a specific card to update UI with distances and times.
 * @async
 * @function processCardTimes
 * @param {HTMLElement} card - The DOM element of the trip card
 * @returns {Promise<void>}
 */
async function processCardTimes(card) {
    const id = card.id.replace('card-', '');
    const data = getTrajetData(id);

    if(!data || !data.depart) return;

    const servers = {
        'voiture': 'https://routing.openstreetmap.de/routed-car',
        'velo': 'https://routing.openstreetmap.de/routed-bike',
        'vélo': 'https://routing.openstreetmap.de/routed-bike',
        'marche': 'https://routing.openstreetmap.de/routed-foot',
        'à pied': 'https://routing.openstreetmap.de/routed-foot'
    };
    const baseUrl = servers[data.mode] || servers['voiture'];

    let coords = `${data.depart.lon},${data.depart.lat}`;
    if(data.sousEtapes) data.sousEtapes.forEach(s => { coords += `;${s.lon},${s.lat}`; });
    coords += `;${data.arrivee.lon},${data.arrivee.lat}`;

    const url = `${baseUrl}/route/v1/driving/${coords}?overview=false&steps=false`;

    try {
        const resp = await fetch(url);
        const json = await resp.json();

        if (json.code === 'Ok' && json.routes && json.routes[0]) {
            const legs = json.routes[0].legs;

            let currentClock = data.heure_depart || '08:00';

            const segmentInfos = card.querySelectorAll('.segment-info');
            const stepRows = card.querySelectorAll('.step-row');

            legs.forEach((leg, index) => {
                if(segmentInfos[index]) {
                    const segDiv = segmentInfos[index];
                    const distKm = (leg.distance / 1000).toFixed(1);

                    segDiv.querySelector('.segment-distance').textContent = `${distKm} km`;
                    segDiv.querySelector('.segment-time').textContent = formatDuration(leg.duration);

                    segDiv.querySelectorAll('.segment-loader').forEach(el => el.remove());
                }

                const arrivalClock = addTime(currentClock, leg.duration);

                const targetRow = stepRows[index + 1];

                if (targetRow) {
                    const loader = targetRow.querySelector('.horaire-calcule');

                    if(loader) {
                        const stepCard = targetRow.querySelector('.step-card');
                        const pauseStr = stepCard ? stepCard.getAttribute('data-pause') : '00:00';
                        const pauseSec = durationToSeconds(pauseStr);

                        const isLast = (index === legs.length - 1);

                        if (isLast) {
                            loader.innerHTML = `🏁 Arrivée : <strong>${arrivalClock}</strong>`;
                        } else {
                            let html = `⏰ Arrivée : <strong>${arrivalClock}</strong>`;

                            if (pauseSec > 0) {
                                const departureClock = addTime(arrivalClock, pauseSec);
                                html += `<br><span class="horaire-depart-etape" style="color:var(--vert); display:block; margin-top:5px; font-size:0.85em;">🚀 Repart : <strong>${departureClock}</strong></span>`;
                                currentClock = departureClock;
                            } else {
                                currentClock = arrivalClock;
                            }
                            loader.innerHTML = html;
                        }
                    }
                }
            });
        }
    } catch (e) {
        console.error("Erreur calcul temps:", e);
    }
}
/**
 * Adds seconds to a time string.
 * @function addTime
 * @param {string} startTime - Format "HH:mm"
 * @param {number} secondsToAdd - Seconds to add
 * @returns {string} New time string "HH:mm"
 */
function addTime(startTime, secondsToAdd) {
    if(!startTime) return "--:--";
    const parts = startTime.split(':').map(Number);
    const date = new Date();
    date.setHours(parts[0], parts[1], 0, 0);
    date.setSeconds(date.getSeconds() + Math.round(secondsToAdd));
    return date.getHours().toString().padStart(2, '0') + ":" +
        date.getMinutes().toString().padStart(2, '0');
}
/**
 * Converts a duration string (HH:mm:ss or HH:mm) to seconds.
 * @function durationToSeconds
 * @param {string} timeStr - Time string
 * @returns {number} Duration in seconds
 */
function durationToSeconds(timeStr) {
    if(!timeStr) return 0;
    const parts = timeStr.split(':').map(Number);
    let seconds = 0;
    if(parts.length >= 2) seconds += (parts[0] * 3600) + (parts[1] * 60);
    if(parts.length === 3) seconds += parts[2];
    return seconds;
}
/**
 * Converts a duration string (HH:mm:ss or HH:mm) to seconds.
 * @function durationToSeconds
 * @param {string} timeStr - Time string
 * @returns {number} Duration in seconds
 */
function formatDuration(seconds) {
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    if (h > 0) return `${h}h ${m}min`;
    return `${m} min`;
}
/**
 * Markdown to HTML initialization.
 * Sanitizes and renders markdown content within elements with '.markdown-to-html' class.
 */
document.addEventListener("DOMContentLoaded", function() {
    marked.setOptions({
        breaks: true,
        gfm: true
    });

    document.querySelectorAll('.markdown-to-html').forEach(function(el) {
        const markdownText = el.getAttribute('data-markdown');

        if (markdownText) {
            const rawHtml = marked.parse(markdownText);

            const cleanHtml = DOMPurify.sanitize(rawHtml);

            el.innerHTML = cleanHtml;
        } else {
            el.innerHTML = '';
        }
    });
});

document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('.js-trip-toggle').forEach(function(header) {

        header.addEventListener('click', function() {
            const tripId = this.getAttribute('data-trip-id');
            window.toggleTrajet(tripId);
        });

    });
});