Greasy Fork is available in English.
Shows you all your already unlocked regions on the Komoot world map
// ==UserScript== // @name My Komoot Regions // @name:de Meine Komoot Regionen // @description Shows you all your already unlocked regions on the Komoot world map // @description:de Zeigt dir alle deine bereits freigeschalteten Regionen auf der Komoot Weltkarte an // @namespace https://github.com/tadwohlrapp // @author Tad Wohlrapp // @version 0.1.2 // @license MIT // @homepageURL https://github.com/tadwohlrapp/my-komoot-regions // @supportURL https://github.com/tadwohlrapp/my-komoot-regions/issues // @icon https://github.com/tadwohlrapp/my-komoot-regions/raw/main/icon.png // @include https://www.komoot.com/*product/regions* // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict' unsafeWindow.komootMap = null let unlockedRegions = [] let features = [] let processedCount = 0 let getMapTries = 0 const lang = document.documentElement.lang function findObjects(object, maxTries, stopAtPrefix) { let tries = 0 const visited = [] const queue = [{ object: object, path: [], }] while (queue.length > 0) { const next = queue.shift() if (!next.object || visited.includes(next.object)) { continue } if (next.object._mapId) { return next.object } visited.push(next.object) for (const property of Object.getOwnPropertyNames(next.object)) { if (stopAtPrefix && property.startsWith(stopAtPrefix)) { return next.object[property]; } queue.push({ object: next.object[property], path: [...next.path, property], }) } if (tries++ > maxTries) { return null } } return null } function getMap() { if (unsafeWindow.komootMap) return const elements = document.getElementsByTagName('*') for (const el of elements) { if ((el.className && el.className.toString().toLowerCase().includes("map"))) { const react = findObjects(el, 5000, '__reactInternal') if (react) { const map = findObjects(react, 25000) if (map && map instanceof Object) { if (!unsafeWindow.komootMap) { console.log('Found map!') unsafeWindow.komootMap = map waitForGlobal() } break } else if (getMapTries < 10) { getMapTries++ console.log(`Looking for map... (Attempt ${getMapTries}/10)`) setTimeout(() => getMap(), 500) } } else if (getMapTries < 10) { getMapTries++ console.log(`Looking for map... (Attempt ${getMapTries}/10)`) setTimeout(() => getMap(), 500) } } } } function waitForGlobal() { unlockedRegions = [...new Map(unsafeWindow.kmtBoot.getProps().packages.models.map(region => [region.attributes.region.id, region])).values()] if (unlockedRegions) { displayHeaderText() processUnlockedRegions() } else { setTimeout(() => waitForGlobal(), 500) } } function displayHeaderText() { const unlockedText = () => { const count = unlockedRegions.length switch (lang) { case 'de': return count > 0 ? `Du hast bereits ${count === 1 ? 'eine' : count} Region${count !== 1 ? 'en' : ''} freigeschaltet.` : `Du hast noch keine Regionen freigeschaltet.` default: return count > 0 ? `You have unlocked ${count === 1 ? 'one' : count} region${count !== 1 ? 's' : ''} already.` : `You haven't unlocked any regions yet.` } } const availableText = () => { const count = unsafeWindow.kmtBoot.getProps().freeProducts.length switch (lang) { case 'de': return count > 0 ? `Du kannst noch <strong>${count === 1 ? 'eine' : count}</strong> weitere Region${count !== 1 ? 'en' : ''} kostenlos freischalten! 🎉` : `Aktuell kannst du leider keine weiteren kostenlosen Regionen freischalten.` default: return count > 0 ? `You can still unlock <strong>${count === 1 ? 'one' : count}</strong> more region${count !== 1 ? 's' : ''} for free! 🎉` : `Unfortunately, there are currently no more free regions to unlock.` } } document.querySelector('h2').innerHTML = unlockedText() + '<br>' + availableText() } function processUnlockedRegions() { const myRegionIds = unlockedRegions.map(region => region.attributes.region.id) const div = document.createElement('div') div.id = 'progress-container' div.classList.add('tw-text-xs', 'tw-px-3', 'tw-py-1', 'tw-overflow-y-auto', 'tw-bg-white-90') document.querySelector('.maplibregl-ctrl-top-left').append(div) switch (lang) { case 'de': div.append(`Verarbeite ${myRegionIds.length} freigeschaltete Regionen...`) break default: div.append(`Processing ${myRegionIds.length} unlocked regions...`) } myRegionIds.forEach(id => getGeometry(id, div)) } function getGeometry(id, div) { const totalCount = unlockedRegions.length GM_xmlhttpRequest({ method: 'GET', url: `https://www.komoot.com/product/regions?region=${id}`, data: false, headers: { "onlyprops": "true" }, responseType: 'json', onload: resp => { if (resp.response) { const { id, name, groupId: type, geometry } = resp.response.regions[0] const children = Array.from(div.children) children.forEach(child => child.classList.remove('region--active')) const p = document.createElement('p') p.classList.add('region', 'region--active') div.append(p) p.textContent = `${processedCount + 1}/${totalCount}: ${name}` buildGeoObject({ id, name, type, geometry }) processedCount++ if (processedCount === totalCount) { p.classList.remove('region--active') drawOnMap(features) switch (lang) { case 'de': div.append('Fertig 👍') break default: div.append('Done 👍') } setTimeout(() => div.remove(), 2000) } div.scrollTo(0, div.scrollHeight) } }, }) } function buildGeoObject({ id, name, type, geometry }) { const geometryArr = geometry[0] const coordinates = [] geometryArr.forEach(item => { const latLng = [] latLng.push(item.lng) latLng.push(item.lat) coordinates.push(latLng) }) const geoJson = { "type": "Feature", "properties": { "id": id, "name": name, "region": type === 1 }, "geometry": { "type": "Polygon", "coordinates": [coordinates] } } features.push(geoJson) } function drawOnMap(features) { if (!unsafeWindow.komootMap) return const geoJsonData = { "type": "FeatureCollection", "features": features } const source = unsafeWindow.komootMap.getSource('my_unlocked_regions') if (source) { source.setData(data) } else { unsafeWindow.komootMap.addSource('my_unlocked_regions', { type: 'geojson', data: geoJsonData }) } unsafeWindow.komootMap.addLayer({ 'id': 'Tad-my-regions', 'type': 'fill', 'source': 'my_unlocked_regions', 'layout': {}, 'paint': { 'fill-color': [ "case", ["boolean", ["get", "region"]], ["rgba", 16, 134, 232, 1], ["rgba", 245, 82, 94, 1] ], 'fill-opacity': 0.333 } }, "komoot-selected-marker") } function addGlobalStyle(css) { const head = document.getElementsByTagName('head')[0] if (!head) return const style = document.createElement('style') style.innerHTML = css head.append(style) } addGlobalStyle(` .maplibregl-ctrl-top-left { max-height: 100%; z-index: 110 !important; } #progress-container { line-height: 1.75; font-weight: bold; } #progress-container .region { margin: 0; font-weight: normal; } #progress-container .region.region--active { position: relative; display: flex; align-items: center; } #progress-container .region.region--active::after { content: ''; box-sizing: border-box; display: inline-flex; width: 13px; height: 13px; margin-left: 8px; border-radius: 50%; border: 2px solid transparent; border-top-color: #4f850d; border-bottom-color: #4f850d; animation: spinner .6s linear infinite; } @keyframes spinner { to {transform: rotate(360deg);} } `) getMap() })()