// ==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()
})()