AllTrails KML Downloader

This script adds a "Download KML" button to any trail on AllTrails.com. KML can be imported into Google Maps and other mapping programs.

目前为 2025-04-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         AllTrails KML Downloader
// @namespace    http://tampermonkey.net/
// @version      2025-04-30
// @description  This script adds a "Download KML" button to any trail on AllTrails.com. KML can be imported into Google Maps and other mapping programs.
// @match        https://www.alltrails.com/trail/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/mapbox-polyline/1.1.1/polyline.min.js
// @license      GNU GPLv3
// ==/UserScript==

(function() {
    'use strict';

    function createDownloadButton() {
        const button = document.createElement('button');
        button.textContent = 'Download KML';
        button.style.position = 'fixed';
        button.style.top = '10px';
        button.style.right = '10px';
        button.style.zIndex = '9999';
        button.style.padding = '10px 15px';
        button.style.background = '#4CAF50';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.fontSize = '16px';
        button.style.cursor = 'pointer';

        button.onclick = function() {
            fetchMapPageAndGenerateKML();
        };

        document.body.appendChild(button);
    }

    function extractTrailDetails() {
        const details = {
            length: '',
            elevation: '',
            time: '',
            type: ''
        };

        const statBlocks = document.querySelectorAll('.TrailStats_stat__O2GvM');

        statBlocks.forEach(stat => {
            const labelEl = stat.querySelector('.TrailStats_statLabel__vKMLy');
            const valueEl = stat.querySelector('.TrailStats_statValueSm__HlKIU');

            if (!labelEl) return;

            const label = labelEl.textContent.trim().toLowerCase();

            if (label.includes('length')) {
                details.length = valueEl?.textContent.trim() || '';
            } else if (label.includes('elevation')) {
                details.elevation = valueEl?.textContent.trim() || '';
            } else if (label.includes('estimated time')) {
                details.time = valueEl?.textContent.trim() || '';
            } else if (label.match(/loop|out.*back|point.*point/i)) {
                details.type = labelEl.textContent.trim();
            }
        });

        // If type still empty, try to grab from last stat label if it's a standalone type
        if (!details.type) {
            statBlocks.forEach(stat => {
                const labelEl = stat.querySelector('.TrailStats_statLabel__vKMLy');
                if (labelEl && ['loop', 'out & back', 'point to point'].includes(labelEl.textContent.trim().toLowerCase())) {
                    details.type = labelEl.textContent.trim();
                }
            });
        }

        return details;
    }

    function fetchMapPageAndGenerateKML() {
        const trailUrl = window.location.href;
        const mapUrl = trailUrl.replace('/trail/', '/explore/trail/') + '?mobileMap=false&initFlyover=true&flyoverReturnToTrail';

        console.log(`Fetching map data from: ${mapUrl}`);

        GM_xmlhttpRequest({
            method: 'GET',
            url: mapUrl,
            onload: function(response) {
                const mapPageHtml = response.responseText;
                const mapDataMatch = mapPageHtml.match(/<div data-react-class="SearchApp" data-react-props="({.+?})"/);

                if (mapDataMatch && mapDataMatch[1]) {
                    const mapDataJson = JSON.parse(mapDataMatch[1].replace(/&quot;/g, '"'));
                    const details = extractTrailDetails();
                    generateKML(mapDataJson, details);
                } else {
                    console.error('Failed to extract map data');
                }
            },
            onerror: function() {
                console.error('Error fetching map page.');
            }
        });
    }

    function generateKML(mapData, trailDetails) {
        const trailName = mapData.initialExploreMap.name;
        const waypoints = mapData.initialExploreMap.waypoints;
        const routes = mapData.initialExploreMap.routes;

        const description = `
            URL: ${window.location.href}<br/>
            Length: ${trailDetails.length}<br/>
            Elevation Gain: ${trailDetails.elevation}<br/>
            Estimated Time: ${trailDetails.time}<br/>
            Trail Type: ${trailDetails.type}
        `.trim();

        let kmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n`;

        routes.forEach(route => {
            route.lineSegments.forEach(segment => {
                const pointsData = segment.polyline.pointsData;

                if (typeof pointsData === 'string') {
                    const decodedPoints = polyline.decode(pointsData);
                    let coordinates = decodedPoints.map(point => `${point[1]},${point[0]},0`).join(' ');
                    kmlContent += `
                    <Placemark>
                        <name>${trailName}</name>
                        <description><![CDATA[${description}]]></description>
                        <LineString>
                            <coordinates>${coordinates}</coordinates>
                        </LineString>
                    </Placemark>
                    `;
                } else {
                    console.error('pointsData is not a polyline string:', pointsData);
                }
            });
        });

        waypoints.forEach(waypoint => {
            kmlContent += `
            <Placemark>
                <name>${waypoint.name}</name>
                <Point>
                    <coordinates>${waypoint.location.longitude},${waypoint.location.latitude},0</coordinates>
                </Point>
                <description><![CDATA[${waypoint.description || ''}]]></description>
            </Placemark>
            `;
        });

        kmlContent += `</Document>\n</kml>`;

        const blob = new Blob([kmlContent], { type: 'application/vnd.google-earth.kml+xml' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${trailName}.kml`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    createDownloadButton();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址