Greasy Fork

Pano Date Detective

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

当前为 2024-07-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         Pano Date Detective
// @namespace    https://greasyfork.org/users/1179204
// @version      1.2.1
// @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/485785/magnifier.svg
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==
(function() {
    let detectButton
    let accuracy=2;
    let dateSvg=`<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#9AA0a6"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Calendar / Calendar_Days"> <path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#9AA0a6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`
    let date_Svg=`<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Calendar / Calendar_Days"> <path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`
    const svgUrl=svgToUrl(dateSvg)
    const svg_Url=svgToUrl(date_Svg)
    function svgToUrl(svgText) {
        const svgBlob = new Blob([svgText], {type: 'image/svg+xml'});
        const svgUrl = URL.createObjectURL(svgBlob);
        return svgUrl;
    }

    function extractParams(link) {
        const regex = /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m\d+!1e\d+!3m\d+!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 systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;

        try {
            const [lat, lng] = coord;
            const url = `https://api.wheretheiss.at/v1/coordinates/${lat},${lng}`;

            const response = await fetch(url);
            if (!response.ok) {
                throw new Error("Request failed: " + response.statusText);
            }
            const data = await response.json();
            const targetTimezoneOffset = data.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() {
        var dateSpan
        const navigationDiv = document.querySelector("[role='navigation']");
        if (!navigationDiv) {
            console.error('Navigation div not found inside titlecard');
            return;
        }

        dateSpan = navigationDiv.querySelector('span.mqX5ad');
        if (!dateSpan){
            dateSpan = navigationDiv.querySelector('span.lchoPb');
            if(!dateSpan){
                dateSpan = navigationDiv.querySelector('div.mqX5ad');
                if(!dateSpan)
                {
                    dateSpan = navigationDiv.querySelector('div.lchoPb');
                }
            }
        }
        if (!detectButton){
            detectButton = document.createElement("button");}
        const symbol=document.querySelector("[aria-label='Show location on map']")
        const parentElement = symbol.parentNode;
        detectButton.style.id='detect-button'
        detectButton.style.backgroundImage =`url(${svgUrl})`
        detectButton.style.backgroundImageSize='cover'
        detectButton.style.backgroundImagePosition='center'
        detectButton.style.display = 'block';
        detectButton.style.width = '24px';
        detectButton.style.fontSize = '12px';
        detectButton.style.height = '24px';
        detectButton.style.borderRadius = '10px';
        detectButton.style.cursor = 'pointer';
        detectButton.style.backgroundColor = 'transparent';
        detectButton.addEventListener("mouseenter", function(event) {

            detectButton.style.backgroundImage =`url(${svg_Url})`
        });
        detectButton.addEventListener("mouseleave", function(event) {

            detectButton.style.backgroundImage =`url(${svgUrl})`
        });
        detectButton.addEventListener("click", async function() {
            if (dateSpan){
                dateSpan.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(dateSpan){
                        dateSpan.textContent = formattedTime;
                    }



                } catch (error) {
                    console.log(error);
                }
            } catch (error) {
                console.error(error);
            }
        });
        if (navigationDiv) {
            const previewButton=navigationDiv.querySelector('#detect-button')
            if (!previewButton){
                parentElement.appendChild(detectButton)
            }
        }
    }


    function onPageLoad() {

        const sceneFooter=document.getElementsByClassName('scene-footer')[0]
        const observer = new MutationObserver(function(mutationsList) {
            for (let mutation of mutationsList) {
                if (mutation) addCustomButton()};
        });
        const config = { childList: true, subtree: true, attributes: true };

        observer.observe(sceneFooter, config);
        setTimeout(function() {
            addCustomButton();
        }, 1000);
    }


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

})();