My Komoot Regions

Shows you all your already unlocked regions on the Komoot world map

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})()