您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily download komoot tours as GPX
// ==UserScript== // @name Komoot Tour Downloader // @namespace http://tampermonkey.net/ // @version 1.0 // @description Easily download komoot tours as GPX // @author Ulysses // @match https://www.komoot.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=komoot.com // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; const downloadGPXToken = "Download GPX"; const BASIC_URL_REGEX = /https:\/\/www.komoot.([a-z]+)(\/[a-z][a-z]-[a-z][a-z])?/; const TOUR_URL_REGEX = /https:\/\/www.komoot.([a-z]+)(\/[a-z][a-z]-[a-z][a-z])?\/(tour|smarttour)\/([a-z0-9]+)/; const getId = () => { const match = window.location.href.match(TOUR_URL_REGEX); if (match === null) { return undefined; } const [, , , , id] = match; // Use the match variable directly return id; }; const getName = () => document.title.split("|")[0]; const containsGPX = () => getId() !== undefined; const getPositionsFromMainPage = async () => { const response = await fetch(window.location.href, { headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "accept-language": "es-ES,es;q=0.9,en;q=0.8,ca;q=0.7", "cache-control": "max-age=0", "sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", }, referrerPolicy: "strict-origin-when-cross-origin", body: null, method: "GET", mode: "cors", credentials: "include", }); const html = await response.text(); const regex = /kmtBoot.setProps\("(.*)"\)/m; const regexResult = regex.exec(html); const jsonText = regexResult[1].replace(/\\"/gm, '"').replace(/\\"/gm, '"'); const json = JSON.parse(jsonText); return json.page._embedded.tour._embedded.coordinates.items; }; const buildGPX = ( id, name, positions ) => `<?xml version="1.0" encoding="UTF-8"?> <gpx creator="Komoot Extension" version="1.1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/11.xsd" xmlns:ns3="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns2="http://www.garmin.com/xmlschemas/GpxExtensions/v3"> <trk> <name>${id} - ${name}</name> <type>cycling</type> <trkseg> ${positions .map( (position) => `<trkpt lat="${position[`lat`]}" lon="${position[`lng`]}"> <ele>${position[`alt`]}</ele> </trkpt>` ) .join("")} </trkseg> </trk> </gpx>`; const configureAsBubble = (container) => { container.style.background = "white"; container.style.border = "1px solid #00c300"; container.style.borderRadius = "8px"; container.style.padding = "5px 10px 5px 10px"; }; const createButton = ( document, onDownload, configureAsBubble, downloadGPXToken ) => { const button = document.createElement("button"); button.setAttribute("name", "ke-download-button"); configureAsBubble(button); button.onclick = onDownload; const buttonContent = document.createElement("div"); buttonContent.style.display = "flex"; const spinner = document.createElement("div"); spinner.innerHTML = `<svg viewBox="0 0 32 32" width="24" height="24" stroke-width="4" fill="none" stroke="currentcolor" role="img" class="css-yay0ma"> <title>loading</title> <circle cx="16" cy="16" r="12" opacity="0.125"></circle> <circle cx="16" cy="16" r="12" stroke-dasharray="75.39822368615503" stroke-dashoffset="56.548667764616276" class="css-wpcq6n"></circle> </svg>`; spinner.style.display = "none"; spinner.setAttribute("name", "ke-spinner"); buttonContent.append(spinner); const image = document.createElement("div"); image.innerHTML = `<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzY3IiBoZWlnaHQ9IjM2NyIgdmlld0JveD0iMCAwIDM2NyAzNjciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE4My41IiBjeT0iMTgzLjUiIHI9IjE4My41IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMl80KSIvPgo8cGF0aCBkPSJNODMgMTAyLjJDODMgOTEuMDkgOTIuMDkgODIgMTAzLjIgODJIMjY0LjhDMjcwLjE1NyA4MiAyNzUuMjk1IDg0LjEyODIgMjc5LjA4NCA4Ny45MTY0QzI4Mi44NzIgOTEuNzA0NyAyODUgOTYuODQyNiAyODUgMTAyLjJWMjYzLjhDMjg1IDI2OS4xNTcgMjgyLjg3MiAyNzQuMjk1IDI3OS4wODQgMjc4LjA4NEMyNzUuMjk1IDI4MS44NzIgMjcwLjE1NyAyODQgMjY0LjggMjg0SDEwMy4yQzk3Ljg0MjYgMjg0IDkyLjcwNDcgMjgxLjg3MiA4OC45MTY0IDI3OC4wODRDODUuMTI4MiAyNzQuMjk1IDgzIDI2OS4xNTcgODMgMjYzLjhWMTAyLjJaTTIyNC40IDIyMy40SDI2NC44VjEwMi4ySDEwMy4yVjIyMy40SDE0My42QzE0My42IDIzNC41MSAxNTIuNjkgMjQzLjYgMTYzLjggMjQzLjZIMjA0LjJDMjA5LjU1NyAyNDMuNiAyMTQuNjk1IDI0MS40NzIgMjE4LjQ4NCAyMzcuNjg0QzIyMi4yNzIgMjMzLjg5NSAyMjQuNCAyMjguNzU3IDIyNC40IDIyMy40Wk0xNzMuOSAxNjIuOFYxMzIuNUgxOTQuMVYxNjIuOEgyMjQuNEwxODQgMjAzLjJMMTQzLjYgMTYyLjhIMTczLjlaIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzJfNCIgeDE9IjE4My41IiB5MT0iMCIgeDI9IjE4My41IiB5Mj0iMzY3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM4RkNFM0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjRBMzIyIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg=="/>`; image.style.width = "20px"; image.setAttribute("name", "ke-logo"); buttonContent.append(image); const text = document.createElement("span"); text.style.marginLeft = "8px"; text.innerText = downloadGPXToken; buttonContent.append(text); button.append(buttonContent); return button; }; const onDownload = async () => { const id = getId(); const name = getName(); document.getElementsByName("ke-download-button").forEach((element) => { element.disabled = true; }); document.getElementsByName("ke-logo").forEach((element) => { element.style.display = "none"; }); document.getElementsByName("ke-spinner").forEach((element) => { element.style.display = ""; }); const positions = await getPositionsFromMainPage(id); const gpx = buildGPX(id, name, positions); const element = document.createElement("a"); element.setAttribute( "href", "data:application/gpx+xml;charset=utf-8," + encodeURIComponent(gpx) ); element.setAttribute("download", id + ".gpx"); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); document.getElementsByName("ke-download-button").forEach((element) => { element.disabled = false; }); document.getElementsByName("ke-logo").forEach((element) => { element.style.display = ""; }); document.getElementsByName("ke-spinner").forEach((element) => { element.style.display = "none"; }); }; const run = () => { var observer = new MutationObserver(function (mutations) { const shouldAddButton = containsGPX(); const downloadButtons = document.getElementsByName("ke-download-button"); const isButtonAlreadyAdded = downloadButtons.length > 0; if (shouldAddButton && isButtonAlreadyAdded) return; if (!shouldAddButton && isButtonAlreadyAdded) { // Remove button downloadButtons.forEach((button) => button.remove()); } else if (shouldAddButton && !isButtonAlreadyAdded) { // Add button const body = document.querySelector("body"); const target = body; const container = document.createElement("div"); container.style.position = "absolute"; container.style.top = "60px"; container.style.right = "20px"; container.style.display = "flex"; container.style.flexDirection = "column"; container.style.alignItems = "end"; const button = createButton(document, onDownload, configureAsBubble, downloadGPXToken); container.appendChild(button); target.appendChild(container); } }); observer.observe(document, { childList: true, subtree: true, // needed if the node you're targeting is not the direct parent }); }; run(); })();