Greasy Fork

Pano Date Detective

Get the exact time a Google Street View image was taken (recent coverage)

当前为 2024-06-12 提交的版本,查看 最新版本

// ==UserScript==
// @name         Pano Date Detective
// @namespace    https://www.geoguessr.com/user/6494f9bbab07ca3ea843220f
// @version      0.3
// @description  Get the exact time a Google Street View image was taken (recent coverage)
// @author       KaKa
// @match        https://www.google.com/maps/*
// @icon         https://www.svgrepo.com/show/176844/map-location.svg
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==
(function() {
    let accuracy=2;
    function extractParams(link) {
        const regex = /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m(?:6|7)!1e1!3m(?:4|5)!1s(.*?)!/;

        const match = link.match(regex);

        if (match && match.length === 4) {
            var lat = match[1];
            var lng = match[2];
            var panoId = match[3];
            return {lat,lng,panoId}
        } else {
            console.error('Invalid Google Street View link format');
            return null;
        }
    }

    async function UE(t, e, s, d) {
        try {
            const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
            let payload = createPayload(t, e,s,d);

            const response = await fetch(r, {
                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 {
                return await response.json();
            }
        } catch (error) {
            console.error(`There was a problem with the UE function: ${error.message}`);
        }
    }

    function createPayload(mode,coorData,s,d) {
        let payload;

        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') {
            var lat =parseFloat( coorData.lat);
            var lng = parseFloat( coorData.lng);
            lat = lat % 1 !== 0 && lat.toString().split('.')[1].length >6 ? parseFloat(lat.toFixed(6)) : lat;
            lng = lng % 1 !== 0 && lng.toString().split('.')[1].length > 6 ? parseFloat(lng.toFixed(6)) : lng;

            payload=[["apiv3"],[[null,null,lat,lng],10],[[null,null,null,null,null,null,null,null,null,null,[s,d]],null,null,null,null,null,null,null,[2],null,[[[2,true,2]]]],[[2,6]]]}

        else {
            throw new Error("Invalid mode!");
        }
        return JSON.stringify(payload);
    }

    async function binarySearch(c, start,end) {
        let capture
        let response
        while (end - start >= accuracy) {
            let mid= Math.round((start + end) / 2);
            response = await UE("SingleImageSearch", c, start,end);
            if (response&&response[0][2]== "Search returned no images." ){
                start=mid+start-end
                end=start-mid+end
                mid=Math.round((start+end)/2)
            } else {
                start=mid
                mid=Math.round((start+end)/2)
            }
            capture=mid
        }

        return capture
    }

    function monthToTimestamp(m) {

        const [year, month] = m

        const startDate =Math.round( new Date(year, month-1,1).getTime()/1000);

        const endDate =Math.round( new Date(year, month, 1).getTime()/1000)-1;

        return { startDate, endDate };
    }

    async function getLocal(coord, timestamp) {
        const apiUrl = "https://api.geotimezone.com/public/timezone?";
        const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;

        try {
            const [lat, lng] = coord;
            const url = `${apiUrl}latitude=${lat}&longitude=${lng}`;

            const responsePromise = new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    responseType: "json",
                    onload: function(response) {
                        if (response.status >= 200 && response.status < 300) {
                            resolve(response.response);
                        } else {
                            reject(new Error("Request failed: " + response.statusText));
                        }
                    },
                    onerror: function(error) {
                        reject(new Error("There was a network error: " + error));
                    }
                });
            });

            function extractOffset(text) {
                const regex = /UTC[+-]?\d+/;
                const match = text.match(regex);
                if (match) {
                    return parseInt(match[0].substring(3));
                } else {
                    return null;
                }
            }
            const data = await responsePromise;
            const offset = extractOffset(data.offset);
            const targetTimezoneOffset = offset * 3600;
            const offsetDiff = systemTimezoneOffset - targetTimezoneOffset;
            const convertedTimestamp = Math.round(timestamp - offsetDiff);
            return convertedTimestamp;
        } catch (error) {
            throw error;
        }
    }

    function formatTimestamp(timestamp) {
        const date = new Date(timestamp * 1000);
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        const seconds = String(date.getSeconds()).padStart(2, '0');

        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    async function addCustomButton() {
        const titlecardDiv = document.getElementById("titlecard");
        if (!titlecardDiv) {
            console.error('Titlecard div not found');
            return;
        }

        const navigationDiv = titlecardDiv.querySelector("[role='navigation']");
        if (!navigationDiv) {
            console.error('Navigation div not found inside titlecard');
            return;
        }
        const timeDisplay = document.createElement('div');
        timeDisplay.style.display='none'
        timeDisplay.style.position='absolute'
        timeDisplay.style.left='21.5%'
        timeDisplay.style.bottom='5px'
        timeDisplay.style.color = '#9AA0A6';
        timeDisplay.style.fontSize = '12px';
        navigationDiv.appendChild(timeDisplay);
        const lchoPbSpan = navigationDiv.querySelector('span.lchoPb');

        const button = document.createElement("button");
        button.textContent = 'exact time';
        button.style.position = 'absolute';
        button.style.top = '50%';
        button.style.right = '5px';
        button.style.display = 'block';
        button.style.width = '64px';
        button.style.fontSize = '12px';
        button.style.height = '18px';
        button.style.borderRadius = '10px';
        button.style.color = '#FFFFFF';
        button.style.cursor = 'pointer';
        button.style.background = '#1A73E8';
        button.addEventListener("click", async function() {
            if (lchoPbSpan){
                lchoPbSpan.textContent='loading...'
            }
            const currentUrl = window.location.href;
            var lat=extractParams(currentUrl).lat;
            var lng=extractParams(currentUrl).lng;
            var panoId=extractParams(currentUrl).panoId;
            try {
                const metaData = await UE('GetMetadata', panoId);
                if (!metaData) {
                    console.error('Failed to get metadata');
                    return;
                }

                let panoDate;
                try {
                    panoDate = metaData[1][0][6][7];
                } catch (error) {
                    try {
                        panoDate = metaData[1][6][7];
                    } catch (error) {
                        console.log(error);
                        return;
                    }
                }

                if (!panoDate) {
                    console.error('Failed to get panoDate');
                    return;
                }

                const timeRange = monthToTimestamp(panoDate);
                if (!timeRange) {
                    console.error('Failed to convert panoDate to timestamp');
                    return;
                }

                try {
                    const captureTime = await binarySearch({"lat":lat,"lng":lng},timeRange.startDate,timeRange.endDate);
                    if (!captureTime) {
                        console.error('Failed to get capture time');
                        return;
                    }
                    const exactTime=await getLocal([lat,lng],captureTime)
                    if(!exactTime){
                        console.error('Failed to get exact time');
                    }
                    const formattedTime=formatTimestamp(exactTime)
                    if(lchoPbSpan){
                        lchoPbSpan.textContent = formattedTime;
                    }



                } catch (error) {
                    console.log(error);
                }
            } catch (error) {
                console.error(error);
            }
        });

        navigationDiv.appendChild(button);
    }

    function onPageLoad() {

        setTimeout(function() {
            addCustomButton();
        }, 1000);

    }

    window.addEventListener('load', onPageLoad);
    const originalXHR = window.XMLHttpRequest;




})();