// ==UserScript== // @name Google Street View Panorama Info // @namespace https://greasyfork.org/users/1340965 // @version 1.12 // @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_log // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @icon https://www.google.com/s2/favicons?sz=64&domain=geohints.com // @connect nominatim.openstreetmap.org // @license MIT // @downloadURL none // ==/UserScript== /* jshint esversion: 11 */ (() => { 'use strict' /* globals */ let VERSION = GM_info.script.version, DEBUG_MODE = false // debug output functions function toLog(typ, msg) { if (DEBUG_MODE) { if(console && console[typ] && console.group && console.groupEnd) { console[typ](msg) } else { GM_log(typ + ": " + msg.toString()) } } } function log(msg) { toLog("log", msg); } function debug(msg) { toLog("debug", msg); } function error(msg) { toLog("error", msg); } // GM_xmlhttpRequest response info function responseInfo(r) { debug([ "", "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")); } function cloneNode(originalNode, value, isClickable) { let node = originalNode.cloneNode(true) node.querySelector('h2').innerText = value if (isClickable) { node.style.cursor = "pointer" node.onclick = () => GM_setClipboard(value) } return node } 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) { parseCoordinates(url).forEach(x => debug(x)) debug(...parsePanoId(url)) return [...parseCoordinates(url), ...parsePanoId(url)] } async function updateTitleCard(_, observer) { const addressElement = document.querySelector('.pB8Nmf') if (addressElement) { observer.disconnect() log('Starting DOM manipulation') let h2Element = document.createElement('h2') h2Element.setAttribute('class', 'lsdM5 fontBodySmall') h2Element.setAttribute('jsan', '7.lsdM5,7.fontBodySmall') let divElement = document.createElement('div') divElement.appendChild(h2Element) let [latitude, longitude, panoId] = [...parsePanoramaInfoFromUrl(window.location.href)] let referenceElement = addressElement.querySelector('div[jstcache="112"]') debug(referenceElement) let panoIdElement = cloneNode(divElement, panoId, true) addressElement.insertBefore(panoIdElement, referenceElement.nextSibling) let longitudeElement = cloneNode(divElement, longitude, true) addressElement.insertBefore(longitudeElement, referenceElement.nextSibling) let latitudeElement = cloneNode(divElement, latitude, true) addressElement.insertBefore(latitudeElement, referenceElement.nextSibling) let url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=en-US` const country = await getCountry(url) let countryElement = cloneNode(divElement, country, false) addressElement.insertBefore(countryElement, referenceElement.nextSibling) } } 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) } }); }); } const init = () => { const observer = new MutationObserver(updateTitleCard) observer.observe(document.body, { childList: true, subtree: true }) } init(); })();