Geoguessr Activities Analysis Tool

map visualization of your geoguessr activities

当前为 2024-05-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
    });
})();