// ==UserScript==
// @name Pano Date Detective
// @namespace https://greasyfork.org/users/1179204
// @version 1.2.2
// @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
// @grant GM_xmlhttpRequest
// @license MIT
// @downloadURL none
// ==/UserScript==
(function() {
let detectButton
let accuracy=2;
let dateSvg=``
let date_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("[jsaction='titlecard.spotlight']")
const buttonContainer = 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){
buttonContainer.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);
})();