Google Street View Panorama Info

Displays the country name, coordinates, and panoId for a given Google Street View panorama

  1. // ==UserScript==
  2. // @name Google Street View Panorama Info
  3. // @namespace https://gf.qytechs.cn/users/1340965-zecageo
  4. // @version 1.15
  5. // @description Displays the country name, coordinates, and panoId for a given Google Street View panorama
  6. // @author ZecaGeo
  7. // @run-at document-end
  8. // @match https://www.google.com/maps/*
  9. // @match https://www.google.at/maps/*
  10. // @match https://www.google.ca/maps/*
  11. // @match https://www.google.de/maps/*
  12. // @match https://www.google.fr/maps/*
  13. // @match https://www.google.it/maps/*
  14. // @match https://www.google.ru/maps/*
  15. // @match https://www.google.co.uk/maps/*
  16. // @match https://www.google.co.jp/maps/*
  17. // @match https://www.google.com.br/maps/*
  18. // @exclude https://ogs.google.com/
  19. // @exclude https://account.google.com/
  20. // @grant GM_setClipboard
  21. // @grant GM_xmlhttpRequest
  22. // @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
  23. // @connect nominatim.openstreetmap.org
  24. // @license MIT
  25. // @copyright 2025, zecageo
  26. // ==/UserScript==
  27.  
  28. /* jshint esversion: 11 */
  29.  
  30. (function () {
  31. 'use strict';
  32.  
  33. const DEBUG_MODE = true;
  34.  
  35. function init() {
  36. console.info(
  37. `>>> Userscript '${GM_info.script.name}' v${GM_info.script.version} by ${GM_info.script.author} <<<`
  38. );
  39. waitForElement('.pB8Nmf', updateTitleCard, '#titlecard');
  40. }
  41.  
  42. async function updateTitleCard(referenceElement) {
  43. const lastChild = referenceElement.lastElementChild;
  44. const panorama = new StreetViewPanorama(window.location.href);
  45.  
  46. referenceElement.insertBefore(
  47. cloneNode(await getCountry(panorama.latitude, panorama.longitude)),
  48. lastChild
  49. );
  50. referenceElement.insertBefore(
  51. cloneNode(panorama.latitude, true),
  52. lastChild
  53. );
  54. referenceElement.insertBefore(
  55. cloneNode(panorama.longitude, true),
  56. lastChild
  57. );
  58. referenceElement.insertBefore(cloneNode(panorama.panoId, true), lastChild);
  59.  
  60. panorama.shareLink = await getShareLink(
  61. panorama.createShareLinkRequestUrl()
  62. );
  63. log(`Share link: ${panorama.shareLink}`);
  64.  
  65. if (!referenceElement) {
  66. error('Reference element not found.');
  67. return;
  68. }
  69. referenceElement.insertBefore(
  70. cloneNode(panorama.shareLink, true),
  71. lastChild
  72. );
  73. }
  74.  
  75. /*
  76. * StreetViewPanorama class
  77. * This class is used to parse and store information about a Google Street View panorama.
  78. * @param {string} url - The URL to parse.
  79. * @returns {Array} - An array containing the latitude, longitude, and panoId.
  80. */
  81. class StreetViewPanorama {
  82. url;
  83. latitude;
  84. longitude;
  85. panoId;
  86. thumb;
  87. fov;
  88. heading;
  89. pitch;
  90. shareLink;
  91.  
  92. constructor(url) {
  93. this.url = url;
  94. this.initPanoramaData();
  95. }
  96.  
  97. initPanoramaData() {
  98. const shortPanoramaRegex = new RegExp(
  99. /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m\d+!1e\d+!3m\d+!1s([^!]+)!2e/
  100. );
  101. const matches = shortPanoramaRegex.exec(this.url);
  102.  
  103. if (matches && matches.length === 4) {
  104. this.latitude = parseFloat(matches[1]);
  105. this.longitude = parseFloat(matches[2]);
  106. this.panoId = matches[3];
  107. log('Panorama Data:');
  108. log(this);
  109. } else {
  110. error('Invalid Google Street View URL format.', matches);
  111. }
  112. }
  113.  
  114. createShareLinkRequestUrl() {
  115. const shareLinkRequestUrl = new URL(
  116. 'https://www.google.com/maps/rpc/shorturl'
  117. );
  118. const tempUrl = new URL(this.url);
  119. const pathname = tempUrl.pathname;
  120. const pathnameRegex = new RegExp(
  121. /@([^,]+),([^,]+),(\d+)a,([^y]+)y,([^h]+)h,([^t]+)t/
  122. );
  123. const matches = pathnameRegex.exec(pathname);
  124. if (matches && matches.length === 7) {
  125. this.latitude = parseFloat(matches[1]);
  126. this.longitude = parseFloat(matches[2]);
  127. this.thumb = parseInt(matches[3]);
  128. this.fov = parseFloat(matches[4]);
  129. this.heading = parseFloat(matches[5]);
  130. this.pitch = parseFloat(matches[6]);
  131. } else {
  132. error('Invalid Google Street View URL format.', matches);
  133. }
  134.  
  135. // const pb = `!1shttps://www.google.com/maps/@${this.latitude},${
  136. // this.longitude
  137. // },${this.thumb}a,${this.fov}y,${this.heading}h,${
  138. // this.pitch
  139. // }t/data=*213m7*211e1*213m5*211s${
  140. // this.panoId
  141. // }*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D${
  142. // this.pitch - 90
  143. // }%26panoid%3D${this.panoId}%26yaw%3D${
  144. // this.heading
  145. // }*217i16384*218i8192?entry=tts!2m1!7e81!6b1`;
  146.  
  147. const pb = `!1shttps://www.google.com/maps/@?api=1&map_action=pano&pano=${this.panoId}&heading=${this.heading}&pitch=${this.pitch}&fov=${this.fov}?entry=tts!2m1!7e81!6b1`;
  148. log(`pb parameter: ${pb}`);
  149.  
  150. const shareLinkSearchParams = new URLSearchParams({
  151. pb: pb,
  152. }).toString();
  153. shareLinkRequestUrl.search = shareLinkSearchParams;
  154.  
  155. log(`Share link request url: ${shareLinkRequestUrl.href}`);
  156. return shareLinkRequestUrl.href;
  157. }
  158. }
  159.  
  160. StreetViewPanorama.prototype.toString = function () {
  161. return `Panorama: { panoId: ${this.panoId}, latitude: ${this.latitude}, longitude: ${this.longitude} }`;
  162. };
  163.  
  164. // Network functions
  165. async function getCountry(latitude, longitude) {
  166. const url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=en-US`;
  167. const response = await promiseRequest('GET', url);
  168. const data = JSON.parse(response.responseText);
  169. return data?.address?.country ?? 'Country not found';
  170. }
  171.  
  172. async function getShareLink(shareLinkRequestUrl) {
  173. const response = await promiseRequest('GET', shareLinkRequestUrl);
  174. const rawText = response.responseText;
  175. return rawText.substring(7, rawText.length - 2);
  176. }
  177.  
  178. function promiseRequest(method, url) {
  179. log(
  180. [
  181. '---PROMISEREQUEST---',
  182. '\tmethod: ' + method,
  183. '\turl: ' + url,
  184. '---PROMISEREQUEST---',
  185. ].join('\n')
  186. );
  187.  
  188. return new Promise((resolve, reject) => {
  189. GM_xmlhttpRequest({
  190. method: method,
  191. url: url,
  192. onload: (result) => {
  193. responseInfo(result);
  194. if (result.status >= 200 && result.status < 300) {
  195. resolve(result);
  196. } else {
  197. reject(result.responseText);
  198. }
  199. },
  200. ontimeout: () => {
  201. let l = new URL(url);
  202. reject(
  203. ' timeout detected: "no answer from ' +
  204. l.host +
  205. ' for ' +
  206. l.timeout / 1000 +
  207. 's"'
  208. );
  209. },
  210. onerror: (result) => {
  211. // network error
  212. responseInfo(result);
  213. reject(
  214. ' error: ' + result.status + ', message: ' + result.statusText
  215. );
  216. },
  217. });
  218. });
  219. }
  220.  
  221. function responseInfo(r) {
  222. log(
  223. [
  224. '',
  225. 'finalUrl: \t\t' + (r.finalUrl || '-'),
  226. 'status: \t\t' + (r.status || '-'),
  227. 'statusText: \t' + (r.statusText || '-'),
  228. 'readyState: \t' + (r.readyState || '-'),
  229. 'responseHeaders: ' +
  230. (r.responseHeaders.replaceAll('\r\n', ';') || '-'),
  231. 'responseText: \t' + (r.responseText || '-'),
  232. ].join('\n')
  233. );
  234. }
  235.  
  236. // DOM manipulation functions
  237. function cloneNode(value, isClickable = false) {
  238. let h2Element = document.createElement('h2');
  239. h2Element.setAttribute('class', 'lsdM5 fontBodySmall');
  240.  
  241. let divElement = document.createElement('div');
  242. divElement.appendChild(h2Element);
  243.  
  244. let node = divElement.cloneNode(true);
  245. node.querySelector('h2').innerText = value;
  246. if (isClickable) {
  247. node.style.cursor = 'pointer';
  248. node.onclick = () => GM_setClipboard(value);
  249. }
  250. return node;
  251. }
  252.  
  253. const waitForElement = (selector, callback, targetNode) => {
  254. new MutationObserver((mutationsList, observer) => {
  255. for (const mutation of mutationsList) {
  256. if (mutation.type === 'childList') {
  257. const element = document.querySelector(selector);
  258. if (element) {
  259. observer.disconnect();
  260. callback(element);
  261. return;
  262. }
  263. }
  264. }
  265. }).observe(
  266. targetNode ? document.querySelector(targetNode) : document.body,
  267. {
  268. childList: true,
  269. subtree: true,
  270. }
  271. );
  272. };
  273.  
  274. // debug output functions
  275. const toLog = (level, msg) => {
  276. if (DEBUG_MODE) {
  277. console[level](msg);
  278. }
  279. };
  280.  
  281. const log = (msg) => {
  282. toLog('log', msg);
  283. };
  284.  
  285. const error = (msg) => {
  286. toLog('error', msg);
  287. };
  288.  
  289. init();
  290. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址