// ==UserScript==
// @name Pano Date Detective
// @namespace https://greasyfork.org/users/1179204
// @version 1.3.1
// @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
// @downloadURL none
// ==/UserScript==
(function() {
let detectButton,downloadButton,previousListener,zoomLevel,w,h
let accuracy=2;
let dateSvg=``
let date_Svg=``
let iconSvg=``
let icon_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') {
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
}
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() {
var dateSpan
const navigationDiv = document.querySelector("[role='navigation']");
if (!navigationDiv) {
console.error('Navigation div not found inside titlecard');
return;
}
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:
'',
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() {
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 (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){
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);
})();