|
(async () => { |
|
try { |
|
const input = prompt('komoot tour link or ID'); |
|
if (!input) return; |
|
const t = input.trim(); |
|
let id, share = ''; |
|
if (/^\d+$/.test(t)) { |
|
id = t; |
|
} else { |
|
const m = t.match(/komoot\.[^/]+\/(?:[a-z]{2}-[a-z]{2}\/)?tour\/(\d+)/); |
|
if (!m) return alert('Cannot parse that — paste a komoot tour link or a plain ID.'); |
|
id = m[1]; |
|
const st = (t.match(/[?&]share_token=([^&#]+)/) || [])[1]; |
|
if (st) share = '?share_token=' + st; |
|
} |
|
|
|
const api = 'https://api.komoot.de/v007/tours/'; |
|
const get = u => fetch(u).then(r => r.ok ? r.json() : Promise.reject('komoot says ' + r.status)); |
|
const [tour, coords] = await Promise.all([get(api + id + share), get(api + id + '/coordinates' + share)]); |
|
const pts = coords.items; |
|
if (!pts || pts.length < 2) return alert('No coordinates in this tour.'); |
|
|
|
const enc = vals => vals.map(v => { |
|
v = v < 0 ? ~(v << 1) : v << 1; |
|
let s = ''; |
|
while (v >= 32) { s += String.fromCharCode((32 | (v & 31)) + 63); v >>= 5; } |
|
return s + String.fromCharCode(v + 63); |
|
}).join(''); |
|
const poly = seg => { |
|
let la = 0, ln = 0, out = ''; |
|
for (const p of seg) { |
|
const a = Math.round(p.lat * 1e5), b = Math.round(p.lng * 1e5); |
|
out += enc([a - la, b - ln]); |
|
la = a; ln = b; |
|
} |
|
return out; |
|
}; |
|
const hav = (p, q) => { |
|
const r = Math.PI / 180; |
|
const h = Math.sin((q.lat - p.lat) * r / 2) ** 2 + |
|
Math.cos(p.lat * r) * Math.cos(q.lat * r) * Math.sin((q.lng - p.lng) * r / 2) ** 2; |
|
return 2 * 6371008.8 * Math.asin(Math.sqrt(h)); |
|
}; |
|
const blob = seg => { |
|
const cum = [0]; |
|
for (let i = 1; i < seg.length; i++) cum.push(cum[i - 1] + hav(seg[i - 1], seg[i])); |
|
const total = cum[cum.length - 1], N = 50, ints = []; |
|
let pd = 0, pe = 0, j = 0; |
|
for (let k = 0; k <= N; k++) { |
|
const d = total * k / N; |
|
while (j < cum.length - 2 && cum[j + 1] < d) j++; |
|
const span = cum[j + 1] - cum[j], f = span ? (d - cum[j]) / span : 0; |
|
const alt = seg[j].alt + f * (seg[j + 1].alt - seg[j].alt); |
|
const di = Math.round(d * 10), ei = Math.round(alt * 100); |
|
ints.push(di - pd, ei - pe); |
|
pd = di; pe = ei; |
|
} |
|
return { encoding: 'DrewsBadIdea', data: enc(ints) }; |
|
}; |
|
const mkleg = (seg, idx) => { |
|
let len = 0, gain = 0, loss = 0; |
|
for (let i = 1; i < seg.length; i++) { |
|
len += hav(seg[i - 1], seg[i]); |
|
const d = seg[i].alt - seg[i - 1].alt; |
|
if (d > 0) gain += d; else loss -= d; |
|
} |
|
return { |
|
legType: 'Fixed', |
|
paths: [{ |
|
length: len, elevationGain: gain, elevationLoss: loss, gradeAdjustedLength: len, |
|
pathType: 'Crow', |
|
origin: { lat: seg[0].lat, lng: seg[0].lng }, |
|
target: { lat: seg[seg.length - 1].lat, lng: seg[seg.length - 1].lng }, |
|
elevation: blob(seg), |
|
polyline: { encoding: 'Google', data: poly(seg) }, |
|
surfaceTypeOffsets: null, directions: null, |
|
}], |
|
startElement: idx, |
|
}; |
|
}; |
|
|
|
const n = pts.length, cuts = [0, Math.floor(n / 3), Math.floor(2 * n / 3), n - 1]; |
|
const elements = cuts.map(i => ({ |
|
elementType: 'Waypoint', |
|
waypoint: { point: { lat: pts[i].lat, lng: pts[i].lng }, snapUncertainty: 215.23788452148438 }, |
|
})); |
|
const legs = cuts.slice(0, -1).map((c, i) => mkleg(pts.slice(c, cuts[i + 1] + 1), i)); |
|
|
|
const html = await (await fetch('/dashboard', { credentials: 'same-origin' })).text(); |
|
const token = (html.match(/name="csrf(?:-token)?" content="([^"]+)"/) || [])[1]; |
|
const aid = (html.match(/\/athletes\/(\d+)/) || [])[1]; |
|
if (!token || !aid) return alert('Could not find your session — are you logged in?'); |
|
|
|
const payload = { props: { |
|
name: tour.name || 'komootloose route', |
|
description: '', |
|
visibility: 'Everyone', |
|
starred: false, |
|
elements, legs, |
|
routePrefs: { routeType: 'Ride', surfaceType: 'Unknown', popularity: 0.5, elevation: 0, straightLine: true }, |
|
athleteId: Number(aid), |
|
} }; |
|
|
|
const r = await fetch('/api/next/data/routes/create-route', { |
|
method: 'POST', credentials: 'same-origin', |
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': token, 'X-Requested-With': 'XMLHttpRequest' }, |
|
body: JSON.stringify(payload), |
|
}); |
|
const body = await r.text(); |
|
if (!r.ok) return alert('Create failed (' + r.status + '): ' + body.slice(0, 200)); |
|
location.assign('/routes/' + JSON.parse(body).createRoute); |
|
} catch (e) { |
|
alert('komootloose: ' + e); |
|
} |
|
})() |