您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
map visualization of your geoguessr activities
当前为
// ==UserScript== // @name Geoguessr Activities Analysis Tool // @version 1.50 // @description map visualization of your geoguessr activities // @author KaKa // @match https://map-making.app/ // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @license MIT // @icon https://www.svgrepo.com/show/191885/map.svg // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_addStyle // @namespace http://tampermonkey.net/ // ==/UserScript== (function() { 'use strict'; let pinSvg=`<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><title>70 Basic icons by Xicons.co</title><path d="M24,1.32c-9.92,0-18,7.8-18,17.38A16.83,16.83,0,0,0,9.57,29.09l12.84,16.8a2,2,0,0,0,3.18,0l12.84-16.8A16.84,16.84,0,0,0,42,18.7C42,9.12,33.92,1.32,24,1.32Z" fill="#a9a3a2"></path><path d="M25.37,12.13a7,7,0,1,0,5.5,5.5A7,7,0,0,0,25.37,12.13Z" fill="#f3edec"></path></g></svg>` let shareSvg=`<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="22px" height="22px" stroke="#ffffff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M21 9.00001L21 3.00001M21 3.00001H15M21 3.00001L12 12M10 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H16.2C17.8802 21 18.7202 21 19.362 20.673C19.9265 20.3854 20.3854 19.9265 20.673 19.362C21 18.7202 21 17.8802 21 16.2V14" stroke="#d3cfcf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>` let linkSvg=`<svg fill="#d3cfcf" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="29px" height="29px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve" stroke="#d3cfcf"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M78.746,33.028L61.62,17.213c-0.368-0.336-0.85-0.713-1.349-0.713H42.634c-5.407,0-9.134,2.81-9.134,6.877V28.5h-3.358 c-4.946,0-8.642,3.124-8.642,7.369V75.87c0,4.492,4.077,7.63,8.642,7.63h29.231c4.677,0,8.127-3.288,8.127-7.63V71.5h4.365 c4.41,0,7.635-3.513,7.635-8.122V35.055c0-0.029,0.242-0.057,0.241-0.085c0.001-0.03,0.134-0.059,0.134-0.089 C79.875,34.041,79.48,33.324,78.746,33.028z M61.5,23.165L72.649,32.5H61.5V23.165z M63.5,75.87c0,2.363-1.938,3.63-4.127,3.63 H30.142c-2.323,0-4.642-1.381-4.642-3.63V35.869c0-2.6,3.095-3.369,4.642-3.369H45.5v15.381c0,1.104,1.396,1.619,2.5,1.619h15.5 V75.87z M61.216,45.5H49.5v-9.878L61.216,45.5z M71.865,67.5H67.5V47.547c0-0.562,0.014-1.097-0.4-1.476l-17.43-16.059 c-0.324-0.667-0.94-1.132-1.732-1.132c-0.036,0-0.039-0.182-0.075-0.18c-0.038-0.002-0.044-0.201-0.083-0.201H37.5v-5.123 c0-2.063,3.02-2.877,5.134-2.877H57.5v14.381c0,1.104,1.396,1.619,2.5,1.619h15.5v26.878C75.5,65.776,74.068,67.5,71.865,67.5z"></path> </g> </g></svg>` let loadingSvg=`<svg fill="#d3cfcf" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="22px" height="22px" viewBox="0 0 330 330" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="XMLID_19_"> <path id="XMLID_20_" d="M307.5,150h-60c-8.284,0-15,6.716-15,15s6.716,15,15,15h60c8.284,0,15-6.716,15-15S315.784,150,307.5,150z"></path> <path id="XMLID_21_" d="M52.5,150h-30c-8.284,0-15,6.716-15,15s6.716,15,15,15h30c8.284,0,15-6.716,15-15S60.784,150,52.5,150z"></path> <path id="XMLID_22_" d="M157.5,240c-8.284,0-15,6.716-15,15v60c0,8.284,6.716,15,15,15c8.284,0,15-6.716,15-15v-60 C172.5,246.716,165.784,240,157.5,240z"></path> <path id="XMLID_23_" d="M157.5,0c-8.284,0-15,6.716-15,15v60c0,8.284,6.716,15,15,15c8.284,0,15-6.716,15-15V15 C172.5,6.716,165.784,0,157.5,0z"></path> <path id="XMLID_24_" d="M231.746,218.033c-5.857-5.858-15.355-5.858-21.213,0c-5.858,5.858-5.858,15.355,0,21.213l42.427,42.427 c2.929,2.929,6.767,4.393,10.606,4.393s7.678-1.464,10.607-4.393c5.858-5.858,5.858-15.355,0-21.213L231.746,218.033z"></path> <path id="XMLID_25_" d="M62.04,48.328c-5.857-5.857-15.355-5.858-21.213,0c-5.858,5.858-5.858,15.356,0,21.213l42.427,42.426 c2.929,2.929,6.768,4.393,10.606,4.393c3.839,0,7.678-1.464,10.607-4.394c5.857-5.858,5.857-15.355,0-21.213L62.04,48.328z"></path> <path id="XMLID_26_" d="M221.14,116.36c3.838,0,7.678-1.465,10.607-4.393l42.427-42.426c5.858-5.858,5.858-15.355,0-21.213 c-5.859-5.858-15.355-5.857-21.213,0l-42.427,42.426c-5.858,5.858-5.858,15.355,0,21.213 C213.462,114.896,217.3,116.36,221.14,116.36z"></path> </g> </g></svg>` let successSvg=`<svg viewBox="-25.6 -25.6 563.20 563.20" xmlns="http://www.w3.org/2000/svg" width="22px" height="22px" fill="#000000" stroke="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <defs> <style>.cls-1{fill:none;stroke:#d3cfcf;stroke-linecap:round;stroke-linejoin:round;stroke-width:35.839999999999996;}</style> </defs> <g data-name="Layer 2" id="Layer_2"> <g data-name="E408, Success, Media, media player, multimedia" id="E408_Success_Media_media_player_multimedia"> <circle class="cls-1" cx="256" cy="256" r="246"></circle> <polyline class="cls-1" points="115.54 268.77 200.67 353.9 396.46 158.1"></polyline> </g> </g> </g></svg>` const svgBlob = new Blob([pinSvg], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); var u = 'https://www.geoguessr.com/api/v4/feed/friends?count=26'; var map,heatmapLayer,streetViewContainer,streetViewMap let myNick let activities =JSON.parse(localStorage.getItem('activities')); let isUpdated=JSON.parse(localStorage.getItem('isUpdated')); const fontAwesomeCDN = document.createElement('link'); fontAwesomeCDN.rel = 'stylesheet'; fontAwesomeCDN.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'; document.head.appendChild(fontAwesomeCDN); getMyNick().then(nick => { myNick = nick; }) .catch(error => { console.error("Failed to fetch user nick:", error); }); if (!activities){ activities={'duels':{},'games':{}}; } function getMyNick() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://geoguessr.com/api/v3/profiles", onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); const myId=data.user.id const nickMap={} nickMap[myId]=data.user.nick resolve(nickMap); } else { console.error("Error fetching user data: " + response.statusText); reject(null); } }, onerror: function(error) { console.error("Error fetching user data:", error); reject(null); } }); }); } function matchPlayerNick(gameData, friendMap) { gameData.forEach(game => { const playerId = game.playerId; Object.assign(friendMap,myNick) game.playerNick = friendMap[playerId] || "Opponents"; }); return gameData; } function getFriends() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://www.geoguessr.com/api/v3/social/friends/summary?page=0&fast=true", onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const friendMap = {}; data.friends.forEach(friend => { friendMap[friend.userId] = friend.nick; }); resolve(friendMap); } catch (error) { console.error("Error parsing JSON: ", error); reject(null); } } else { console.error("Error fetching user data: " + response.statusText); reject(null); } }, onerror: function(error) { console.error("Error fetching user data:", error); reject(null); } }); }); } function getDiffdays(dataList) { const currentDate = new Date(); return dataList.map(item => { if ('gameDate' in item) { const gameDate = new Date(item.gameDate); const timeDiff = Math.abs(currentDate - gameDate); const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); item.gameDate = daysDiff; } return item; }); } function saveLocalStorage(item) { localStorage.setItem('activities', JSON.stringify(item)); activities =JSON.parse(localStorage.getItem('activities')); localStorage.setItem('isUpdated',JSON.stringify('updated')); } function downloadJSON(coords,format) { function processData(coords) { return new Promise((resolve, reject) => { var convertedData = { "name": "coordinates", "customCoordinates": coords.map(function(item) { var scoreCategory; if (item.score >= 0 && item.score <= 1499) { scoreCategory = "0-1499"; } else if (item.score >= 1500 && item.score <= 2999) { scoreCategory = "1500-2999"; } else if (item.score >= 3000 && item.score <= 4499) { scoreCategory = "3000-4499"; } else if (item.score >= 4500 && item.score <= 4999) { scoreCategory = "4500-4999"; } else { scoreCategory = "5000"; } var tags = []; if (item.gameMode !== null) { tags.push(item.gameMode); } if (item.playerNick !== null) { tags.push(item.playerNick); } if (item.forbidOptions !== null) { tags.push(item.forbidOptions); } if (item.usingMap !== null) { tags.push(item.usingMap); } tags.push(scoreCategory); return { "lat": item.latLng.lat, "lng": item.latLng.lng, "heading": item.heading, "pitch": item.pitch, "zoom": item.zoom, "panoId": null, "countryCode": null, "stateCode": null, "extra": { "tags": tags } }; }) }; resolve(convertedData); }); } processData(coords) .then((convertedData) => { if (format) { GM_setClipboard(JSON.stringify(convertedData)); } else{ var originalJson = JSON.stringify(coords, null, 2); var originalBlob = new Blob([originalJson], { type: 'application/json' }); var originalUrl = URL.createObjectURL(originalBlob); var originalLink = document.createElement('a'); originalLink.href = originalUrl; originalLink.download = 'original_data.json'; document.body.appendChild(originalLink); originalLink.click(); document.body.removeChild(originalLink); var convertedJson = JSON.stringify(convertedData, null, 2); var convertedBlob = new Blob([convertedJson], { type: 'application/json' }); var convertedUrl = URL.createObjectURL(convertedBlob); var convertedLink = document.createElement('a'); convertedLink.href = convertedUrl; convertedLink.download = 'map-making_data.json'; document.body.appendChild(convertedLink); convertedLink.click(); document.body.removeChild(convertedLink);} }) .catch((error) => { console.error("处理数据时发生错误:", error); }); } function clearLocalStorage() { localStorage.removeItem('activities'); localStorage.removeItem('isUpdated'); } function loadScript(url) { return new Promise((resolve, reject) => { var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; script.async = true; script.onload = resolve; script.onerror = reject; document.body.appendChild(script); }); } function loadGoogleMapsAPI(apiKey, libraries) { var librariesParam = libraries ? '&libraries=' + libraries.join(',') : ''; var url = 'https://maps.googleapis.com/maps/api/js?key=' + apiKey + librariesParam; return new Promise((resolve, reject) => { if (typeof google === 'undefined' || typeof google.maps === 'undefined') { loadScript(url) .then(resolve) .catch(() => { reject(new Error('Failed to load Google Maps JavaScript API')); }); } else { resolve(); } }); } function createHeatmap(coordinates) { let heatmapData=[] let filteredData let marker let markers = []; function createMap() { var css = ` #downloadButton { position:absolute; border-radius:1px; width:40px; height:40px; padding: 10px; border: none; background-color: #f0f0f0; background-image: url('https://www.svgrepo.com/show/177647/clipboard-list.svg'); background-repeat: no-repeat; background-position: 5px; background-size: 30px auto; position: relative; transition: background-color 0.3s ease; cursor: pointer; opacity:0.8; } #downloadButton:hover { background-color: #f0f0f0; opacity:1 } #downloadButton::after { position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 5px; border-radius: 5px; font-size: 11px; line-height: 1; height: auto; white-space: nowrap; opacity: 0; transition: opacity 0.3s ease; } #downloadButton:hover::after { content: "Paste JSON data to your clipboard"; opacity: 0.8; } #map-container { position: fixed; bottom: 0px; left: 40%; transform: translateX(-50%); width: 800px; height: 600px; z-index: 1; } .control-panel { width: 120px; height: 30px; background-color: #fff; cursor: pointer; text-align: center; line-height: 30px; } .control-panel .select-container { display: none; } .control-panel:hover .select-container { display: block; } .control-panel select { width: 100%; padding: 5px; border: 1px solid #ccc; border-radius: 5px; background-color: #fff; cursor: pointer; outline: none; } `; var style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); const mapContainer = document.createElement('div'); mapContainer.id = 'map-container'; document.body.appendChild(mapContainer); var downloadButton = document.createElement('button'); downloadButton.id = 'downloadButton' map = new google.maps.Map(mapContainer, { zoom: 2, center: { lat: 35.77, lng: 139.76 }, mapTypeId: 'roadmap', gestureHandling: 'greedy', disableDefaultUI: true, streetViewControl: true }); const controlDiv = document.createElement('div'); controlDiv.className = 'control-panel'; map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv); map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(downloadButton); controlDiv.classList.add('hoverable'); controlDiv.addEventListener('mouseenter', () => { selectContainer.style.display = 'block'; controlDiv.style.height = '380px'; }); controlDiv.addEventListener('mouseleave', () => { selectContainer.style.display = 'none'; controlDiv.style.height = '30px'; }); const layerCheckbox = document.createElement('input'); layerCheckbox.type = 'checkbox'; layerCheckbox.id = 'scatter' const label = document.createElement('label'); label.htmlFor = 'scatter'; label.textContent = 'Draw Scatter'; label.style.color='#000' label.style.fontSize='16px' controlDiv.appendChild(layerCheckbox) controlDiv.appendChild(label); const keysToFilter = ['playerNick', 'gameMode', 'forbidOptions', 'usingMap', 'country', 'score', 'distance', 'guessSeconds', 'gameDate']; const optionValues = {}; let filters = []; let players=[]; const selectContainer = document.createElement('div'); selectContainer.className = 'select-container'; controlDiv.appendChild(selectContainer); keysToFilter.forEach(key => { const select = document.createElement('select'); select.setAttribute('name', key); select.style.marginBottom = '15px'; const defaultOption = document.createElement('option'); defaultOption.setAttribute('value', ''); defaultOption.textContent = `${key}`; select.appendChild(defaultOption); if (key === 'score' || key === 'distance' || key === 'guessSeconds') { const ranges = key === 'score' ? [ { min: 0, max: 1499 }, { min: 1500, max: 2999 }, { min: 3000, max: 3999 }, { min: 4000, max: 4499 }, { min: 4500, max: 4999 }, { min: 5000 } ] : key === 'distance' ? [ { max: 0.5 }, { min: 0.5, max: 5 }, { min: 5, max: 100 }, { min: 100, max: 1000 }, { min: 1000, max: 5000 }, { min: 5000 } ] : [ { max: 5 }, { min: 5, max: 15 }, { min: 15, max: 30 }, { min: 30, max: 60 }, { min: 60, max: 300 }, { min: 300 } ]; for (let range of ranges) { const option = document.createElement('option'); let label = range.max === undefined ? (key === 'score' ? '5000' : key === 'distance' ? '>5000km' : '>300s') : (key === 'score' ? `${range.min}-${range.max}` : key === 'distance' ? `${range.min}-${range.max}km` : `${range.min}-${range.max}s`); if (range.min==undefined&&key==='distance'){ label='<0.5km'; } if (range.min==undefined&&key==='guessSeconds'){ label='<5s'; } option.setAttribute('value', label); option.textContent = label; select.appendChild(option); } } else if (key === 'gameDate') { const dateRanges = [ { label: 'More than 1 month', min:30, max: 1000 }, { label: 'More than 15 days', min: 15, max: 30 }, { label: 'More than 1 week', min: 7, max: 15 }, { label: 'More than 1 day', min: 1, max: 7 }, { label: 'Within 24 hours', min: 0, max: 1 } ]; dateRanges.forEach(dateRange => { const option = document.createElement('option'); option.setAttribute('value', `${dateRange.min}-${dateRange.max}` ); option.textContent = dateRange.label; select.appendChild(option); }); } else { const optionCounts = {}; coordinates.forEach(item => { const value = item[key]; optionCounts[value] = (optionCounts[value] || 0) + 1; }); const sortedOptions = Object.keys(optionCounts).sort((a, b) => optionCounts[b] - optionCounts[a]); sortedOptions.forEach(value => { if (!optionValues[value]) { optionValues[value] = true; const option = document.createElement('option'); option.setAttribute('value', value); if (key === 'playerNick') { const myKey = Object.keys(myNick); option.textContent = value; if (value == myNick[myKey]) { option.textContent = `${value}(me)`; option.style.color = 'red'; } } else { option.textContent = value; } select.appendChild(option); } }); } selectContainer.appendChild(select); select.addEventListener('change', () => { const selectedValue = select.value; filters[key] = selectedValue; let filteredData = coordinates Object.keys(filters).forEach(filterKey => { const filterValue = filters[filterKey]; if (filterValue) { if (filterValue.includes('-')) { filteredData = filteredData.filter(item => { if (filterKey=='gameDate'){ const [minDays, maxDays] = filterValue.split('-') const itemValue = parseFloat(item[filterKey]) return itemValue >=minDays && itemValue <maxDays; } else{ const [minValue, maxValue] = filterValue.split('-').map(val => parseFloat(val)); const itemValue = parseFloat(item[filterKey]); return itemValue >= minValue && itemValue <= maxValue;} }); } else if (filterValue.includes('>') || filterValue.includes('<')) { const operator = filterValue.includes('>') ? '>' : '<'; const value = parseFloat(filterValue.substring(1)); filteredData = filteredData.filter(item => { const itemValue = parseFloat(item[filterKey]); return operator === '>' ? itemValue > value : itemValue < value; }); } else if (filterValue.includes('5000')) { filteredData = filteredData.filter(item => { const itemValue = parseFloat(item[filterKey]); return itemValue === 5000; });} else { filteredData = filteredData.filter(item => item[filterKey] === filterValue); } } }); refreshHeatmap(filteredData, 'score'); downloadButton.addEventListener('click', function() { if (filteredData){downloadJSON(filteredData,'mm')} else{downloadJSON(coordinates,'mm')} setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177648/clipboard-list.svg')"; }, 300); setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177640/clipboard-list.svg')"; }, 700); setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177647/clipboard-list.svg')"; }, 1000); }); }); }); let checkedMarker=null function handleMarkerClick(marker, coord) { return function() { createStreetViewContainer(coord.location.lat(), coord.location.lng(), coord.heading, coord.pitch, coord.zoom); if (checkedMarker && checkedMarker !== marker) { checkedMarker.setIcon({ url: svgUrl, fillOpacity: 0.8, scaledSize: new google.maps.Size(25, 25) }); } marker.setIcon({ url: "https://www.svgrepo.com/show/313155/pin.svg", scaledSize: new google.maps.Size(25, 25) }); checkedMarker = marker; var closeListen = document.querySelector('#close'); closeListen.addEventListener('click', function() { marker.setIcon({ url: svgUrl, fillOpacity: 0.8, scaledSize: new google.maps.Size(25, 25) }); }); }; } layerCheckbox.addEventListener('change', (event) => { if (heatmapData.length==0){ for (const coord of coordinates) { heatmapData.push({ location: new google.maps.LatLng(coord.latLng.lat, coord.latLng.lng), weight: coord.score, player:coord.playerNick, forbidOptions:coord.forbidOptions, heading:coord.heading, pitch:coord.pitch, zoom:coord.zoom }); } } if (event.target.checked) { let existingMarkers = [] for (const coord of heatmapData) { let markerExists = existingMarkers.some(existingPosition => { return existingPosition.equals(coord.location); }); if (!markerExists) { const marker = new google.maps.Marker({ position: new google.maps.LatLng(coord.location), map: map, title: coord.player+' : ' + coord.weight+'('+coord.forbidOptions+')', icon: {url:svgUrl, fillOpacity: 0.8, scaledSize:new google.maps.Size(24,24) } }); existingMarkers.push(coord.location) marker.addListener('click', handleMarkerClick(marker, coord)); markers.push(marker); } } } else { markers.forEach(m => m.setMap(null)); markers = []; } }) downloadButton.addEventListener('click', function() { downloadJSON(coordinates,'mm') setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177648/clipboard-list.svg')"; }, 300); setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177640/clipboard-list.svg')"; }, 700); setTimeout(function() { downloadButton.style.backgroundImage = "url('https://www.svgrepo.com/show/177647/clipboard-list.svg')"; }, 1000); }); } function refreshHeatmap(fd,k) { if (fd.length === 0) { console.error('No valid coordinates data found'); } heatmapData=[] for (const coord of fd) { heatmapData.push({ location: new google.maps.LatLng(coord.latLng.lat, coord.latLng.lng), weight: coord[k] }); } heatmapLayer.setData(heatmapData); if (markers.length!=0){ markers.forEach(marker => marker.setMap(null)); markers = []; } } if (!map) { createMap(); getDefault(coordinates) } } function getDefault(coordinates){ var h=[]; if (heatmapLayer) { heatmapLayer.setMap(null); } for (const coord of coordinates) { h.push({ location: new google.maps.LatLng(coord.latLng.lat, coord.latLng.lng), weight: coord.score, heading:coord.heading, pitch:coord.pitch, zoom:coord.zoom }); } heatmapLayer = new google.maps.visualization.HeatmapLayer({ data: h, dissipating: true, map: map, });} function getStatics(url, result, maxPages,currentPage = 1, pageToken = null) { if (currentPage==1){ const swal = Swal.fire({ title: 'Fetching Activities', text: 'Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); } if (currentPage > maxPages) { console.log(`Reached maximum number of pages (${maxPages}). Stopping requests.`); swal.close() Swal.fire('Success', 'All activities retrieved successfully!', 'success'); saveLocalStorage(result) getCoords(result); return; } let nextPageUrl = url; if (pageToken) { nextPageUrl += `&paginationToken=${encodeURIComponent(pageToken)}`; } GM_xmlhttpRequest({ method: "GET", url: nextPageUrl, onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); processActivities(data, result); const nextPageToken = data.paginationToken; if (nextPageToken) { getStatics(url, result, maxPages, currentPage + 1, nextPageToken); } else { Swal.fire('Success', 'All activities retrieved successfully!', 'success'); saveLocalStorage(result) getCoords(result); } } else { console.error('Request failed: ' + response.statusText); Swal.fire('Error', 'Failed to fetch activities. Please try again later.', 'error'); } }, onerror: function(response) { console.error('Request failed: ' + response.statusText); Swal.fire('Error', 'Failed to fetch activities. Please try again later.', 'error'); } }); } function processActivities(data, result) { const entries = data.entries; if (entries && entries.length > 0) { entries.forEach(entry => { if (entry.payload) { const payloadList = JSON.parse(entry.payload); const userId = entry.user.id; if (!Array.isArray(payloadList)) { processPayload(payloadList, userId, result); } else { payloadList.forEach(payload => { processPayload(payload, userId, result); }); } } }); } else { console.error('Data not found!'); } } function processPayload(payload, userId, result) { if (payload.gameToken) { result.games[userId] = result.games[userId] || []; if (!result.games[userId].includes(payload.gameToken)) { result.games[userId].push(payload.gameToken); } } else if (payload.gameId) { result.duels[userId] = result.duels[userId] || []; if (!result.duels[userId].includes(payload.gameId)) { result.duels[userId].push(payload.gameId); } } } async function getCoords(data) { try { var coordinates = []; const duelsPromises = []; const gamesPromises = []; const chunkSize = 20; const swal = Swal.fire({ title: 'Fetching Coordinates', text: 'Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); for (const gameIds of Object.values(data.duels)) { for (let i = 0; i < gameIds.length; i += chunkSize) { const chunk = gameIds.slice(i, i + chunkSize); const chunkPromises = chunk.map(gameId => { const requestUrl = `https://game-server.geoguessr.com/api/duels/${gameId}`; return getGameSummary(requestUrl, 'duels', coordinates); }); duelsPromises.push(Promise.allSettled(chunkPromises)); } } for (const gameIds of Object.values(data.games)) { for (let i = 0; i < gameIds.length; i += chunkSize) { const chunk = gameIds.slice(i, i + chunkSize); const chunkPromises = chunk.map(gameId => { const requestUrl = `https://www.geoguessr.com/api/v3/games/${gameId}?client=web`; return getGameSummary(requestUrl, 'games', coordinates); }); gamesPromises.push(Promise.allSettled(chunkPromises)); } } await Promise.all([...duelsPromises, ...gamesPromises]); swal.close(); try { const friends = await getFriends(); var matchedData = matchPlayerNick(coordinates, friends); await downloadJSON(matchedData); matchedData=getDiffdays(matchedData) createHeatmap(matchedData); Swal.fire('Success', 'Heatmap is prepared!', 'success'); } catch (error) { console.error("Error:", error); Swal.fire('Error', 'Failed to prepare heatmap', 'error'); } } catch (error) { Swal.fire('Error', 'Error parsing JSON from localStorage', 'error'); console.error('Error parsing JSON from localStorage:', error); } } async function getGameSummary(url, mode, coordinates) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { try {let forbidOptions const data = JSON.parse(response.responseText); if(data.teams||data.player){ if (mode === 'duels') { const movementOptions=data.movementOptions if(movementOptions.forbidMoving&&movementOptions.forbidZooming&&movementOptions.forbidRotating){forbidOptions='NMPZ'} else if (!movementOptions.forbidMoving&&!movementOptions.forbidZooming&&!movementOptions.forbidRotating){forbidOptions='Moving'} else if(movementOptions.forbidMoving&&!movementOptions.forbidZooming&&!movementOptions.forbidRotating){forbidOptions='NoMoving'} else{forbidOptions='Entertainment'} const usingMap=data.options.map.name data.teams.forEach(team => { team.players.forEach(player => { player.guesses.forEach(guess => { const roundNumber = guess.roundNumber; const roundData = data.rounds.find(round => round.roundNumber === roundNumber); if (roundData) { const gameDate = guess.created.substring(0, 10); const gameMode = 'duels'; const latLng = { 'lat': roundData.panorama.lat, 'lng': roundData.panorama.lng }; const country = roundData.panorama.countryCode; const heading=roundData.panorama.heading const pitch=roundData.panorama.pitch const zoom=roundData.panorama.zoom const playerId = player.playerId; const score = team.roundResults.find(result => result.roundNumber === roundNumber).score; const distance = (guess.distance / 1000).toFixed(2); const guessSeconds = (Math.abs(new Date(guess.created) - new Date(roundData.startTime)) / 1000).toFixed(2); coordinates.push({ gameMode, playerId, latLng, country, score, distance, guessSeconds, forbidOptions, usingMap, gameDate,heading,pitch,zoom}); } }); }); }); } else { if(data.forbidMoving&&data.forbidZooming&&data.forbidRotating){forbidOptions='NMPZ'} else if (!data.forbidMoving&&!data.forbidZooming&&!data.forbidRotating){forbidOptions='Moving'} else if(data.forbidMoving&&!data.forbidZooming&&!data.forbidRotating){forbidOptions='NoMoving'} else{forbidOptions='Entertainment'} const gameMode = 'classic'; const player = data.player; const playerId = player.id; const usingMap=data.mapName player.guesses.forEach((guess, index) => { const roundData = data.rounds[index]; const gameDate = roundData.startTime.substring(0, 10); const heading=roundData.heading const pitch=roundData.pitch const zoom=roundData.zoom const latLng = { 'lat': roundData.lat, 'lng': roundData.lng }; const country = roundData.streakLocationCode; const score = guess.roundScoreInPoints; const distance = parseFloat(guess.distance.meters.amount); const guessSeconds = guess.time; coordinates.push({ gameMode, playerId, latLng, country, score, distance, guessSeconds, forbidOptions, usingMap, gameDate,heading,pitch,zoom}); }); }} resolve(); }catch (error) { console.error(`Error parsing JSON from URL: ${url}`, error); } }, onerror: function(error) { reject(error); } }); }); } async function getPlayerName(id) { return new Promise((resolve, reject) => { const url = `https://www.geoguessr.com/user/${id}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { if (response.status === 200) { const playerName = extractPlayerName(response.responseText); resolve(playerName); } else { reject('Error:', response.status); } }, onerror: function(error) { reject('Error:', error); } }); }); } function extractPlayerName(responseText) { const regex = /"user"\s*:\s*{\s*"nick"\s*:\s*"(.+?)"/; const match = responseText.match(regex); if (match && match.length > 1) { return match[1]; } return null; } function createStreetViewContainer(latitude, longitude,h,p,z) { var css = ` .custom-container div[style*='position: absolute; left: 0px; bottom: 0px;'] { display: none !important; } .transfer-button::after, .link-button::after { position: absolute; bottom: 42px; left: 100%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 5px; border-radius: 5px; font-size: 11px; line-height: 1; height: auto; white-space: nowrap; opacity: 0; transition: opacity 0.3s ease; } .transfer-button:hover::after, .link-button:hover::after { content: attr(data-text); opacity: 0.9; } `; GM_addStyle(css); if (streetViewContainer) { streetViewContainer.remove(); } if (streetViewMap){streetViewMap.setStreetView(null) } streetViewContainer = document.createElement('div'); streetViewContainer.id = 'street-view-container'; streetViewContainer.classList.add('custom-container'); streetViewContainer.style.position = 'fixed'; streetViewContainer.style.bottom = '0px'; streetViewContainer.style.left = '0px'; streetViewContainer.style.width = '800px'; streetViewContainer.style.height = '600px'; streetViewContainer.style.overflow = 'hidden'; streetViewContainer.style.zIndex = '1'; streetViewMap = new google.maps.Map(streetViewContainer, { center: { lat: latitude, lng: longitude }, zoom: 14, streetViewControl: true }); var closeButton = document.createElement('div'); closeButton.className = 'custom-close-button'; closeButton.id='close' closeButton.innerHTML = '×'; closeButton.style.position = 'absolute'; closeButton.style.top = '1px'; closeButton.style.right = '80px'; closeButton.style.margin = '10px'; closeButton.style.padding = '0px'; closeButton.style.background = 'none'; closeButton.style.border = '0px'; closeButton.style.textTransform = 'none'; closeButton.style.appearance = 'none'; closeButton.style.cursor = 'pointer'; closeButton.style.userSelect = 'none'; closeButton.style.borderRadius = '2px'; closeButton.style.height = '40px'; closeButton.style.width = '40px'; closeButton.style.boxShadow = 'rgba(0, 0, 0, 0.3) 0px 1px 4px -1px'; closeButton.style.overflow = 'hidden'; closeButton.style.color = '#d3cfcf'; closeButton.style.fontSize = '30px'; closeButton.style.lineHeight = '40px'; closeButton.style.textAlign = 'center'; closeButton.style.backgroundColor = 'rgb(34, 34, 34)'; closeButton.onclick = function() { streetViewContainer.remove(); }; closeButton.style.zIndex = '2'; closeButton.onclick = function() { streetViewContainer.remove(); }; streetViewContainer.appendChild(closeButton); var transferButton = document.createElement("button"); const shareUrl = `data:image/svg+xml;base64,${btoa(shareSvg)}`; transferButton.style.backgroundImage = `url('${shareUrl}')`; transferButton.style.backgroundPosition = 'center' transferButton.classList.add('transfer-button'); transferButton.setAttribute('data-text', 'Open in map'); transferButton.style.zIndex = '2'; transferButton.style.position = 'absolute'; transferButton.style.bottom = '2px'; transferButton.style.left = '2px'; transferButton.style.margin = '10px'; transferButton.style.padding = '0px'; transferButton.style.border = 'none'; transferButton.style.cursor = 'pointer'; transferButton.style.borderRadius = '2px'; transferButton.style.height = '40px'; transferButton.style.width = '40px'; transferButton.style.boxShadow = 'rgba(0, 0, 0, 0.3) 0px 1px 4px -1px'; transferButton.style.overflow = 'visible'; transferButton.style.fontSize = '24px'; transferButton.style.lineHeight = '40px'; transferButton.style.textAlign = 'center'; transferButton.style.backgroundColor = 'rgb(34, 34, 34)'; streetViewContainer.appendChild(transferButton); var linkButton = document.createElement("button"); const linkUrl = `data:image/svg+xml;base64,${btoa(linkSvg)}`; linkButton.style.backgroundImage = `url('${linkUrl}')`; linkButton.style.backgroundPosition = 'center' linkButton.classList.add('link-button'); linkButton.setAttribute('data-text', 'Copy as link'); linkButton.style.zIndex = '2'; linkButton.style.position = 'absolute'; linkButton.style.bottom = '2px'; linkButton.style.left = '40px'; linkButton.style.margin = '10px'; linkButton.style.padding = '0px'; linkButton.style.border = 'none'; linkButton.style.cursor = 'pointer'; linkButton.style.borderRadius = '2px'; linkButton.style.height = '40px'; linkButton.style.width = '40px'; linkButton.style.boxShadow = 'rgba(0, 0, 0, 0.3) 0px 1px 4px -1px'; linkButton.style.overflow = 'visible'; linkButton.style.fontSize = '24px'; linkButton.style.lineHeight = '40px'; linkButton.style.textAlign = 'center'; linkButton.style.backgroundColor = 'rgb(34, 34, 34)'; streetViewContainer.appendChild(linkButton); var streetViewService = new google.maps.StreetViewService(); streetViewService.getPanorama({ location: { lat: latitude, lng: longitude }, radius: 50 }, function(data, status) { if (status === 'OK') { var panoId = data.location.pano; var streetView = new google.maps.StreetViewPanorama(streetViewContainer, { position: data.location.latLng, pov: { heading: h, pitch: p }, zoom:z, }); streetViewMap.setStreetView(streetView); var focus = { id: panoId, heading: h, pitch: p,zoom:z}; streetView.addListener("pov_changed", function() { focus.heading = streetView.getPov().heading; focus.pitch = streetView.getPov().pitch; focus.zoom = streetView.getZoom(); }); streetView.addListener("pano_changed", function() { focus.id = streetView.getPano(); }); transferButton.addEventListener("click", function() { window.open(`https://www.google.com/maps/@?api=1&map_action=pano&heading=${focus.heading}&pitch=${focus.pitch}&fov=${Math.floor(90/focus.zoom)}&pano=${focus.id}`, "_blank"); }); linkButton.addEventListener("click", function() { setTimeout(function() { linkButton.style.backgroundImage = `url(data:image/svg+xml;base64,${btoa(loadingSvg)})`; }, 500); setTimeout(function() { linkButton.style.backgroundImage = `url(data:image/svg+xml;base64,${btoa(successSvg)})`; }, 1000); setTimeout(function() { linkButton.style.backgroundImage = `url(data:image/svg+xml;base64,${btoa(linkSvg)})`; }, 1300); GM_setClipboard(`https://www.google.com/maps/@?api=1&map_action=pano&heading=${focus.heading}&pitch=${focus.pitch}&fov=${Math.floor(90/focus.zoom)}&pano=${focus.id}`); }); } else { console.error('Street View data not found for this location'); } }); document.body.appendChild(streetViewContainer); } async function swalOption(){ const { value: inputOption,dismiss: inputDismiss } =await Swal.fire({ title: 'Get Activities', text: 'Do you want to fetch activities from your Geoguessr account? If you click "Cancel", you will need to upload a JSON file', icon: 'question', showCancelButton: true, showCloseButton:true, allowOutsideClick: false, input: 'number', inputLabel: 'Set A Limit Of Activities Pages', inputPlaceholder: '10', inputAttributes: { min:1, max:80, step:10, }, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', inputValidator: (value) => { if (!value||parseInt(value)<1) { return 'Please set a valid limit number'; } if (parseInt(value) > 100) { return 'It is recommended that the maximum number of pages should not exceed 100!'; } } }); if (inputOption) { if (!isUpdated){ const pageValue=parseInt(inputOption) getStatics(u,activities,pageValue)} else{ getCoords(activities)} } else if(inputDismiss==='cancel'){ const input = document.createElement('input'); input.type = 'file'; input.style.display = 'none' document.body.appendChild(input); const coordsPromise = new Promise((resolve) => { input.addEventListener('change', async () => { const file = input.files[0]; const reader = new FileReader(); reader.onload = (event) => { try { const result = JSON.parse(event.target.result); resolve(result); document.body.removeChild(input); } catch (error) { Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invalid or incorrectly formatted.','error'); } }; reader.readAsText(file); }); input.click(); }); coordsPromise.then(async (data) => { try { const friends = await getFriends(); var matchedData = matchPlayerNick(data, friends); matchedData=getDiffdays(matchedData) createHeatmap(matchedData); Swal.fire('Success', 'Heatmap is prepared!', 'success'); } catch (error) { console.error("Error:", error); Swal.fire('Error', 'Failed to prepare heatmap', 'error'); } }); } } function createButton() { var mapButton = document.createElement('button'); mapButton.textContent = 'Create Heatmap'; mapButton.addEventListener('click',swalOption); mapButton.style.position = 'fixed'; mapButton.style.top = '10px'; mapButton.style.right = '340px'; mapButton.style.zIndex = '9999'; mapButton.style.borderRadius = "18px"; mapButton.style.padding = "10px 20px"; mapButton.style.border = "none"; mapButton.style.backgroundColor = "#4CAF50"; mapButton.style.color = "white"; mapButton.style.cursor = "pointer"; document.body.appendChild(mapButton); var refreshButton = document.createElement('button'); refreshButton.textContent = 'Update Activities'; refreshButton.addEventListener('click', function(){getStatics(u,activities,10)}); refreshButton.style.position = 'fixed'; refreshButton.style.top = '10px'; refreshButton.style.right = '180px'; refreshButton.style.zIndex = '9999'; refreshButton.style.borderRadius = "18px"; refreshButton.style.padding = "10px 20px"; refreshButton.style.border = "none"; refreshButton.style.backgroundColor = "#4CAF50"; refreshButton.style.color = "white"; refreshButton.style.cursor = "pointer"; document.body.appendChild(refreshButton); var clearButton = document.createElement('button'); clearButton.textContent = 'Clear Activities'; clearButton.addEventListener('click', clearLocalStorage); clearButton.style.position = 'fixed'; clearButton.style.top = '10px'; clearButton.style.right = '35px'; clearButton.style.zIndex = '9999'; clearButton.style.borderRadius = "18px"; clearButton.style.padding = "10px 20px"; clearButton.style.border = "none"; clearButton.style.backgroundColor = "#4CAF50"; clearButton.style.color = "white"; clearButton.style.cursor = "pointer"; document.body.appendChild(clearButton); } loadGoogleMapsAPI('AIzaSyAiRLvmrxcqZRhsiPMzK5Ps2b5Ov6XhJrY', ['visualization','streetView']) .then(createButton) .catch(error => { console.error(error); }); })();