Geoguessr Activities Analysis Tool

map visualization of your geoguessr activities

目前為 2024-05-14 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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