map visualization of your geoguessr activities
当前为
// ==UserScript==
// @name Geoguessr Activities Analysis Tool
// @namespace http://tampermonkey.net/
// @version 1.0
// @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
// ==/UserScript==
(function() {
'use strict';
var mapCreated = false;
var u = 'https://www.geoguessr.com/api/v4/feed/friends?count=100';
var map,heatmapLayer
let coordinates=JSON.parse(localStorage.getItem('coordinates'));
let activities =JSON.parse(localStorage.getItem('activities'));
if (!activities){
activities={'duels':{},'games':{}}}
if (!coordinates){
coordinates=[]}
function saveLocalStorage(item) {
if(item==='a'){
localStorage.setItem('activities', JSON.stringify(activities));
activities =JSON.parse(localStorage.getItem('activities'));}
else{
localStorage.setItem('coordinates', JSON.stringify(coordinates));
coordinates =JSON.parse(localStorage.getItem('coordinates'));}
}
function downloadJSON() {
var data = JSON.stringify(coordinates, null, 2);
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'coordinates.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function clearLocalStorage() {
localStorage.removeItem('activities');
localStorage.removeItem('coordinates')
activities = {'duels':{},'games':{}};
}
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() {
let mapCreated = false;
let weightKey = 'score';
let heatmapData=[]
let filteredData
let marker
let markers = [];
function filterHeatmapData(minScore, maxScore) {
filteredData = heatmapData.filter(point => point.weight >= minScore && point.weight <= maxScore);
heatmapLayer.setData(filteredData);
}
function createMap() {
var css = `
#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);
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);
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 data=coordinates
const keysToFilter = ['playerId', 'gameMode', 'forbidOptions', 'usingMap', 'country', 'score', 'distance', 'guessSeconds'];
const selectContainer = document.createElement('div');
selectContainer.className = 'select-container';
controlDiv.appendChild(selectContainer);
const optionValues = {};
let filters = [];
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: 3 },
{ min: 3, 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='<3s'
}
option.setAttribute('value', label);
option.textContent = label;
select.appendChild(option);
}
}
else {
const optionCounts = {};
data.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 === 'playerId') {
getPlayerName(value)
.then(name => {
option.textContent = name;
})
.catch(error => {
option.textContent = value;
});
} else {
option.textContent = value;
}
select.appendChild(option);
}
});
}
selectContainer.appendChild(select);
select.addEventListener('change', () => {
const selectedValue = select.value;
filters[key] = selectedValue;
let filteredData = data;
if (selectedValue.includes('-')) {
const [minValue, maxValue] = selectedValue.split('-').map(val => parseFloat(val));
filteredData = filteredData.filter(item => {
const itemValue = parseFloat(item[key]);
return itemValue >= minValue && itemValue <= maxValue;
})
}
else if (selectedValue.includes('>') || selectedValue.includes('<')) {
const operator = selectedValue.includes('>') ? '>' : '<';
const value = parseFloat(selectedValue.substring(1));
filteredData = filteredData.filter(item => {
const itemValue = parseFloat(item[key]);
return operator === '>' ? itemValue > value : itemValue < value;
});
}
else if(selectedValue.includes('5000')){
filteredData = filteredData.filter(item => {
const itemValue = parseFloat(item[key]);
return itemValue == 5000 ;
})}
else{ Object.keys(filters).forEach(filterKey => {
const filterValue = filters[filterKey];
if (filterValue) {
filteredData = filteredData.filter(item => item[filterKey] === filterValue);
}
})};
//if ('distance'in filters){
//refreshHeatmap(filteredData,'distance')}
//else if('guessSeconds'in filters){
//refreshHeatmap(filteredData,'guessSeconds')}
refreshHeatmap(filteredData,'score')
});
});
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
});
}
}
if (event.target.checked) {
for (const coord of heatmapData) {
const marker = new google.maps.Marker({
position: new google.maps.LatLng(coord.location),
map: map,
title: 'Score: ' + coord.weight,
pin: {
fillOpacity: 1,
strokeWeight: 0,
color:'gray',
scale: (coord.weight/5000)
}
});
markers.push(marker);
marker.addListener('click', () => {
createStreetViewContainer(coord.location.lat(),coord.location.lng(),mapContainer)
});
}
} else {
markers.forEach(marker => marker.setMap(null));
markers = [];
}
})
}
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();
}
}
function getDefault(){
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
});
}
heatmapLayer = new google.maps.visualization.HeatmapLayer({
data: h,
dissipating: true,
map: map,
});}
function getStatics(url, result) {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
if (response.status === 200) {
var data = JSON.parse(response.responseText);
var entries = data.entries;
if (entries && entries.length > 0) {
entries.forEach(function(entry) {
if (entry.payload) {
var payloadList = JSON.parse(entry.payload);
var userId = entry.user.id;
if (!Array.isArray(payloadList)) {
if (payloadList.gameToken) {
if (result.games[userId]) {
if (!result.games[userId].includes(payloadList.gameToken)){
result.games[userId].push(payloadList.gameToken)};
} else {
result.games[userId] = [payloadList.gameToken];
}
} else if (payloadList.gameId) {
if (result.duels[userId]) {
if (!result.duels[userId].includes(payloadList.gameId)){
result.duels[userId].push(payloadList.gameId);}
} else {
result.duels[userId] = [payloadList.gameId];
}
}}
else{
payloadList.forEach(function(payloads) {
const payload=payloads.payload
if (payload.gameToken) {
if (result.games[userId]) {
if (!result.games[userId].includes(payload.gameToken)){
result.games[userId].push(payload.gameToken)};
} else {
result.games[userId] = [payload.gameToken];
}
} else if (payload.gameId) {
if (result.duels[userId]) {
if (!result.duels[userId].includes(payload.gameId)){
result.duels[userId].push(payload.gameId)};
} else {
result.duels[userId] = [payload.gameId];
}
}
});
}
}
});
var paginationToken = data.paginationToken;
if (paginationToken) {
var nextPageUrl = url + "&paginationToken=" + encodeURIComponent(paginationToken);
console.log('fecthing...')
getStatics(nextPageUrl, result)
}
else {
saveLocalStorage('a');
console.log(result);
}
} else {
saveLocalStorage('a');
console.error('Data not found!');
}
} else {console.log(result)
saveLocalStorage('a');
console.error('Request failed: ' + response.statusText);
getCoords()
}
}
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getCoords() {
try {
const data = activities;
const duelsPromises = [];
const gamesPromises = [];
for (const userId in data.duels) {
const gameIds = data.duels[userId] || [];
for (const gameId of gameIds) {
const requestUrl = `https://game-server.geoguessr.com/api/duels/${gameId}`;
duelsPromises.push(getGameSummary(requestUrl, 'duels'));
}
}
for (const playerId in data.games) {
const gameIds = data.games[playerId] || [];
for (const gameId of gameIds) {
const requestUrl = `https://www.geoguessr.com/api/v3/games/${gameId}?client=web`;
gamesPromises.push(getGameSummary(requestUrl, 'games'));
}
}
await Promise.all([...duelsPromises, ...gamesPromises]).then(() => {
getDefault()
Swal.fire('Success!', 'Map visualization is ready!','success')
});
} catch (error) {
console.error('Error parsing JSON from localStorage:', error);
}
}
function getGameSummary(url,mode) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
let forbidOptions
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 gameMode='duels'
const latLng ={'lat': roundData.panorama.lat,'lng':roundData.panorama.lng}
const country = roundData.panorama.countryCode;
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})
}
});
});
});
}
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 usingMap=data.mapName
const playerId=data.player.id
data.player.guesses.forEach((guess,index) => {
const roundData = data.rounds[index];
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});
})
}
saveLocalStorage('c')
} catch (error) {
console.log('Error parsing JSON data: ' + error);
}
},
onerror: function(error) {
console.error('Error fetching data:', error);
}
});
});
}
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,mapContainer) {
if (streetViewContainer) {
streetViewContainer.remove();
}
var streetViewContainer = document.createElement('div');
streetViewContainer.id = 'street-view-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 = '9999';
var streetViewMap = new google.maps.Map(streetViewContainer, {
center: { lat: latitude, lng: longitude },
zoom: 14,
streetViewControl: true
});
var streetViewService = new google.maps.StreetViewService();
streetViewService.getPanorama({ location: { lat: latitude, lng: longitude }, radius: 50 }, function(data, status) {
if (status === 'OK') {
var streetView = new google.maps.StreetViewPanorama(streetViewContainer, {
position: data.location.latLng,
pov: { heading: 235, pitch: 10 }
});
streetViewMap.setStreetView(streetView);
} 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 the script to automatically fetch activities from your Geoguessr account and download it as JSON file? If you click "Cancel", you will need to upload a JSON file',
icon: 'question',
showCancelButton: true,
showCloseButton:true,
allowOutsideClick: false,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel'
});
if (inputOption) {
getStatics(u,activities)
sleep(500)
if (activities.duels.length!=0&&activities.games.length!=0){
createHeatmap()
getDefault()
}
}
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((data) => {
coordinates=data
createHeatmap()
getDefault()
}
);
}
}
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 = '380px';
mapButton.style.zIndex = '9999';
document.body.appendChild(mapButton);
var storeButton = document.createElement('button');
storeButton.textContent = 'Download data as JSON';
storeButton.addEventListener('click', downloadJSON);
storeButton.style.position = 'fixed';
storeButton.style.top = '10px';
storeButton.style.right = '200px';
storeButton.style.zIndex = '9999';
document.body.appendChild(storeButton);
var clearButton = document.createElement('button');
clearButton.textContent = 'Clear Activities';
clearButton.addEventListener('click', clearLocalStorage);
clearButton.style.position = 'fixed';
clearButton.style.top = '10px';
clearButton.style.right = '80px';
clearButton.style.zIndex = '9999';
document.body.appendChild(clearButton);
}
loadGoogleMapsAPI('AIzaSyAiRLvmrxcqZRhsiPMzK5Ps2b5Ov6XhJrY', ['visualization','streetView'])
.then(createButton)
.catch(error => {
console.error(error);
});
})();