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-19 提交的版本,查看 最新版本

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