├── Readme.md ├── komoot_regions.png ├── render_unlocked_komoot_regions.user.js └── view.js /Readme.md: -------------------------------------------------------------------------------- 1 | # Komoot unlocked regions viewer 2 | 3 | A small utility to display all unlocked regions of your komoot account. 4 | 5 | ![Screenshot](komoot_regions.png) 6 | 7 | ## Usage 8 | 9 | 1. Sign into your kommot account and navigate to https://www.komoot.com/product/regions 10 | 2. Open the developer console (`F12`) 11 | 3. Paste the content of `view.js` into the console and execute it 12 | 13 | Alternatively you can use the provided userscript. 14 | -------------------------------------------------------------------------------- /komoot_regions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahz/komoot-unlocked-regions/4b6c47a49485caa9a6afff8f12321965ebf2fb86/komoot_regions.png -------------------------------------------------------------------------------- /render_unlocked_komoot_regions.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Render Unlocked Komoot Regions 3 | // @namespace http://tampermonkey.net/ 4 | // @version 0.2 5 | // @include /^https?://www\.komoot\.(.*)/product/regions(.*) 6 | // @grant none 7 | // ==/UserScript== 8 | 9 | 10 | function GM_main () { 11 | window.onload = function () { 12 | 13 | function loadScriptFromGithub() { 14 | const http = new XMLHttpRequest(); 15 | http.open("GET", "https://raw.githubusercontent.com/cahz/komoot-unlocked-regions/master/view.js", true); 16 | http.onreadystatechange = function() { 17 | console.log(http.status); 18 | if(http.readyState == 4 && http.status == 200) { 19 | const d = document; 20 | const scriptNode = d.createElement ('script'); 21 | scriptNode.type = "text/javascript"; 22 | scriptNode.textContent = http.responseText; 23 | 24 | const eHead = d.getElementsByTagName('head')[0] || d.body || d.documentElement; 25 | eHead.appendChild (scriptNode); 26 | } 27 | } 28 | http.send(); 29 | } 30 | 31 | // run loadScriptFromGithub after 1sec 32 | setTimeout(loadScriptFromGithub, 1000); 33 | } 34 | } 35 | 36 | const d = document; 37 | const scriptNode = d.createElement ('script'); 38 | scriptNode.type = "text/javascript"; 39 | scriptNode.textContent = '(' + GM_main.toString() + ')()'; 40 | 41 | const eHead = d.getElementsByTagName('head')[0] || d.body || d.documentElement; 42 | eHead.appendChild (scriptNode); 43 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | // get list of unlocked regions from komoot internal API 2 | var regions = kmtBoot.getProps().packages.models.map((p) => { 3 | if (p.attributes.region === undefined) {return 9999;} 4 | return p.attributes.region.id 5 | }); 6 | 7 | function loadScripts(srcs, callback={}){ 8 | if(srcs.length == 0){ 9 | callback(); 10 | return; 11 | } 12 | let script = document.createElement('script'); 13 | script.src = srcs.shift(); 14 | document.getElementsByTagName('head')[0].appendChild(script); 15 | script.onload = () => {loadScripts(srcs, callback)}; 16 | } 17 | 18 | // Load Maplibre style 19 | let style = document.createElement('link'); 20 | style.rel = "stylesheet"; 21 | style.href = "https://unpkg.com/maplibre-gl/dist/maplibre-gl.css"; 22 | document.getElementsByTagName('head')[0].appendChild(style); 23 | 24 | // load scripts: Maplibre 25 | loadScripts(["https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"], () => { 26 | let mapelem = document.createElement('div'); 27 | mapelem.id = 'mapid'; 28 | mapelem.style.height = document.getElementsByClassName("c-inline-map__container")[0].getBoundingClientRect().height + "px" 29 | document.getElementsByClassName("c-inline-map__container")[0].parentNode.parentNode.appendChild(mapelem); 30 | 31 | let map = new maplibregl.Map({ 32 | container: 'mapid', 33 | style: 'https://tiles-api.maps.komoot.net/v1/style.json?optimize=true', 34 | attributionControl: false, 35 | }); 36 | 37 | // Colors as used by komoot for region bundle (red) and single region (blue) 38 | let regionColor = [ "case", [ "boolean", [ "get", "region" ] ], [ "rgba", 16, 134, 232, 1 ], [ "rgba", 245, 82, 94, 1 ] ]; 39 | 40 | map.once("load", () => { 41 | for(let key in map.style?.sourceCaches){ // Remove komoot's wrong osm attribution 42 | if(map.style.sourceCaches[key]?._source?.attribution){ 43 | map.style.sourceCaches[key]._source.attribution = ""; 44 | } 45 | } 46 | 47 | map.addControl(new maplibregl.AttributionControl({ 48 | customAttribution: ['Maplibre', 49 | '© komoot', 50 | 'Map data © OpenStreetMap contributors'] 51 | })) 52 | map.addControl(new maplibregl.NavigationControl()); 53 | map.addControl(new maplibregl.GeolocateControl()); 54 | 55 | map.setLayoutProperty("komoot-region", 'visibility', 'visible'); // Enable the region layer 56 | map.addLayer({ 57 | id: 'custom-region-text', 58 | source: 'komoot_region', 59 | type: 'symbol', 60 | minzoom: 6, 61 | layout: { 62 | 'text-field': ['get', 'name'], 63 | 'text-font': ['Noto Sans Bold'], 64 | }, 65 | paint: { 66 | 'text-color': regionColor, 67 | } 68 | }); 69 | map.addLayer({ 70 | id: 'custom-region-polygon', 71 | source: 'komoot_region', 72 | type: 'fill', 73 | paint: { 74 | 'fill-color': regionColor, 75 | 'fill-opacity': 0.3, 76 | } 77 | },"custom-region-text"); // Draw polygons *under* text layer 78 | 79 | // Prepare for initial zoom 80 | let allbounds = new maplibregl.LngLatBounds(); 81 | 82 | // iterate over unlocked regions and draw the result 83 | regions.forEach(async (id) => { 84 | if (id == 9999) {return} 85 | 86 | // load regions and add them as geojson features 87 | fetch("?region="+id, {headers: {'onlyprops': 'true'}}) 88 | .then(res => res.json()) 89 | .then(json => { 90 | let counter = 0; 91 | json.regions[0].geometry.forEach(p => { 92 | counter++; 93 | let region = []; 94 | p.forEach((i) => { 95 | region.push([i.lng, i.lat]); 96 | allbounds.extend([i.lng, i.lat]); 97 | }); 98 | 99 | map.getSource('komoot_region').updateData({ 100 | add: [{ 101 | type: "Feature", 102 | id: id+"p"+counter, 103 | geometry: { 104 | type: "Polygon", 105 | coordinates: [region], 106 | }, 107 | properties: { 108 | region: json.regions[0].groupId==1, 109 | name: json.regions[0].name, 110 | }, 111 | }] 112 | }); 113 | }); 114 | map.fitBounds(allbounds, {padding: 20}); // Zoom in 115 | }); 116 | }); 117 | }); 118 | 119 | map.on('click', 'custom-region-polygon', (e) => { 120 | const coordinates = e.features[0].geometry.coordinates[0]; 121 | const bounds = coordinates.reduce((bounds, coord) => { 122 | return bounds.extend(coord); 123 | }, new maplibregl.LngLatBounds(coordinates[0], coordinates[0])); 124 | 125 | map.fitBounds(bounds, { 126 | padding: 20 127 | }); 128 | }); 129 | 130 | // Change the cursor to a pointer when the mouse is over the region layer. 131 | map.on('mouseenter', 'custom-region-polygon', () => { map.getCanvas().style.cursor = 'pointer'; }); 132 | map.on('mouseleave', 'custom-region-polygon', () => { map.getCanvas().style.cursor = ''; }); 133 | 134 | }); 135 | 136 | 137 | availableRegions = kmtBoot.getProps().freeProducts.length; 138 | if(availableRegions>0){ 139 | let elem = document.createElement('span'); 140 | elem.innerHTML = "You have "+availableRegions+" free region"+(availableRegions!=1?"s":"")+" available!"; 141 | document.getElementsByClassName("c-inline-map__container")[0].parentNode.parentNode.parentNode.appendChild(elem); 142 | } 143 | 144 | console.log("You have %d free region(s) available!", kmtBoot.getProps().freeProducts.length); 145 | --------------------------------------------------------------------------------