您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Find the exact time a Google Street View image was taken (recent coverage)
当前为
// ==UserScript== // @name Pano Detective // @namespace https://greasyfork.org/users/1179204 // @version 1.3.4 // @description Find the exact time a Google Street View image was taken (recent coverage) // @author KaKa // @match *://www.google.com/* // @icon https://www.svgrepo.com/show/485785/magnifier.svg // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function() { let dateSpan,detectButton,downloadButton,previousListener,zoomLevel,w,h let accuracy=2; let type=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>` let iconSvg=`<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="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#9AA0a6" stroke-width="2.16" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>` let icon_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="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#ffffff" stroke-width="2.16" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>` const iconUrl=svgToUrl(iconSvg) const icon_Url=svgToUrl(icon_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') { const length=coorData.length if (length>22){ type=10 } payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[type,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,[[[type,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 } async function downloadPanoramaImage(panoId, fileName,panoramaWidth,panoramaHeight) { return new Promise(async (resolve, reject) => { try { const imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=1&fover=2`; const tileWidth = 512; const tileHeight = 512; const zoomTiles=[2,4,8,16,32] const tilesPerRow = Math.min(Math.ceil(panoramaWidth / tileWidth),zoomTiles[zoomLevel-1]); const tilesPerColumn = Math.min(Math.ceil(panoramaHeight / tileHeight),zoomTiles[zoomLevel-1]/2); const canvasWidth = tilesPerRow * tileWidth; const canvasHeight = tilesPerColumn * tileHeight; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = canvasWidth; canvas.height = canvasHeight; for (let y = 0; y < tilesPerColumn; y++) { for (let x = 0; x < tilesPerRow; x++) { const tileUrl = `${imageUrl}&x=${x}&y=${y}`; const tile = await loadImage(tileUrl); ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight); } } 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(); window.URL.revokeObjectURL(url); resolve(); }, 'image/jpeg'); } catch (error) { Swal.fire('Error!', error.toString(),'error'); 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; }); } 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() { 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"); } if (!downloadButton){ downloadButton = document.createElement("button"); } const symbol=document.querySelector("[jsaction='titlecard.spotlight']") const buttonContainer = symbol.parentNode; detectButton.id = 'detect-button'; setupButton(detectButton, svgUrl); downloadButton.id = 'download-button'; setupButton(downloadButton, iconUrl); downloadButton.style.marginLeft = '5px'; if (!previousListener){ previousListener=true addButtonHoverEffect(detectButton, svg_Url, svgUrl); addButtonHoverEffect(downloadButton, icon_Url, iconUrl); downloadButton.addEventListener("click",async function(){ const { value: zoom, dismiss: inputDismiss } = await Swal.fire({ title: 'Zoom Level', html: '<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' + '<option value="1">1 (100KB~500KB)</option>' + '<option value="2">2 (500KB~1MB)</option>' + '<option value="3">3 (1MB~4MB)</option>' + '<option value="4">4 (4MB~8MB)</option>' + '<option value="5">5 (8MB~15MB)</option>' + '</select>', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', preConfirm: () => { return document.getElementById('zoom-select').value; } }); if (zoom){ zoomLevel=parseInt(zoom) const currentUrl = window.location.href; var panoId=extractParams(currentUrl).panoId; const metaData = await UE('GetMetadata', panoId); if (!metaData) { console.error('Failed to get metadata'); return; } try{ w=parseInt(metaData[1][0][2][2][1]) h=parseInt(metaData[1][0][2][2][0]) } catch (error){ try{ w=parseInt(metaData[1][2][2][1]) h=parseInt(metaData[1][2][2][0]) } catch (error){ console.log(error) return } } if(w&&h){ const fileName = `${panoId}.jpg`; const swal = Swal.fire({ title: 'Downloading', text: 'Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); await downloadPanoramaImage(panoId, fileName,w,h); swal.close() Swal.fire('Success!','Download completed', 'success'); } } }) 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; if (panoId.length>22)type=3 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) { dateSpan.textContent='unknown' 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){ buttonContainer.appendChild(detectButton) } const previewButton_=navigationDiv.querySelector('#download-button') if (!previewButton_){ buttonContainer.appendChild(downloadButton) } } } function setupButton(button, backgroundUrl) { button.style.backgroundImage = `url(${backgroundUrl})`; button.style.backgroundSize = 'cover'; button.style.backgroundPosition = 'center'; button.style.display = 'block'; button.style.width = '24px'; button.style.height = '24px'; button.style.fontSize = '12px'; button.style.borderRadius = '10px'; button.style.cursor = 'pointer'; button.style.backgroundColor = 'transparent'; } function addButtonHoverEffect(button, hoverImageUrl, defaultImageUrl) { button.addEventListener("mouseenter", function(event) { button.style.backgroundImage = `url(${hoverImageUrl})`; }); button.addEventListener("mouseleave", function(event) { button.style.backgroundImage = `url(${defaultImageUrl})`; }); } 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); })();