// ==UserScript==
// @name Geoguessr Activities Analysis Tool
// @version 1.4
// @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.google.com/s2/favicons?domain=geoguessr.com
// @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>`
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 {
content: "Paste JSON data to your clipboard";
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 {
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);
coordinates=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');
}
await downloadJSON(coordinates);
} 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:hover {
opacity: 1;
}
.transfer-button::after {
content: 'Open in map';
position: absolute;
bottom:cal(100% + 15px);
left:100px;
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 {
opacity: 1;
}
`;
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 = '#fff';
closeButton.style.fontSize = '24px';
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.style.zIndex = '2';
transferButton.style.position = 'absolute';
transferButton.style.bottom = '2px';
transferButton.style.left = '10px';
transferButton.style.margin = '10px';
transferButton.style.padding = '0px';
transferButton.style.border = '0px';
transferButton.style.opacity = '0.8';
transferButton.style.textTransform = 'none';
transferButton.style.appearance = 'none';
transferButton.style.cursor = 'pointer';
transferButton.style.userSelect = 'none';
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.color = '#fff';
transferButton.style.fontSize = '24px';
transferButton.style.lineHeight = '40px';
transferButton.style.textAlign = 'center';
transferButton.style.backgroundColor = 'rgb(34, 34, 34)';
streetViewContainer.appendChild(transferButton);
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);
transferButton.addEventListener("click", function() {
window.open(`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${latitude},${longitude}&heading=${h}&pitch=${p}&fov=90&pano=${panoId}`, "_blank");
});
} 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);
});
})();