Greasy Fork is available in English.
Displays the country name, coordinates, and panoId for a given Google Street View panorama
当前为
// ==UserScript==
// @name Google Street View Panorama Info
// @namespace http://greasyfork.icu/users/1340965-zecageo
// @version 1.14
// @description Displays the country name, coordinates, and panoId for a given Google Street View panorama
// @author ZecaGeo
// @run-at document-end
// @match https://www.google.com/maps/*
// @match https://www.google.at/maps/*
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @icon https://www.google.com/s2/favicons?sz=64&domain=geohints.com
// @connect nominatim.openstreetmap.org
// @license MIT
// @copyright 2025, zecageo
// ==/UserScript==
/* jshint esversion: 11 */
(function () {
'use strict';
const DEBUG_MODE = true;
function init() {
console.info(
`>>> Userscript '${GM_info.script.name}' v${GM_info.script.version} by ${GM_info.script.author} <<<`
);
waitForElement('.pB8Nmf', updateTitleCard, '#titlecard');
}
async function updateTitleCard(referenceElement) {
const [latitude, longitude, panoId] = [
...parsePanoramaInfoFromUrl(window.location.href),
];
const lastChild = referenceElement.lastElementChild;
let h2Element = document.createElement('h2');
h2Element.setAttribute('class', 'lsdM5 fontBodySmall');
let divElement = document.createElement('div');
divElement.appendChild(h2Element);
const url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=en-US`;
referenceElement.insertBefore(cloneNode(await getCountry(url)), lastChild);
referenceElement.insertBefore(cloneNode(latitude, true), lastChild);
referenceElement.insertBefore(cloneNode(longitude, true), lastChild);
referenceElement.insertBefore(cloneNode(panoId, true), lastChild);
}
function parseCoordinates(url) {
const regex_coordinates = new RegExp(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
return regex_coordinates.exec(url).slice(1);
}
function parsePanoId(url) {
const regex_panoId = new RegExp(/!1s(.+)!2e/);
return regex_panoId.exec(url).slice(1);
}
function parsePanoramaInfoFromUrl(url) {
console.group('Parsing panorama info from URL:');
parseCoordinates(url).forEach((x) => log(x));
log(...parsePanoId(url));
console.groupEnd();
return [...parseCoordinates(url), ...parsePanoId(url)];
}
function cloneNode(value, isClickable = false) {
let h2Element = document.createElement('h2');
h2Element.setAttribute('class', 'lsdM5 fontBodySmall');
let divElement = document.createElement('div');
divElement.appendChild(h2Element);
let node = divElement.cloneNode(true);
node.querySelector('h2').innerText = value;
if (isClickable) {
node.style.cursor = 'pointer';
node.onclick = () => GM_setClipboard(value);
}
return node;
}
async function getCountry(url) {
const response = await promiseRequest('GET', url);
const data = JSON.parse(response.responseText);
return data?.address?.country ?? 'Country not found';
}
function promiseRequest(method, url) {
log(
[
'---PROMISEREQUEST---',
'\tmethod: ' + method,
'\turl: ' + url,
'---PROMISEREQUEST---',
].join('\n')
);
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
onload: (result) => {
responseInfo(result);
if (result.status >= 200 && result.status < 300) {
// ok
resolve(result);
} else {
// error
reject(result.responseText);
}
},
ontimeout: () => {
let l = new URL(url);
reject(
' timeout detected: "no answer from ' +
l.host +
' for ' +
l.timeout / 1000 +
's"'
);
},
onerror: (result) => {
// network error
responseInfo(result);
reject(
' error: ' + result.status + ', message: ' + result.statusText
);
},
});
});
}
// GM_xmlhttpRequest response info
function responseInfo(r) {
log(
[
'',
'finalUrl: \t\t' + (r.finalUrl || '-'),
'status: \t\t' + (r.status || '-'),
'statusText: \t' + (r.statusText || '-'),
'readyState: \t' + (r.readyState || '-'),
'responseHeaders: ' +
(r.responseHeaders.replaceAll('\r\n', ';') || '-'),
'responseText: \t' + (r.responseText || '-'),
].join('\n')
);
}
const waitForElement = (selector, callback, targetNode) => {
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
callback(element);
return;
}
}
}
}).observe(
targetNode ? document.querySelector(targetNode) : document.body,
{
childList: true,
subtree: true,
}
);
};
// debug output functions
function toLog(level, msg) {
if (DEBUG_MODE) {
console[level](msg);
}
}
function log(msg) {
toLog('log', msg);
}
function debug(msg) {
toLog('debug', msg);
}
function dir(obj) {
toLog('dir', obj);
}
init();
})();