您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Get the exact time a Google Street View image was taken (recent coverage)
当前为
// ==UserScript== // @name Pano Date Detective // @namespace https://www.geoguessr.com/user/6494f9bbab07ca3ea843220f // @version 0.4 // @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(formattedTime){ timeDisplay.textContent = formattedTime; timeDispaly.style.display='block' } } 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; })();