Google Street View Panorama Info

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

// ==UserScript==
// @name         Google Street View Panorama Info
// @namespace    https://gf.qytechs.cn/users/1340965-zecageo
// @version      1.15
// @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/*
// @match        https://www.google.ca/maps/*
// @match        https://www.google.de/maps/*
// @match        https://www.google.fr/maps/*
// @match        https://www.google.it/maps/*
// @match        https://www.google.ru/maps/*
// @match        https://www.google.co.uk/maps/*
// @match        https://www.google.co.jp/maps/*
// @match        https://www.google.com.br/maps/*
// @exclude      https://ogs.google.com/
// @exclude      https://account.google.com/
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.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 lastChild = referenceElement.lastElementChild;
    const panorama = new StreetViewPanorama(window.location.href);

    referenceElement.insertBefore(
      cloneNode(await getCountry(panorama.latitude, panorama.longitude)),
      lastChild
    );
    referenceElement.insertBefore(
      cloneNode(panorama.latitude, true),
      lastChild
    );
    referenceElement.insertBefore(
      cloneNode(panorama.longitude, true),
      lastChild
    );
    referenceElement.insertBefore(cloneNode(panorama.panoId, true), lastChild);

    panorama.shareLink = await getShareLink(
      panorama.createShareLinkRequestUrl()
    );
    log(`Share link: ${panorama.shareLink}`);

    if (!referenceElement) {
      error('Reference element not found.');
      return;
    }
    referenceElement.insertBefore(
      cloneNode(panorama.shareLink, true),
      lastChild
    );
  }

  /*
   * StreetViewPanorama class
   * This class is used to parse and store information about a Google Street View panorama.
   * @param {string} url - The URL to parse.
   * @returns {Array} - An array containing the latitude, longitude, and panoId.
   */
  class StreetViewPanorama {
    url;
    latitude;
    longitude;
    panoId;
    thumb;
    fov;
    heading;
    pitch;
    shareLink;

    constructor(url) {
      this.url = url;
      this.initPanoramaData();
    }

    initPanoramaData() {
      const shortPanoramaRegex = new RegExp(
        /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m\d+!1e\d+!3m\d+!1s([^!]+)!2e/
      );
      const matches = shortPanoramaRegex.exec(this.url);

      if (matches && matches.length === 4) {
        this.latitude = parseFloat(matches[1]);
        this.longitude = parseFloat(matches[2]);
        this.panoId = matches[3];
        log('Panorama Data:');
        log(this);
      } else {
        error('Invalid Google Street View URL format.', matches);
      }
    }

    createShareLinkRequestUrl() {
      const shareLinkRequestUrl = new URL(
        'https://www.google.com/maps/rpc/shorturl'
      );
      const tempUrl = new URL(this.url);
      const pathname = tempUrl.pathname;
      const pathnameRegex = new RegExp(
        /@([^,]+),([^,]+),(\d+)a,([^y]+)y,([^h]+)h,([^t]+)t/
      );
      const matches = pathnameRegex.exec(pathname);
      if (matches && matches.length === 7) {
        this.latitude = parseFloat(matches[1]);
        this.longitude = parseFloat(matches[2]);
        this.thumb = parseInt(matches[3]);
        this.fov = parseFloat(matches[4]);
        this.heading = parseFloat(matches[5]);
        this.pitch = parseFloat(matches[6]);
      } else {
        error('Invalid Google Street View URL format.', matches);
      }

      // const pb = `!1shttps://www.google.com/maps/@${this.latitude},${
      //   this.longitude
      // },${this.thumb}a,${this.fov}y,${this.heading}h,${
      //   this.pitch
      // }t/data=*213m7*211e1*213m5*211s${
      //   this.panoId
      // }*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D${
      //   this.pitch - 90
      // }%26panoid%3D${this.panoId}%26yaw%3D${
      //   this.heading
      // }*217i16384*218i8192?entry=tts!2m1!7e81!6b1`;

      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`;
      log(`pb parameter: ${pb}`);

      const shareLinkSearchParams = new URLSearchParams({
        pb: pb,
      }).toString();
      shareLinkRequestUrl.search = shareLinkSearchParams;

      log(`Share link request url: ${shareLinkRequestUrl.href}`);
      return shareLinkRequestUrl.href;
    }
  }

  StreetViewPanorama.prototype.toString = function () {
    return `Panorama: { panoId: ${this.panoId}, latitude: ${this.latitude}, longitude: ${this.longitude} }`;
  };

  // Network functions
  async function getCountry(latitude, longitude) {
    const url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=en-US`;
    const response = await promiseRequest('GET', url);
    const data = JSON.parse(response.responseText);
    return data?.address?.country ?? 'Country not found';
  }

  async function getShareLink(shareLinkRequestUrl) {
    const response = await promiseRequest('GET', shareLinkRequestUrl);
    const rawText = response.responseText;
    return rawText.substring(7, rawText.length - 2);
  }

  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) {
            resolve(result);
          } else {
            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
          );
        },
      });
    });
  }

  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')
    );
  }

  // DOM manipulation functions
  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;
  }

  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
  const toLog = (level, msg) => {
    if (DEBUG_MODE) {
      console[level](msg);
    }
  };

  const log = (msg) => {
    toLog('log', msg);
  };

  const error = (msg) => {
    toLog('error', msg);
  };

  init();
})();

QingJ © 2025

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