Greasy Fork

Greasy Fork is available in English.

Pano Date Detective

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

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

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

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

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

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

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

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Pano Date Detective
// @namespace    http://greasyfork.icu/users/1179204
// @version      1.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/485785/magnifier.svg
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==
(function() {
    let accuracy=2;
    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 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);
         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');
                }
            }
        }


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

        navigationDiv.appendChild(button);
    }

    function onPageLoad() {

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

    }

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

})();