// ==UserScript== // @name AllTrails KML Exporter // @namespace http://tampermonkey.net/ // @version 2025-05-04 // @description This script adds a "Download KML" button to any trail on AllTrails.com, or the explorer view. In the explorer view it will download the KML for all trails currently showing. KML can be imported into Google Maps and other mapping programs. // @match https://www.alltrails.com/trail/* // @match https://www.alltrails.com/explore* // @grant GM_xmlhttpRequest // @grant GM_download // @require https://cdnjs.cloudflare.com/ajax/libs/mapbox-polyline/1.1.1/polyline.min.js // @connect alltrails.com // @license GNU GPLv3 // @downloadURL none // ==/UserScript== (function () { 'use strict'; function extractBoundingBoxFromURL() { const urlParams = new URLSearchParams(window.location.search); const b_br_lat = parseFloat(urlParams.get("b_br_lat")); const b_br_lng = parseFloat(urlParams.get("b_br_lng")); const b_tl_lat = parseFloat(urlParams.get("b_tl_lat")); const b_tl_lng = parseFloat(urlParams.get("b_tl_lng")); if ([b_br_lat, b_br_lng, b_tl_lat, b_tl_lng].some(isNaN)) return null; return { topLeft: { lat: b_tl_lat, lng: b_tl_lng }, bottomRight: { lat: b_br_lat, lng: b_br_lng }, }; } async function fetchSearchResults(location) { const body = { filters: {}, location: { mapRotation: 0, ...location }, recordTypesToReturn: ["trail"], sort: "best_match", recordAttributesToRetrieve: ["ID", "name", "slug"], resultsToInclude: ["searchResults"] }; const response = await fetch("https://www.alltrails.com/api/alltrails/explore/v1/search?", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await response.json(); return data.searchResults || []; } function fetchTrailData(trailSlug){ return new Promise((resolve, reject) => { const mapUrl = `https://www.alltrails.com/${trailSlug}`; GM_xmlhttpRequest({ method: 'GET', url: mapUrl, onload: function (response) { resolve(response.responseText); }, onerror: reject }); }); } function extractTrailDetails(trailHTML) { const parser = new DOMParser(); const doc = parser.parseFromString(trailHTML, 'text/html'); const details = { length: '', elevation: '', time: '', type: '' }; const statBlocks = doc.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(); } }); } // Extract difficulty const difficultyEl = doc.querySelector('[data-testid="trail-difficulty"]'); if (difficultyEl) { details.difficulty = difficultyEl.textContent.trim(); } return details; } function fetchTrailMapData(trailSlug) { return new Promise((resolve, reject) => { const mapSlug = trailSlug.replace('trail/', 'explore/trail/'); const mapUrl = `https://www.alltrails.com/${mapSlug}?mobileMap=false&initFlyover=true&flyoverReturnToTrail`; GM_xmlhttpRequest({ method: 'GET', url: mapUrl, onload: function (response) { const html = response.responseText; const match = html.match(/