Geoguessr Activities Analysis Tool

map visualization of your geoguessr activities

目前为 2024-05-20 提交的版本。查看 最新版本

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