Greasy Fork

Greasy Fork is available in English.

Google Street View Panorama Info

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

目前为 2025-03-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         Google Street View Panorama Info
// @namespace    http://greasyfork.icu/users/1340965
// @version      1.13
// @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
// ==/UserScript==
/* jshint esversion: 11 */

(function () {
  'use strict';

  const DEBUG_MODE = false;

  function init() {
    log(
      `Starting userscript '${GM_info.script.name}' v${GM_info.script.version}`
    );
    waitForElement('.pB8Nmf > div:last-child', updateTitleCard, '#titlecard');
  }

  async function updateTitleCard(referenceElement) {
    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 countryNode = referenceElement.parentNode.insertBefore(
      cloneNode(divElement, 'Retrieving country...'),
      referenceElement
    );

    let url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&accept-language=en-US`;
    countryNode.querySelector('h2').innerText = await getCountry(url);

    referenceElement.parentNode.insertBefore(
      cloneNode(divElement, latitude, true),
      referenceElement
    );
    referenceElement.parentNode.insertBefore(
      cloneNode(divElement, longitude, true),
      referenceElement
    );
    referenceElement.parentNode.insertBefore(
      cloneNode(divElement, panoId, true),
      referenceElement
    );
  }

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

  function cloneNode(originalNode, value, isClickable = false) {
    let node = originalNode.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) {
    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')
    );
  }

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

  init();
})();