Geoguessr Replay Analyzer

analyze geoguessr replay data

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Geoguessr Replay Analyzer
// @namespace    https://greasyfork.org/users/1179204
// @version      0.0.6
// @description  analyze geoguessr replay data
// @author       KaKa
// @match        https://www.geoguessr.com/duels/*
// @match        https://www.geoguessr.com/team-duels/*
// @run-at       document-end
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @license      BSD
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-annotation.min.js
// ==/UserScript==
(function() {
    let replayData,playersList,selectedPlayer,rounds,currentGameId

    async function getReplayer(gameId,round){
        let replayControls = document.querySelector('[class^="replay_main__"]');
        const keys = Object.keys(replayControls)
        const key = keys.find(key => key.startsWith("__reactProps"))
        const props = replayControls[key]
        playersList=props.children[4].props.players
        if(playersList) rounds=Math.max(...playersList.map(player => player.guesses?.length || 0));
        else rounds= document.querySelectorAll('[class*="playedRound"]').length -3;
        const selectedPlayerLabels = document.querySelectorAll('label[class*="switch_label"][aria-selected="true"]');
        selectedPlayerLabels.forEach(label => {
            const playerName = label.textContent.trim();
            if(playersList)selectedPlayer=playersList.find(player=>player.nick.trim()==playerName)
            else selectedPlayer = props.children[4].props.selectedPlayer
        });
        currentGameId=gameId
        replayData=await fetchReplayData(currentGameId,selectedPlayer.playerId,round)
    }

    async function fetchReplayData( gameId,userId,round) {
        const url = `https://www.geoguessr.com/api/v4/replays/${userId}/${gameId}/${round}`;
        try {
            const response = await fetch(url,{method: "GET",credentials: "include"});

            if (!response.ok) {
                console.error(`HTTP error! Status: ${response.status}`);
                return null
            }
            return await response.json();

        } catch (error) {
            console.error('Error fetching replay data:', error);
            return null;
        }
    }

    function parseUrl() {
        const url = window.location.href;
        const urlObj = new URL(url);

        const pathSegments = urlObj.pathname.split('/');
        const gameId = pathSegments.length > 2 ? pathSegments[2] : null;

        const round = urlObj.searchParams.get("round");
        return { gameId, round };
    }

    async function downloadPanoramaImage(panoId, fileName, w, h, zoom,d) {
        return new Promise(async (resolve, reject) => {
            try {
                let canvas, ctx, tilesPerRow, tilesPerColumn, tileUrl, imageUrl;
                const tileWidth = 512;
                const tileHeight = 512;

                let zoomTiles;
                imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoom}&nbt=0&fover=2`;
                zoomTiles = [2, 4, 8, 16, 32];
                tilesPerRow = Math.min(Math.ceil(w / tileWidth), zoomTiles[zoom - 1]);
                tilesPerColumn = Math.min(Math.ceil(h / tileHeight), zoomTiles[zoom - 1] / 2);

                const canvasWidth = tilesPerRow * tileWidth;
                const canvasHeight = tilesPerColumn * tileHeight;
                canvas = document.createElement('canvas');
                ctx = canvas.getContext('2d');
                canvas.width = canvasWidth;
                canvas.height = canvasHeight;

                const loadTile = (x, y) => {
                    return new Promise(async (resolveTile) => {
                        let tile;

                        tileUrl = `${imageUrl}&x=${x}&y=${y}`;


                        try {
                            tile = await loadImage(tileUrl);
                            ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
                            resolveTile();
                        } catch (error) {
                            console.error(`Error loading tile at ${x},${y}:`, error);
                            resolveTile();
                        }
                    });
                };

                let tilePromises = [];
                for (let y = 0; y < tilesPerColumn; y++) {
                    for (let x = 0; x < tilesPerRow; x++) {
                        tilePromises.push(loadTile(x, y));
                    }
                }

                await Promise.all(tilePromises);
                if(d){
                    resolve(canvas.toDataURL('image/jpeg'));}
                else{
                    canvas.toBlob(blob => {
                        const url = window.URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = fileName;
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                        window.URL.revokeObjectURL(url);
                        resolve();
                    }, 'image/jpeg');}
            } catch (error) {
                Swal.fire({
                    title: 'Error!',
                    text: error.toString(),
                    icon: 'error',
                    backdrop: false
                });
                reject(error);
            }
        });
    }

    async function loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => resolve(img);
            img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
            img.src = url;
        });
    }

    async function searchGooglePano(t, e, z) {
        try {
            const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
            const r=50*(21-z)**2
            let payload = createPayload(t,e,r);

            const response = await fetch(u, {
                method: "POST",
                headers: {
                    "content-type": "application/json+protobuf",
                    "x-user-agent": "grpc-web-javascript/0.1"
                },
                body: payload,
                mode: "cors",
                credentials: "omit"
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            } else {
                const data = await response.json();
                if(t=='GetMetadata'){
                    return {
                        panoId: data[1][0][1][1],
                        heading: data[1][0][5][0][1][2][0],
                        worldHeight:data[1][0][2][2][0],
                        worldWidth:data[1][0][2][2][1]
                    };
                }
                return {
                    panoId: data[1][1][1],
                    heading: data[1][5][0][1][2][0]
                };
            }
        } catch (error) {
            console.error(`Failed to fetch metadata: ${error.message}`);
        }
    }

    function createPayload(mode,coorData,r) {
        let payload;
        if(!r)r=50
        if (mode === 'GetMetadata') {
            payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]];
        }
        else if (mode === 'SingleImageSearch') {
            payload =[["apiv3"],
                      [[null,null,coorData.lat,coorData.lng],r],
                      [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,true,2],[10,true,2]]]], [[1,2,3,4,8,6]]]
        } else {
            throw new Error("Invalid mode!");
        }
        return JSON.stringify(payload);
    }

    function analyze(round){

        Swal.fire({
            title: 'Replay Analysis',
            html: `
<div style="text-align: center; font-family: sans-serif;">
            <div style="margin-bottom: 10px;">
                <select id="roundSelect" style="background: #db173e; color: white; font-size: 16px; padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; margin: 5px;"></select>
                <select id="playerSelect" style="background: #007bff; color: white; font-size: 16px; padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; margin: 5px;"></select>
                <button id="toggleEventBtn" style="background: #28a745; color: white; font-size: 14px; padding: 8px 15px; border: 2px solid grey; border-radius: 6px; cursor: pointer; margin: 5px;">Event Analysis</button>
                <button id="toggleSVBtn" style="background: #ffc107; color: black; font-size: 14px; padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer;">StreetView Analysis</button>
            </div>
            <canvas id="chartCanvas" width="300" height="150" style="background: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);"></canvas>
            <div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; margin-top: 5px;">
                <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); width: 300px; text-align: center;">
                    <p><strong>Event Density:</strong> <span id="eventDensity">Loading...</span></p>
                    <p><strong>Avgerage Gap Time:</strong> <span id="AvgGapTime">Loading...</span></p>
                    <p><strong>Pano Event Ratio:</strong> <span id="streetViewRatio">Loading...</span></p>
                    <p><strong>First PanoZoom:</strong> <span id="firstPanoZoomTime">Loading...</span></p>
                    <p><strong>Longest Single Gap:</strong> <span id="longestGapTime">Loading...</span></p>
                </div>
                <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); width: 300px; text-align: center;">
                    <p><strong>Switch Count:</strong> <span id="switchCount">Loading...</span></p>
                    <p><strong>Total Gap Time:</strong> <span id="stagnationTime">Loading...</span></p>
                    <p><strong>Map Event Ratio:</strong> <span id="mapEventRatio">Loading...</span></p>
                    <p><strong>First Map Zoom:</strong> <span id="firstMapZoomTime">Loading...</span></p>
                    <p><strong>Pano POV Speed:</strong> <span id="avgPovSpeed">Loading...</span></p>
                </div>
            </div>
        </div>
    `,
            width: 800,
            showCloseButton: true,
            backdrop:null,
            didOpen: () => {

                const canvas = document.getElementById('chartCanvas')
                const ctx = canvas.getContext('2d', {willReadFrequently: true })

                const playerSelect = document.getElementById("playerSelect");
                const roundSelect = document.getElementById("roundSelect");
                if(playersList){
                    playersList.forEach(player => {
                        let option = document.createElement("option");
                        option.value = player.playerId;
                        option.textContent = player.nick;
                        playerSelect.appendChild(option);
                    });
                    if(selectedPlayer)playerSelect.value=selectedPlayer.playerId;}
                else playerSelect.style.display='none'
                if(rounds){
                    for (let i = 1; i <= rounds; i++) {
                        let option =document.createElement("option")
                        option.value = i;
                        option.textContent=`Round ${i}`
                        roundSelect.appendChild(option);
                    }
                }
                if(round)roundSelect.value=parseInt(round)
                const toggleSVBtn = document.getElementById('toggleSVBtn');
                const toggleEventBtn = document.getElementById('toggleEventBtn');

                function updateChartData(data, playerName) {
                    chart.resize()
                    const interval = 1000;
                    const eventTypes = [
                        "PanoPov",
                        "PanoZoom",
                        "MapPosition",
                        "MapZoom",
                        "PinPosition",
                        "MapDisplay",
                        "PanoPosition",
                        "Focus",
                        "Timer",
                        "KeyPress",
                    ];

                    const keyEventTypes = ["PinPosition", "MapDisplay", "GuessWithLatLng","Timer", "Focus","KeyPress"];
                    const eventColors = {
                        "MapZoom": "#0000FF",
                        "MapPosition": "#FFA500",
                        "PanoPov": "#00FF00",
                        "PinPosition": "#00FFFF",
                        "MapDisplay": "#800080",
                        "PanoZoom": "#FF69B4",
                        "PanoPosition": "#1E90FF",
                        "KeyPress":"lightgreen",
                        "Timer":"red",
                        "Focus":"#FFD700"
                    };

                    const eventBuckets = {};
                    const allEventTimes = {};

                    eventTypes.forEach(eventType => {
                        eventBuckets[eventType] = {};

                    });
                    keyEventTypes.forEach(eventType => {
                        allEventTimes[eventType] = [];
                    });

                    data.forEach(event => {
                        const eventTime = event.time;
                        const relativeTime = eventTime - data[0].time;
                        if(eventBuckets[event.type]){
                            const bucket = Math.floor(relativeTime / interval);

                            if (!eventBuckets[event.type][bucket]) {
                                eventBuckets[event.type][bucket] = 0;
                            }
                            eventBuckets[event.type][bucket]++;
                        }
                        if(allEventTimes[event.type]){
                            allEventTimes[event.type].push(relativeTime); }
                    });

                    const labels = [];
                    const maxBucket = Math.max(
                        ...Object.values(eventBuckets).flatMap(bucket => Object.keys(bucket).map(Number))
                    );

                    for (let i = 0; i <= maxBucket; i++) {
                        const relativeSeconds = (i * interval + interval / 2) / 1000; // 获取3秒区间的中点
                        const minutes = Math.floor(relativeSeconds / 60);
                        const seconds = Math.floor(relativeSeconds % 60);
                        const formattedTime = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
                        labels.push(formattedTime);
                    }

                    const datasets = eventTypes.map(eventType => {
                        const dataPoints = labels.map((label, index) => eventBuckets[eventType][index] || 0);
                        return {
                            label: eventType,
                            data: dataPoints,
                            fill: false,
                            borderColor: eventColors[eventType],
                            backgroundColor: eventColors[eventType],
                            tension: 0.5,
                            hidden: true
                        };
                    });

                    const totalEventsData = labels.map((label, index) => {
                        let total = 0;
                        eventTypes.forEach(eventType => {
                            total += eventBuckets[eventType][index] || 0;
                        });
                        return total;
                    });

                    datasets.push({
                        label: 'Total Events',
                        data: totalEventsData,
                        fill: false,
                        borderColor: 'rgba(0,0,0,0.6)',
                        backgroundColor: 'rgba(0,0,0,0.6)',
                        tension: 0.5
                    });

                    const annotations = [];
                    const hiddenKeyPoints = [];

                    Object.keys(allEventTimes).forEach(eventType => {
                        allEventTimes[eventType].forEach((eventTime, idx) => {
                            const xPosition = eventTime / 1000;

                            const annotation = {
                                type: 'line',
                                xMin: xPosition,
                                xMax: xPosition,
                                borderColor: eventColors[eventType],
                                borderWidth: 1.5,
                                borderDash: [5, 5],
                            };

                            if (eventType === "KeyPress") {
                                const keyPayload = data.find(
                                    ev => ev.type === "KeyPress" && (ev.time - data[0].time) === eventTime
                                )?.payload?.key || "";

                                annotations.push({
                                    type: 'line',
                                    xMin: xPosition,
                                    xMax: xPosition,
                                    borderColor: eventColors[eventType],
                                    borderWidth: 1.5,
                                    borderDash: [5, 5],

                                    hiddenPoint: {
                                        x: xPosition,
                                        key: keyPayload
                                    }
                                });

                                hiddenKeyPoints.push({
                                    x: xPosition,
                                    y: 0,
                                    key: keyPayload
                                });
                            }

                            annotations.push(annotation);
                        });
                    });
                    datasets.push({
                        label: "KeyPressHidden",
                        data: hiddenKeyPoints,
                        parsing: false,
                        pointRadius: 4,
                        pointHoverRadius: 10,
                        borderWidth: 0,
                        borderColor: "rgba(0,0,0,0)",
                        backgroundColor: "rgba(0,0,0,0)",
                        showLine: false
                    });
                    chart.data.datasets = datasets;
                    chart.data.labels = labels;
                    chart.options.plugins.annotation.annotations = annotations;
                    chart.update();}

                const chart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: [],
                        datasets: []
                    },
                    options: {
                        responsive: true,
                        plugins: {
                            legend: {
                                display: true,
                                labels: {
                                    boxWidth: 30,
                                    boxHeight: 15,
                                    padding: 30
                                },
                                position: 'top',
                                align: 'center',
                                labels: {
                                    usePointStyle: true,
                                    padding: 20,
                                    pointStyle: 'rectRounded'
                                },
                            },
                            tooltip: {
                                enabled: true,
                                intersect: false,
                                mode: "nearest",

                                callbacks: {
                                    title: () => "",
                                    label: (ctx) => {
                                        if (ctx.dataset.label === "KeyPressHidden") {
                                            return `Key: ${ctx.raw.key}`;
                                        }
                                        return null;
                                    }
                                },
                                backgroundColor: "rgba(0,0,0,0.75)",
                                padding: 8,
                                cornerRadius: 6
                            },
                            annotation: { annotations: {} },
                    customTooltip: true
                },
                                        scales: {
                                        x: { title: { display: true } },
                  y: { title: { display: true, text: 'Event Counts' }, beginAtZero: true }
 },

},

});

function updateEventAnalysisData(data) {
    const { eventDensity, switchCount, stagnationTime, stagnationCount, AvgGapTime, streetViewRatio, mapEventRatio, firstMapZoomTime, firstPanoZoomTime,longestGapTime,avgPovSpeed} = updateEventAnalysis(data);
    document.getElementById('eventDensity').textContent = eventDensity.toFixed(2) + " times/s";
    document.getElementById('stagnationTime').textContent = stagnationTime.toFixed(2) + " s";
    document.getElementById('longestGapTime').textContent = longestGapTime.toFixed(2) + " s";
    document.getElementById('avgPovSpeed').textContent = avgPovSpeed.toFixed(2) + " °/s";
    document.getElementById('switchCount').textContent = `${switchCount/2} times`;
    document.getElementById('AvgGapTime').textContent =!stagnationCount?'None': `${(parseFloat(stagnationTime/stagnationCount)).toFixed(2)}s`;
    document.getElementById('streetViewRatio').textContent = (streetViewRatio * 100).toFixed(2) + "%";
    document.getElementById('mapEventRatio').textContent = (mapEventRatio * 100).toFixed(2) + "%";
    document.getElementById('firstMapZoomTime').textContent = firstMapZoomTime === null ? "None" : "At " + firstMapZoomTime + " s";
    document.getElementById('firstPanoZoomTime').textContent = firstPanoZoomTime === null ? "None" : "At " + firstPanoZoomTime + " s";
}

updateChartData(replayData);
updateEventAnalysisData(replayData);
playerSelect.onchange = async () => {
    canvas.style.pointerEvents = 'auto';
    try{
        replayData=await fetchReplayData(currentGameId,playerSelect.value,roundSelect.value)
        selectedPlayer=playersList.find(player=>player.playerId==playerSelect.value)
    }
    catch(e){
        console.error("Error fetching replay data")
        return
    }
    updateChartData(replayData);
    updateEventAnalysisData(replayData);
};

roundSelect.onchange = async () => {
    canvas.style.pointerEvents = 'auto';
    try{
        replayData=await fetchReplayData(currentGameId,playerSelect.value||selectedPlayer.playerId,roundSelect.value)
    }
    catch(e){
        console.error("Error fetching replay data")
        return
    }
    updateChartData(replayData);
    updateEventAnalysisData(replayData);
};

toggleEventBtn.addEventListener('click',()=>{
    toggleSVBtn.style.border='none'
    toggleEventBtn.style.border='2px solid grey'
    canvas.style.pointerEvents = 'auto';
    updateChartData(replayData);
    updateEventAnalysisData(replayData);
})
toggleSVBtn.addEventListener('click',async () => {
    toggleEventBtn.style.border='none'
    toggleSVBtn.style.border='2px solid grey'
    canvas.style.pointerEvents='none'
    var centerHeading;
    const panoIds = replayData
    .filter(item => item.type === 'PanoPosition' && item.payload?.panoId)
    .map(item => item.payload.panoId);
    if(panoIds.length>1){
        var panoId=panoIds[Math.floor(Math.random() * panoIds.length)]
        }
    else{
        panoId=panoIds[0]
    }
    const metaData = await searchGooglePano('GetMetadata',panoId );

    var w = metaData.worldWidth;
    var h = metaData.worldHeight;

    centerHeading = metaData.heading;


    try {
        const imageUrl = await downloadPanoramaImage(panoId, panoId, w, h, w==13312?5:3, true);
        const img = await loadImage(imageUrl);
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        let lastPanoPov = { heading: 0, pitch: 0 };
        let stagnationPoints =[];
        const heatData = replayData.filter(event => ["PanoZoom", "PanoPov"].includes(event.type)).map((event, index, events) => {
            let heading, pitch, type;
            let time = event.time;

            if (event.type === "PanoPov") {
                [heading, pitch] =[event.payload.heading,event.payload.pitch]
                lastPanoPov = { heading, pitch };
                type = "PanoPov";
            } else if (event.type === "PanoZoom") {
                heading = lastPanoPov.heading;
                pitch = lastPanoPov.pitch;
                type = "PanoZoom";
            }

            if (index > 0) {
                const prevEvent = events[index - 1];
                const timeDiff = Math.abs(time - prevEvent.time);
                if (timeDiff > 3000) {
                    stagnationPoints.push(index);
                }
            }

            return { heading, pitch, type};
        });

        drawHeatMapOnImage(canvas, heatData, centerHeading,stagnationPoints);
    } catch (error) {
        console.error('Error downloading panorama image:', error);
    }

})}

});
}

function drawHeatMapOnImage(canvas, heatData, centerHeading,points) {
    const ctx = canvas.getContext('2d');
    heatData.forEach((point, index) => {
        let headingDifference = point.heading - centerHeading;
        if (headingDifference > 180) {
            headingDifference -= 360;
        } else if (headingDifference < -180) {
            headingDifference += 360;
        }
        const x = (headingDifference + 180) / 360 * canvas.width;
        const y = (90 - point.pitch) / 180 * canvas.height;

        ctx.beginPath();
        if(canvas.width===13312) ctx.arc(x, y, (points.includes(index))?80:40, 0,2* Math.PI);
        else ctx.arc(x, y, (points.includes(index))?30:15, 0,2* Math.PI);

        if (points.includes(index)) {
            ctx.fillStyle = 'yellow';
        } else if (point.type === "PanoZoom") {
            ctx.fillStyle = '#FF0000';
        } else if (point.type === "PanoPov") {
            ctx.fillStyle = '#00FF00';
        }

        ctx.fill();
    });
}

function updateEventAnalysis(data) {
    let totalEvents = 0;
    let totalTime = 0;
    let stagnationTime = 0;
    let stagnationCount = 0;
    let switchCount = 0;
    let streetViewEvents = 0;
    let mapEvents = 0;
    let lastEventTime = null;
    let longestGapTime = 0;

    let totalHeadingDifference = 0;
    let totalTimeGap = 0;

    let lastPanoPovEventTime = null;
    let lastHeading = null;

    data.forEach(event => {
        const eventTime = event.time;
        const relativeTime = Math.floor((eventTime - data[0].time) / 1000);

        totalEvents++;
        totalTime = relativeTime;

        if (event.type.includes("Pano")) {
            streetViewEvents++;
        } else if (event.type.includes("Map")) {
            mapEvents++;
        }

        if (lastEventTime !== null) {
            const timeGap = (eventTime - lastEventTime) / 1000;

            if (timeGap >= 3) {
                if (timeGap > longestGapTime) longestGapTime = timeGap;
                stagnationTime += timeGap;
                stagnationCount++;
            }
        }

        if (event.type === "PanoPov" && lastPanoPovEventTime !== null) {
            const headingDifference = Math.abs(event.payload.heading - lastHeading);
            const timeGap = (eventTime - lastPanoPovEventTime) / 1000;

            totalHeadingDifference += headingDifference;
            totalTimeGap += timeGap;
        }

        lastEventTime = eventTime;

        if (event.type === "PanoPov") {
            lastPanoPovEventTime = eventTime;
            lastHeading =event.payload.heading;
        }

        if (event.type === "Focus" && !event.payload.focus) {
            switchCount++;
        }
    });

    const eventDensity = totalEvents / totalTime;

    const streetViewRatio = streetViewEvents / totalEvents;
    const mapEventRatio = mapEvents / totalEvents;

    let firstMapZoomTime = null;
    let firstMapZoomTime_ = null;
    let firstPanoZoomTime_ = null;
    let firstPanoZoomTime = null;
    data.forEach(event => {
        if (event.type === "MapZoom" && !firstMapZoomTime) {
            if (firstMapZoomTime_ === null) firstMapZoomTime_ = 1;
            else {
                firstMapZoomTime = Math.floor((event.time - data[0].time) / 1000);
            }
        }
        if (event.type === "PanoZoom" && !firstPanoZoomTime) {
            if (firstPanoZoomTime_ === null) firstPanoZoomTime_ = 1;
            else {
                firstPanoZoomTime = Math.floor((event.time - data[0].time) / 1000);
            }
        }
    });

    let avgPovSpeed = 0;
    if (totalTimeGap > 0) {
        avgPovSpeed = totalHeadingDifference / totalTimeGap;
    }

    return {
        eventDensity,
        stagnationTime,
        switchCount,
        stagnationCount,
        streetViewRatio,
        mapEventRatio,
        firstPanoZoomTime,
        firstMapZoomTime,
        longestGapTime,
        avgPovSpeed
    };
}


let onKeyDown =async (e) => {
    if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
        return;
    }
    if (e.shiftKey&&(e.key === 'K' || e.key === 'k')){
        const {gameId, round}=parseUrl()
        const selectedRounds = document.querySelectorAll('[class*="selectedRound"]');
        var match
        selectedRounds.forEach(round => {
            const roundTextElement = round.querySelector('[class*="roundText"], [class*="game-summary_text"]');
            if (roundTextElement && roundTextElement.textContent.includes('Round')) {
                match = roundTextElement.textContent.match(/Round\s+(\d+)/);
            }
        });
        await getReplayer(gameId,match[1])
        analyze(match[1])
    }
}

document.addEventListener("keydown", onKeyDown);
})();