Geonections Extension

Geonections Mode and Final Check for map-making.app

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Geonections Extension
// @namespace    https://geonections.com/
// @version      1.6.0
// @description  Geonections Mode and Final Check for map-making.app
// @author       Geonections
// @match        https://map-making.app/maps/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";
  function run(code) {
    var s = document.createElement("script");
    s.textContent = code;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
  }
  var contentCode = "// Geonections Extension – content script (runs in page context on map-making.app/maps/*)\n\n(function () {\n  \"use strict\";\n\n  /* ------------------------------------------------------------------------------- */\n  /* ----- KEYBOARD SHORTCUTS (Geonections Mode must be ON – refresh page for changes) */\n  /* ------------------------------------------------------------------------------- */\n\n  const KEYBOARD_SHORTCUTS = {\n    // Navigation\n    prevLocation: \"ArrowLeft\",\n    nextLocation: \"ArrowRight\",\n\n    // Autoplay\n    autoplayToggle: \"Space\",\n\n    // Difficulty tags (single key)\n    difficultyEasy: \"1\",\n    difficultyMedium: \"2\",\n    difficultyHard: \"3\",\n    difficultyExpert: \"4\",\n\n    // Actions (single key, no modifier)\n    tagCountry: \"T\",\n    hideUi: \"H\",\n    deleteLoc: \"C\",\n    openInTabs: \"O\",\n    tagUsable: \"U\",\n    selectAll: \"A\",\n    saveCloseLoc: \"S\",\n\n    // Copy link (modifier + key)\n    copyMapsLink: \"c\", // with Ctrl (Windows/Linux) or Cmd (Mac)\n  };\n\n  // Autoplay delay (when autoplay is ON): 1–9 = 1–9 sec, 0 = 10 sec;\n  // - = 15 sec, = = 30 sec, ] = 60 sec;\n  // Shift+1 = 15 sec, Shift+2..Shift+9 = 20–90 sec, Shift+0 = 60 sec.\n\n  /* ############################################################################### */\n  /* ##### DON'T MODIFY BELOW UNLESS YOU KNOW WHAT YOU ARE DOING ################### */\n  /* ############################################################################### */\n\n  function addStyle(css) {\n    const el = document.createElement(\"style\");\n    el.textContent = css;\n    document.head.appendChild(el);\n    return el;\n  }\n\n  // Clipboard for Auto-Tag (Geoguessr Map-Making Auto-Tag script)\n  function fallbackCopy(text) {\n    const ta = document.createElement(\"textarea\");\n    ta.value = text;\n    ta.style.position = \"fixed\";\n    ta.style.left = \"-9999px\";\n    document.body.appendChild(ta);\n    ta.select();\n    document.execCommand(\"copy\");\n    document.body.removeChild(ta);\n  }\n  window.setClipboard = function (text) {\n    if (navigator.clipboard && navigator.clipboard.writeText) {\n      navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));\n    } else {\n      fallbackCopy(text);\n    }\n  };\n\n  // Country code (ISO 3166-1 alpha-2) to full name for Geonections\n  const COUNTRY_CODE_TO_NAME = {\n    AD: \"Andorra\", AE: \"United Arab Emirates\", AF: \"Afghanistan\", AG: \"Antigua and Barbuda\", AI: \"Anguilla\",\n    AL: \"Albania\", AM: \"Armenia\", AO: \"Angola\", AQ: \"Antarctica\", AR: \"Argentina\", AS: \"American Samoa\",\n    AT: \"Austria\", AU: \"Australia\", AW: \"Aruba\", AX: \"Åland Islands\", AZ: \"Azerbaijan\", BA: \"Bosnia and Herzegovina\",\n    BB: \"Barbados\", BD: \"Bangladesh\", BE: \"Belgium\", BF: \"Burkina Faso\", BG: \"Bulgaria\", BH: \"Bahrain\",\n    BI: \"Burundi\", BJ: \"Benin\", BL: \"Saint Barthélemy\", BM: \"Bermuda\", BN: \"Brunei\", BO: \"Bolivia\",\n    BQ: \"Caribbean Netherlands\", BR: \"Brazil\", BS: \"Bahamas\", BT: \"Bhutan\", BV: \"Bouvet Island\", BW: \"Botswana\",\n    BY: \"Belarus\", BZ: \"Belize\", CA: \"Canada\", CC: \"Cocos (Keeling) Islands\", CD: \"DR Congo\", CF: \"Central African Republic\",\n    CG: \"Republic of the Congo\", CH: \"Switzerland\", CI: \"Côte d'Ivoire\", CK: \"Cook Islands\", CL: \"Chile\",\n    CM: \"Cameroon\", CN: \"China\", CO: \"Colombia\", CR: \"Costa Rica\", CU: \"Cuba\", CV: \"Cape Verde\", CW: \"Curaçao\",\n    CX: \"Christmas Island\", CY: \"Cyprus\", CZ: \"Czech Republic\", DE: \"Germany\", DJ: \"Djibouti\", DK: \"Denmark\",\n    DM: \"Dominica\", DO: \"Dominican Republic\", DZ: \"Algeria\", EC: \"Ecuador\", EE: \"Estonia\", EG: \"Egypt\",\n    EH: \"Western Sahara\", ER: \"Eritrea\", ES: \"Spain\", ET: \"Ethiopia\", FI: \"Finland\", FJ: \"Fiji\",\n    FK: \"Falkland Islands\", FM: \"Micronesia\", FO: \"Faroe Islands\", FR: \"France\", GA: \"Gabon\", GB: \"United Kingdom\",\n    GD: \"Grenada\", GE: \"Georgia\", GF: \"French Guiana\", GG: \"Guernsey\", GH: \"Ghana\", GI: \"Gibraltar\",\n    GL: \"Greenland\", GM: \"Gambia\", GN: \"Guinea\", GP: \"Guadeloupe\", GQ: \"Equatorial Guinea\", GR: \"Greece\",\n    GS: \"South Georgia and the South Sandwich Islands\", GT: \"Guatemala\", GU: \"Guam\", GW: \"Guinea-Bissau\",\n    GY: \"Guyana\", HK: \"Hong Kong\", HM: \"Heard Island and McDonald Islands\", HN: \"Honduras\", HR: \"Croatia\",\n    HT: \"Haiti\", HU: \"Hungary\", ID: \"Indonesia\", IE: \"Ireland\", IL: \"Israel\", IM: \"Isle of Man\",\n    IN: \"India\", IO: \"British Indian Ocean Territory\", IQ: \"Iraq\", IR: \"Iran\", IS: \"Iceland\", IT: \"Italy\",\n    JE: \"Jersey\", JM: \"Jamaica\", JO: \"Jordan\", JP: \"Japan\", KE: \"Kenya\", KG: \"Kyrgyzstan\", KH: \"Cambodia\",\n    KI: \"Kiribati\", KM: \"Comoros\", KN: \"Saint Kitts and Nevis\", KP: \"North Korea\", KR: \"South Korea\",\n    KW: \"Kuwait\", KY: \"Cayman Islands\", KZ: \"Kazakhstan\", LA: \"Laos\", LB: \"Lebanon\", LC: \"Saint Lucia\",\n    LI: \"Liechtenstein\", LK: \"Sri Lanka\", LR: \"Liberia\", LS: \"Lesotho\", LT: \"Lithuania\", LU: \"Luxembourg\",\n    LV: \"Latvia\", LY: \"Libya\", MA: \"Morocco\", MC: \"Monaco\", MD: \"Moldova\", ME: \"Montenegro\", MF: \"Saint Martin\",\n    MG: \"Madagascar\", MH: \"Marshall Islands\", MK: \"North Macedonia\", ML: \"Mali\", MM: \"Myanmar\", MN: \"Mongolia\",\n    MO: \"Macau\", MP: \"Northern Mariana Islands\", MQ: \"Martinique\", MR: \"Mauritania\", MS: \"Montserrat\",\n    MT: \"Malta\", MU: \"Mauritius\", MV: \"Maldives\", MW: \"Malawi\", MX: \"Mexico\", MY: \"Malaysia\", MZ: \"Mozambique\",\n    NA: \"Namibia\", NC: \"New Caledonia\", NE: \"Niger\", NF: \"Norfolk Island\", NG: \"Nigeria\", NI: \"Nicaragua\",\n    NL: \"Netherlands\", NO: \"Norway\", NP: \"Nepal\", NR: \"Nauru\", NU: \"Niue\", NZ: \"New Zealand\", OM: \"Oman\",\n    PA: \"Panama\", PE: \"Peru\", PF: \"French Polynesia\", PG: \"Papua New Guinea\", PH: \"Philippines\", PK: \"Pakistan\",\n    PL: \"Poland\", PM: \"Saint Pierre and Miquelon\", PN: \"Pitcairn Islands\", PR: \"Puerto Rico\", PS: \"Palestine\",\n    PT: \"Portugal\", PW: \"Palau\", PY: \"Paraguay\", QA: \"Qatar\", RE: \"Réunion\", RO: \"Romania\", RS: \"Serbia\",\n    RU: \"Russia\", RW: \"Rwanda\", SA: \"Saudi Arabia\", SB: \"Solomon Islands\", SC: \"Seychelles\", SD: \"Sudan\",\n    SE: \"Sweden\", SG: \"Singapore\", SH: \"Saint Helena, Ascension and Tristan da Cunha\", SI: \"Slovenia\",\n    SJ: \"Svalbard and Jan Mayen\", SK: \"Slovakia\", SL: \"Sierra Leone\", SM: \"San Marino\", SN: \"Senegal\",\n    SO: \"Somalia\", SR: \"Suriname\", SS: \"South Sudan\", ST: \"São Tomé and Príncipe\", SV: \"El Salvador\",\n    SX: \"Sint Maarten\", SY: \"Syria\", SZ: \"Eswatini\", TC: \"Turks and Caicos Islands\", TD: \"Chad\", TF: \"French Southern and Antarctic Lands\",\n    TG: \"Togo\", TH: \"Thailand\", TJ: \"Tajikistan\", TK: \"Tokelau\", TL: \"Timor-Leste\", TM: \"Turkmenistan\",\n    TN: \"Tunisia\", TO: \"Tonga\", TR: \"Turkey\", TT: \"Trinidad and Tobago\", TV: \"Tuvalu\", TW: \"Taiwan\",\n    TZ: \"Tanzania\", UA: \"Ukraine\", UG: \"Uganda\", UM: \"United States Minor Outlying Islands\", US: \"USA\",\n    UY: \"Uruguay\", UZ: \"Uzbekistan\", VA: \"Vatican City\", VC: \"Saint Vincent and the Grenadines\", VE: \"Venezuela\",\n    VG: \"British Virgin Islands\", VI: \"United States Virgin Islands\", VN: \"Vietnam\", VU: \"Vanuatu\",\n    WF: \"Wallis and Futuna\", WS: \"Samoa\", YE: \"Yemen\", YT: \"Mayotte\", ZA: \"South Africa\", ZM: \"Zambia\", ZW: \"Zimbabwe\",\n  };\n\n  /** Territory / region code → sovereign country code. Tag these as the parent country. */\n  const TERRITORY_TO_COUNTRY = {\n    PR: \"US\",   /* Puerto Rico → USA */\n    GU: \"US\",   /* Guam */\n    VI: \"US\",   /* US Virgin Islands */\n    AS: \"US\",   /* American Samoa */\n    MP: \"US\",   /* Northern Mariana Islands */\n    UM: \"US\",   /* US Minor Outlying Islands */\n    RE: \"FR\",   /* Réunion → France */\n    YT: \"FR\",   /* Mayotte */\n    GF: \"FR\",   /* French Guiana */\n    GP: \"FR\",   /* Guadeloupe */\n    MQ: \"FR\",   /* Martinique */\n    NC: \"FR\",   /* New Caledonia */\n    PF: \"FR\",   /* French Polynesia */\n    PM: \"FR\",   /* Saint Pierre and Miquelon */\n    BL: \"FR\",   /* Saint Barthélemy */\n    MF: \"FR\",   /* Saint Martin (FR) */\n    WF: \"FR\",   /* Wallis and Futuna */\n    TF: \"FR\",   /* French Southern and Antarctic Lands */\n    FO: \"DK\",   /* Faroe Islands → Denmark */\n    GL: \"DK\",   /* Greenland */\n    AX: \"FI\",   /* Åland Islands → Finland */\n    VG: \"GB\",   /* British Virgin Islands → UK */\n    KY: \"GB\",   /* Cayman Islands */\n    BM: \"GB\",   /* Bermuda */\n    FK: \"GB\",   /* Falkland Islands */\n    GI: \"GB\",   /* Gibraltar */\n    IM: \"GB\",   /* Isle of Man */\n    JE: \"GB\",   /* Jersey */\n    GG: \"GB\",   /* Guernsey */\n    SH: \"GB\",   /* Saint Helena, Ascension and Tristan da Cunha */\n    TC: \"GB\",   /* Turks and Caicos */\n    IO: \"GB\",   /* British Indian Ocean Territory */\n    CW: \"NL\",   /* Curaçao → Netherlands */\n    BQ: \"NL\",   /* Caribbean Netherlands */\n    AW: \"NL\",   /* Aruba */\n    SX: \"NL\",   /* Sint Maarten */\n    HK: \"CN\",   /* Hong Kong → China */\n    MO: \"CN\",   /* Macau */\n    SJ: \"NO\",   /* Svalbard and Jan Mayen → Norway */\n  };\n\n  function countryNameForCode(code) {\n    if (!code) return null;\n    const sovereign = TERRITORY_TO_COUNTRY[code] || code;\n    return COUNTRY_CODE_TO_NAME[sovereign] || code;\n  }\n\n  async function fetchGooglePanorama(mode, coorData) {\n    try {\n      const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${mode}`;\n      let payload;\n      if (mode === \"GetMetadata\") {\n        payload = JSON.stringify([[\"apiv3\", null, null, null, \"US\", null, null, null, null, null, [[0]]], [\"en\", \"US\"], [[[2, coorData]]], [[1, 2, 3, 4, 8, 6]]]);\n      } else {\n        payload = JSON.stringify([[\"apiv3\"], [[null, null, coorData.lat, coorData.lng], 50], [null, [\"en\", \"US\"], null, null, null, null, null, null, [2], null, [[[2, true, 2]]]], [[1, 2, 3, 4, 8, 6]]]);\n      }\n      const response = await fetch(u, { method: \"POST\", headers: { \"content-type\": \"application/json+protobuf\", \"x-user-agent\": \"grpc-web-javascript/0.1\" }, body: payload, mode: \"cors\", credentials: \"omit\" });\n      if (!response.ok) return null;\n      const data = await response.json();\n      return data;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  function getCountryNameForLoc(loc) {\n    return new Promise((resolve) => {\n      const getCode = () => {\n        if (typeof google === \"undefined\" || !google.maps) {\n          resolve(null);\n          return;\n        }\n        const service = new google.maps.StreetViewService();\n        const opts = loc.panoId ? { pano: loc.panoId } : { location: { lat: loc.location.lat, lng: loc.location.lng }, radius: 50 };\n        service.getPanorama(opts, (data, status) => {\n          if (status !== \"OK\" || !data || !data.location || !data.location.description) {\n            fetchGooglePanorama(loc.panoId ? \"GetMetadata\" : \"SingleImageSearch\", loc.panoId || loc.location).then((apiData) => {\n              if (!apiData || !apiData[1]) {\n                resolve(null);\n                return;\n              }\n              try {\n                const cc = apiData[1][0] && apiData[1][0][5] && apiData[1][0][5][0] && apiData[1][0][5][0][1] && apiData[1][0][5][0][1][4];\n                resolve(cc ? countryNameForCode(cc) : null);\n              } catch (e) {\n                resolve(null);\n              }\n            });\n            return;\n          }\n          const parts = data.location.description.split(\",\").map((s) => s.trim());\n          const last = parts[parts.length - 1];\n          resolve(last && last.length === 2 ? countryNameForCode(last) : (COUNTRY_CODE_TO_NAME[last] || last));\n        });\n      };\n      getCode();\n    });\n  }\n\n  async function getCountryForCurrentLocation() {\n    if (typeof editor === \"undefined\" || !editor.currentLocation) return null;\n    const loc = editor.currentLocation.updatedProps || editor.currentLocation;\n    const code = await (async () => {\n      const res = await fetchGooglePanorama(loc.panoId ? \"GetMetadata\" : \"SingleImageSearch\", loc.panoId || loc.location);\n      if (!res || !res[1]) return null;\n      try {\n        const inner = res[1][0];\n        if (inner && inner[5] && inner[5][0] && inner[5][0][1] && inner[5][0][1][4]) return inner[5][0][1][4];\n        if (inner && inner[5] && inner[5][0] && inner[5][0][1]) return inner[5][0][1][4];\n      } catch (e) {}\n      return null;\n    })();\n    return code ? countryNameForCode(code) : null;\n  }\n\n  let selections, currentIndex;\n  let mapListener;\n  let isDrawing, isHidden;\n  let startX, startY, endX, endY;\n  let selectionBox;\n  let style;\n\n  let autoplayTimer = null;\n  let autoplayOn = false;\n  let autoplayDelayMs = 3000;\n  let geonectionsModeOn = false;\n  const DIFFICULTY_TAGS = { 1: \"Easy\", 2: \"Medium\", 3: \"Hard\", 4: \"Expert\" };\n  const COUNTRY_NAMES_SET = new Set(Object.values(COUNTRY_CODE_TO_NAME));\n\n  function startAutoplay(getActiveSelectionsFn) {\n    if (autoplayTimer) return;\n    autoplayOn = true;\n    autoplayTimer = setInterval(() => {\n      try {\n        const activeSelections = getActiveSelectionsFn();\n        switchLoc(activeSelections);\n      } catch (err) {\n        console.error(\"Autoplay error:\", err);\n        stopAutoplay();\n      }\n    }, autoplayDelayMs);\n    console.log(\"[Geonections] Autoplay ON\", autoplayDelayMs + \"ms\");\n    if (typeof updateGeonectionsLabel === \"function\") updateGeonectionsLabel();\n  }\n\n  function stopAutoplay() {\n    autoplayOn = false;\n    if (autoplayTimer) clearInterval(autoplayTimer);\n    autoplayTimer = null;\n    console.log(\"[Geonections] Autoplay OFF\");\n    if (typeof updateGeonectionsLabel === \"function\") updateGeonectionsLabel();\n  }\n\n  function googleMapsStreetViewUrl(loc, options) {\n    const lat = loc.location && loc.location.lat != null ? loc.location.lat : loc.lat;\n    const lng = loc.location && loc.location.lng != null ? loc.location.lng : loc.lng;\n    const heading = (loc.heading != null ? loc.heading : 0);\n    const zoom = (options && options.zoom !== undefined) ? options.zoom : (loc.zoom != null ? loc.zoom : 1);\n    const apiPitch = (loc.pitch != null ? loc.pitch : 0);\n    const pitchUrl = Math.max(0, Math.min(180, apiPitch + 90));\n    if (loc.panoId && lat != null && lng != null) {\n      return \"https://www.google.com/maps/@\"\n        + lat + \",\" + lng\n        + \",\" + zoom + \"a,75y,\" + heading + \"h,\" + pitchUrl + \"t/data=!3m5!1e1!3m3!1s\" + encodeURIComponent(loc.panoId) + \"!2e0!3e11\";\n    }\n    return \"https://www.google.com/maps?cbll=\" + lat + \",\" + lng + \"&cbp=\" + zoom + \",\" + heading + \",\" + pitchUrl + \",0,0,0&layer=c\";\n  }\n\n  function openLocationsInGoogleMapsTabs(locations, count, options) {\n    const list = Array.isArray(locations) ? locations : [];\n    const n = Math.min(list.length, count != null ? count : 16, 24);\n    list.slice(0, n).forEach((loc, i) => {\n      setTimeout(() => {\n        const url = googleMapsStreetViewUrl(loc, options);\n        window.open(url, \"_blank\", \"noopener,noreferrer\");\n      }, i * 150);\n    });\n  }\n\n  function openSelectedInTabs(count) {\n    const url = window.location.href.split(\"?\")[0].split(\"#\")[0];\n    const n = count != null ? Math.min(Math.max(1, count), 24) : 16;\n    for (let i = 0; i < n; i++) window.open(url, \"_blank\", \"noopener\");\n  }\n\n  function exportAsCsv(locs) {\n    const csvContent = jsonToCSV(locs);\n    downloadCSV(csvContent);\n  }\n\n  function downloadCSV(csvContent, fileName = \"output.csv\") {\n    const blob = new Blob([csvContent], { type: \"text/csv;charset=utf-8;\" });\n    const link = document.createElement(\"a\");\n    if (link.download !== undefined) {\n      const url = URL.createObjectURL(blob);\n      link.setAttribute(\"href\", url);\n      link.setAttribute(\"download\", fileName);\n      link.style.visibility = \"hidden\";\n      document.body.appendChild(link);\n      link.click();\n      document.body.removeChild(link);\n    }\n  }\n\n  function getTagsForRow(item, maxTags) {\n    const tags = item.tags || [];\n    return Array.from({ length: maxTags }, (_, index) => tags[index] || \"\");\n  }\n\n  function getFormattedDate(dateStr) {\n    if (!dateStr) return \"\";\n    const date = new Date(dateStr);\n    if (isNaN(date.getTime())) return \"\";\n    const year = date.getFullYear();\n    const month = String(date.getMonth() + 1).padStart(2, \"0\");\n    return `${year}-${month}`;\n  }\n\n  function getMaxTagCount(jsonData) {\n    let maxTags = 0;\n    jsonData.forEach((item) => {\n      if (item.tags && item.tags.length > maxTags) {\n        maxTags = item.tags.length;\n      }\n    });\n    return maxTags;\n  }\n\n  function jsonToCSV(jsonData) {\n    const maxTags = getMaxTagCount(jsonData);\n    const tagHeaders = Array.from({ length: maxTags }, (_, i) => `tag${i + 1}`);\n    const headers = [\"lat\", \"lng\", \"panoId\", \"heading\", \"pitch\", \"zoom\", \"date\", ...tagHeaders];\n    const rows = jsonData.map((item) => {\n      const lat = item.location.lat || \"\";\n      const lng = item.location.lng || \"\";\n      const panoId = item.panoId || \"\";\n      const heading = item.heading || \"\";\n      const pitch = item.pitch || \"\";\n      const zoom = item.zoom || \"\";\n      const date = getFormattedDate(item.panoDate) || \"\";\n      const tags = getTagsForRow(item, maxTags);\n      return [lat, lng, panoId, heading, pitch, zoom, date, ...tags];\n    });\n    const csvContent = [headers, ...rows].map((row) => row.join(\",\")).join(\"\\n\");\n    return csvContent;\n  }\n\n  function switchLoc(locs) {\n    const isReview = document.querySelector(\".review-header\");\n    if (isReview) {\n      const nextBtn = document.querySelector('[data-qa=\"review-next\"]');\n      if (nextBtn) nextBtn.click();\n    } else {\n      if (typeof editor !== \"undefined\" && editor.currentLocation) editor.closeLocation(editor.currentLocation.updatedProps);\n      if (!currentIndex) currentIndex = 1;\n      else {\n        currentIndex += 1;\n        if (currentIndex > locs.length) currentIndex = 1;\n      }\n      if (typeof editor !== \"undefined\") editor.openLocation(locs[currentIndex - 1]);\n      focusOnLoc(locs[currentIndex - 1]);\n    }\n  }\n\n  function rewindLoc(locs) {\n    const isReview = document.querySelector(\".review-header\");\n    if (isReview) {\n      const prevBtn = document.querySelector('[data-qa=\"review-prev\"]');\n      if (prevBtn) prevBtn.click();\n    } else {\n      if (typeof editor !== \"undefined\" && editor.currentLocation) editor.closeLocation(editor.currentLocation.updatedProps);\n      if (!currentIndex) currentIndex = 1;\n      else {\n        currentIndex -= 1;\n        if (currentIndex < 1) currentIndex = locs.length;\n      }\n      if (typeof editor !== \"undefined\") editor.openLocation(locs[currentIndex - 1]);\n    }\n  }\n\n  function focusOnLoc(loc) {\n    if (typeof map !== \"undefined\" && loc) {\n      map.setCenter(loc.location);\n      map.setZoom(16);\n    }\n  }\n\n  function deleteLoc(loc) {\n    const isReview = document.querySelector(\".review-header\");\n    if (isReview) {\n      const deleteButton = document.querySelector('[data-qa=\"location-delete\"]');\n      if (deleteButton) deleteButton.click();\n    } else if (typeof editor !== \"undefined\") editor.closeAndDeleteLocation(loc);\n  }\n\n  function copyLoc() {\n    if (typeof editor !== \"undefined\" && editor.currentLocation) editor.addLocation(editor.currentLocation.updatedProps);\n  }\n\n  function copyGoogleMapsLinkToClipboard() {\n    const copyLinkBtn = Array.from(document.querySelectorAll(\"button, [role='button'], a, div[onclick], [data-qa]\")).find(\n      (el) => /copy link/i.test((el.textContent || el.innerText || el.getAttribute(\"aria-label\") || \"\").trim())\n    );\n    if (copyLinkBtn) {\n      copyLinkBtn.click();\n    }\n  }\n\n  function selectAllLocations() {\n    const locs = typeof locations !== \"undefined\" ? locations : [];\n    if (!locs.length) return;\n    if (typeof editor === \"undefined\") return;\n    if (typeof editor.selectLocations === \"function\") {\n      editor.selectLocations(locs);\n      return;\n    }\n    if (typeof editor.setSelections === \"function\") {\n      editor.setSelections([{ key: JSON.stringify({ tagName: \"All\" }), locations: locs }]);\n      return;\n    }\n    const selectAllBtn = Array.from(document.querySelectorAll(\"button, [role='button'], a, label, span, div\")).find(\n      (el) => /select all|select everything/i.test((el.textContent || el.innerText || el.getAttribute(\"aria-label\") || \"\").trim())\n    );\n    if (selectAllBtn) {\n      selectAllBtn.click();\n    }\n  }\n\n  function closeAndSaveLoc() {\n    const isReview = document.querySelector(\".review-header\");\n    if (isReview) {\n      const saveButton = document.querySelector('[data-qa=\"location-save\"]');\n      if (saveButton) saveButton.click();\n    } else if (typeof editor !== \"undefined\" && editor.currentLocation) editor.closeLocation(editor.currentLocation.updatedProps);\n  }\n\n  function setZoom(z) {\n    if (z < 0) z = 0;\n    if (z > 4) z = 4;\n    const svControl = window.streetView;\n    if (svControl) svControl.setZoom(z);\n  }\n\n  async function tagLoc(tag) {\n    if (typeof editor !== \"undefined\" && editor.currentLocation && tag) {\n      await addTag(tag);\n    }\n  }\n\n  async function addTag(tag) {\n    if (typeof editor === \"undefined\") return;\n    const isReview = document.querySelector(\".review-header\");\n    const prevBtn = document.querySelector('[data-qa=\"review-prev\"]');\n    const nextBtn = document.querySelector('[data-qa=\"review-next\"]');\n    const editLoc = editor.currentLocation.updatedProps;\n\n    if (isReview) {\n      await editor.currentLocation.updatedProps.tags.push(tag);\n      setTimeout(() => { if (nextBtn) nextBtn.click(); }, 100);\n      setTimeout(() => { if (prevBtn) prevBtn.click(); }, 200);\n    } else {\n      await editor.closeAndDeleteLocation(editor.currentLocation.location);\n      editLoc.tags.push(tag);\n      await editor.addAndOpenLocation(editLoc);\n    }\n  }\n\n  function deleteTags() {\n    if (typeof editor === \"undefined\") return;\n    let selections = editor.selections;\n    while (selections.length > 0) {\n      const item = selections[0];\n      const tag = JSON.parse(item.key);\n      const tagName = tag.tagName;\n      const locations = item.locations;\n      editor.deleteTag(tagName, locations);\n      selections = editor.selections;\n    }\n  }\n\n  function customLayer(name, tileUrl, maxZoom, minZoom) {\n    return new google.maps.ImageMapType({\n      getTileUrl: function (coord, zoom) {\n        return tileUrl.replace(\"{z}\", zoom).replace(\"{x}\", coord.x).replace(\"{y}\", coord.y);\n      },\n      tileSize: new google.maps.Size(256, 256),\n      name: name,\n      maxZoom: maxZoom,\n      minZoom: minZoom || 1,\n    });\n  }\n\n  function classicMap() {\n    if (typeof map === \"undefined\") return;\n    const tileUrl = `https://mapsresources-pa.googleapis.com/v1/tiles?map_id=61449c20e7fc278b&version=15797339025669136861&pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m2!1e0!2sm!3m7!2sen!3sCN!5e1105!12m1!1e3!12m1!1e2!4e0!5m5!1e0!8m2!1e1!1e1!8i47083502!6m6!1e12!2i2!11e0!39b0!44e0!50e0`;\n    const tileLayer = customLayer(\"google_labels_reset\", tileUrl, 20);\n    map.mapTypes.stack.layers[0] = tileLayer;\n    map.setMapTypeId(\"stack\");\n  }\n\n  function resetGulf() {\n    if (typeof map === \"undefined\") return;\n    let tileUrl = `https://maps.googleapis.com/maps/vt?pb=%211m5%211m4%211i{z}%212i{x}%213i{y}%214i256%212m2%211e0%212sm%213m17%212sen%213sMX%215e18%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.e%3Ag%7Cp.v%3Aoff%2Cs.t%3A1%7Cs.e%3Ag.s%7Cp.v%3Aon%2Cs.e%3Al%7Cp.v%3Aon%215m1%215f1.350000023841858`;\n    if (JSON.parse(localStorage.getItem(\"mapBoldCountryBorders\") || \"false\"))\n      tileUrl = `https://maps.googleapis.com/maps/vt?pb=%211m5%211m4%211i{z}%212i{x}%213i{y}%214i256%212m2%211e0%212sm%213m17%212sen%213smx%215e18%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.t%3A17%7Cs.e%3Ag.s%7Cp.w%3A2%7Cp.c%3A%23000000%2Cs.e%3Ag%7Cp.v%3Aoff%2Cs.t%3A1%7Cs.e%3Ag.s%7Cp.v%3Aon%2Cs.e%3Al%7Cp.v%3Aon%215m1%215f1.350000023841858`;\n    if (JSON.parse(localStorage.getItem(\"mapBoldSubdivisionBorders\") || \"false\"))\n      tileUrl = `https://maps.googleapis.com/maps/vt?pb=%211m5%211m4%211i{z}%212i{x}%213i{y}%214i256%212m2%211e0%212sm%213m17%212sen%213smx%215e18%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.t%3A18%7Cs.e%3Ag.s%7Cp.w%3A3%2Cs.e%3Al%7Cp.v%3Aoff%2Cs.t%3A1%7Cs.e%3Ag.s%7Cp.v%3Aoff%215m1%215f1.350000023841858`;\n    if (JSON.parse(localStorage.getItem(\"mapBoldSubdivisionBorders\") || \"false\") && JSON.parse(localStorage.getItem(\"mapBoldCountryBorders\") || \"false\"))\n      tileUrl = `https://maps.googleapis.com/maps/vt?pb=%211m5%211m4%211i{z}%212i{x}%213i{y}%214i256%212m2%211e0%212sm%213m17%212sen%213smx%215e18%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.t%3A17%7Cs.e%3Ag.s%7Cp.w%3A2%7Cp.c%3A%23000000%2Cs.t%3A18%7Cs.e%3Ag.s%7Cp.w%3A3%2Cs.e%3Ag%7Cp.v%3Aoff%2Cs.t%3A1%7Cs.e%3Ag.s%7Cp.v%3Aon%2Cs.e%3Al%7Cp.v%3Aon%215m1%215f1.350000023841858`;\n    const tileLayer = customLayer(\"google_labels_reset\", tileUrl, 20);\n    map.mapTypes.stack.layers[2] = tileLayer;\n    map.setMapTypeId(\"stack\");\n  }\n\n  function setGD() {\n    if (typeof map === \"undefined\") return;\n    const tileUrl = `https://t2.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=75f0434f240669f4a2df6359275146d2`;\n    const tileLayer = customLayer(\"GaoDe_Terrain\", tileUrl, 20);\n    const tileUrl_ = `https://t2.tianditu.gov.cn/ibo_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ibo&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=75f0434f240669f4a2df6359275146d2`;\n    const tileLayer_ = customLayer(\"GaoDe_Border\", tileUrl_, 10);\n    map.mapTypes.stack.layers[1] = tileLayer_;\n    map.setMapTypeId(\"stack\");\n  }\n\n  function setYandex() {\n    if (typeof map === \"undefined\") return;\n    const svUrl = `https://core-stv-renderer.maps.yandex.net/2.x/tiles?l=stv&x={x}&y={y}&z={z}&scale=1&v=2025.04.04.20.13-1_25.03.31-4-24330`;\n    const baseUrl = `https://core-renderer-tiles.maps.yandex.net/tiles?l=map&v=5.04.07-2~b:250311142430~ib:250404100358-24371&x={x}&y={y}&z={z}&scale=1&lang=en_US`;\n    const svLayer = customLayer(\"Yandex_StreetView\", svUrl, 20, 5);\n    const baseLayer = customLayer(\"Yandex_Maps\", baseUrl, 20, 1);\n    map.mapTypes.stack.layers.splice(2, 0, svLayer);\n    map.mapTypes.stack.layers.splice(2, 0, baseLayer);\n    map.mapTypes.set(\"stack\", map.mapTypes.stack.layers);\n    map.setMapTypeId(\"stack\");\n  }\n\n  function setApple() {\n    if (typeof map === \"undefined\") return;\n    const svUrl = `https://lookmap.eu.pythonanywhere.com/bluelines_raster_2x/{z}/{x}/{y}.png`;\n    const svLayer = customLayer(\"Apple_StreetView\", svUrl, 16);\n    map.mapTypes.stack.layers.splice(2, 0, svLayer);\n    map.setMapTypeId(\"stack\");\n  }\n\n  function getBingTilesUrl(tileX, tileY, zoom) {\n    const quadKey = tileXYToQuadKey(tileX, tileY, zoom);\n    return `https://t.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/${quadKey}?it=Z,HC`;\n  }\n\n  function tileXYToQuadKey(tileX, tileY, zoom) {\n    let quadKey = \"\";\n    for (let i = zoom; i > 0; i--) {\n      let digit = 0;\n      const mask = 1 << (i - 1);\n      if ((tileX & mask) !== 0) digit += 1;\n      if ((tileY & mask) !== 0) digit += 2;\n      quadKey += digit.toString();\n    }\n    return quadKey;\n  }\n\n  function setBing() {\n    if (typeof map === \"undefined\") return;\n    const svLayer = new google.maps.ImageMapType({\n      getTileUrl: function (coord, zoom) {\n        return getBingTilesUrl(coord.x, coord.y, zoom);\n      },\n      tileSize: new google.maps.Size(256, 256),\n      name: \"Bing_StreetSide\",\n      maxZoom: 20,\n    });\n    map.mapTypes.stack.layers.splice(2, 0, svLayer);\n    map.setMapTypeId(\"stack\");\n  }\n\n  async function downloadTile(id, g) {\n    try {\n      const response = await fetch(\n        `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${id}&output=tile&x=${g === \"Gen4\" ? 18 : 16}&y=${g === \"Gen4\" ? 13 : 11}&zoom=5&nbt=1&fover=2`\n      );\n      const imageBlob = await response.blob();\n      const img = new Image();\n      img.onload = function () {\n        const canvas = document.createElement(\"canvas\");\n        const ctx = canvas.getContext(\"2d\");\n        canvas.width = img.width;\n        canvas.height = img.height;\n        ctx.drawImage(img, 0, 0);\n        const dataUrl = canvas.toDataURL(\"image/jpeg\");\n        const link = document.createElement(\"a\");\n        link.href = dataUrl;\n        link.download = id + \".jpg\";\n        link.click();\n      };\n      img.src = URL.createObjectURL(imageBlob);\n    } catch (error) {\n      console.error(\"Error:\", error);\n    }\n  }\n\n  function getMap() {\n    const element = document.getElementsByClassName(\"map-embed\")[0];\n    if (!element) return;\n    try {\n      const keys = Object.keys(element);\n      const key = keys.find((k) => k.startsWith(\"__reactFiber$\"));\n      const props = element[key];\n      if (props && props.pendingProps && props.pendingProps.children) {\n        const mapRef = props.pendingProps.children[1]?.props?.children[1]?.props?.map;\n        if (mapRef) window.map = mapRef;\n      }\n    } catch (e) {\n      console.error(\"Failed to get map:\", e);\n    }\n  }\n\n  function delay(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  async function fetchPanorama(service, panoId) {\n    await delay(100);\n    return await service.getPanorama({ pano: panoId });\n  }\n\n  async function findLinkPanos() {\n    if (typeof editor === \"undefined\" || !editor.currentLocation) return;\n    const startLoc = editor.currentLocation.updatedProps;\n    let prevHeading = startLoc.heading;\n    const service = new google.maps.StreetViewService();\n    let metadata = await fetchPanorama(service, startLoc.panoId);\n    while (metadata.data.links.length === 2) {\n      const nextLoc = metadata.data.links.find((loc) => Math.abs(loc.heading - prevHeading) <= 90);\n      if (nextLoc) {\n        metadata = await fetchPanorama(service, nextLoc.pano);\n        editor.addLocation({\n          location: { lat: metadata.data.location.latLng.lat(), lng: metadata.data.location.latLng.lng() },\n          panoId: metadata.data.location.pano,\n          heading: nextLoc.heading,\n          pitch: 0,\n          zoom: 0,\n          tags: [],\n          flags: 1,\n        });\n        prevHeading = nextLoc.heading;\n      } else break;\n    }\n  }\n\n  function toggleElementHidden() {\n    if (!isHidden) {\n      style = addStyle(`\n        .embed-controls {display: none !important}\n        .SLHIdE-sv-links-control {display: none !important}\n        [class$=\"gmnoprint\"], [class$=\"gm-style-cc\"] {display: none !important}\n      `);\n      isHidden = true;\n    } else {\n      if (style && style.remove) style.remove();\n      isHidden = false;\n    }\n  }\n\n  function matchKey(e, key) {\n    if (!key) return false;\n    return e.key === key || (key.length === 1 && e.key.toLowerCase() === key.toLowerCase());\n  }\n\n  function onKeyDown(e) {\n    if (e.target.tagName === \"INPUT\" || e.target.isContentEditable) return;\n    const activeSelections =\n      typeof editor !== \"undefined\" && editor.selections && editor.selections.length > 0\n        ? editor.selections.flatMap((s) => s.locations)\n        : typeof locations !== \"undefined\" ? locations : [];\n    const getActiveSelections = () => activeSelections;\n    const noMod = !e.shiftKey && !e.ctrlKey && !e.metaKey;\n\n    if (geonectionsModeOn) {\n      if (e.code === KEYBOARD_SHORTCUTS.prevLocation && noMod) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        rewindLoc(activeSelections);\n        return;\n      }\n      if (e.code === KEYBOARD_SHORTCUTS.nextLocation && noMod) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        switchLoc(activeSelections);\n        return;\n      }\n      if (e.code === KEYBOARD_SHORTCUTS.autoplayToggle && noMod) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        if (autoplayOn) stopAutoplay();\n        else startAutoplay(getActiveSelections);\n        return;\n      }\n      if (autoplayOn && (e.key >= \"0\" && e.key <= \"9\")) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        let sec = 0;\n        if (e.shiftKey) {\n          if (e.key === \"0\") sec = 60;\n          else if (e.key === \"1\") sec = 15;\n          else sec = parseInt(e.key, 10) * 10;\n        } else {\n          if (e.key === \"0\") sec = 10;\n          else sec = parseInt(e.key, 10);\n        }\n        if (sec > 0) {\n          autoplayDelayMs = sec * 1000;\n          stopAutoplay();\n          startAutoplay(getActiveSelections);\n          updateGeonectionsLabel();\n        }\n        return;\n      }\n      if (autoplayOn && noMod) {\n        if (e.key === \"-\" || e.key === \"_\") {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          autoplayDelayMs = 15000;\n          stopAutoplay();\n          startAutoplay(getActiveSelections);\n          updateGeonectionsLabel();\n          return;\n        }\n        if (e.key === \"=\" || e.key === \"+\") {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          autoplayDelayMs = 30000;\n          stopAutoplay();\n          startAutoplay(getActiveSelections);\n          updateGeonectionsLabel();\n          return;\n        }\n        if (e.key === \"]\" || e.key === \"}\") {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          autoplayDelayMs = 60000;\n          stopAutoplay();\n          startAutoplay(getActiveSelections);\n          updateGeonectionsLabel();\n          return;\n        }\n      }\n      if (noMod) {\n        if (e.key >= \"1\" && e.key <= \"4\") {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          const tag = DIFFICULTY_TAGS[parseInt(e.key, 10)];\n          if (tag) tagLoc(tag);\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.tagCountry)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          getCountryForCurrentLocation().then((name) => { if (name) tagLoc(name); });\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.hideUi)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          toggleElementHidden();\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.deleteLoc)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          deleteLoc(activeSelections[currentIndex - 1]);\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.openInTabs)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          if (activeSelections.length) openSelectedInTabs(activeSelections.length);\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.tagUsable)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          tagLoc(\"Usable\");\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.selectAll)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          selectAllLocations();\n          return;\n        }\n        if (matchKey(e, KEYBOARD_SHORTCUTS.saveCloseLoc)) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          closeAndSaveLoc();\n          return;\n        }\n      }\n      if ((e.metaKey || e.ctrlKey) && matchKey(e, KEYBOARD_SHORTCUTS.copyMapsLink)) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        copyGoogleMapsLinkToClipboard();\n        return;\n      }\n    }\n  }\n\n  document.addEventListener(\"keydown\", onKeyDown);\n\n  document.querySelectorAll(\".geonections-mode-toggle\").forEach((el) => el.remove());\n  const geonectionsToggle = document.createElement(\"button\");\n  geonectionsToggle.setAttribute(\"aria-label\", \"Geonections Mode\");\n  geonectionsToggle.className = \"geonections-mode-toggle\";\n  function updateGeonectionsLabel() {\n    let text = geonectionsModeOn ? \"Geonections Mode: ON \\u{1F60E}\" : \"Geonections Mode: Off \\u{1F61E}\";\n    if (geonectionsModeOn && autoplayOn) {\n      const sec = autoplayDelayMs / 1000;\n      text += sec >= 60 ? \" · \" + (sec / 60) + \"m\" : \" · \" + sec + \"s\";\n    }\n    geonectionsToggle.textContent = text;\n    geonectionsToggle.style.background = geonectionsModeOn\n      ? \"linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%)\"\n      : \"linear-gradient(135deg, #64748b 0%, #475569 100%)\";\n    geonectionsToggle.style.boxShadow = geonectionsModeOn ? \"0 4px 14px rgba(14,165,233,0.45)\" : \"0 2px 8px rgba(0,0,0,0.2)\";\n  }\n  Object.assign(geonectionsToggle.style, {\n    position: \"fixed\",\n    top: \"0\",\n    left: \"50%\",\n    transform: \"translateX(-50%)\",\n    zIndex: \"10000\",\n    padding: \"10px 20px\",\n    borderRadius: \"999px\",\n    border: \"none\",\n    color: \"#fff\",\n    fontSize: \"15px\",\n    fontWeight: \"600\",\n    cursor: \"pointer\",\n    fontFamily: \"system-ui, -apple-system, sans-serif\",\n    letterSpacing: \"0.02em\",\n    transition: \"background 0.2s, box-shadow 0.2s\",\n  });\n  updateGeonectionsLabel();\n  geonectionsToggle.addEventListener(\"click\", function () {\n    geonectionsModeOn = !geonectionsModeOn;\n    if (!geonectionsModeOn) stopAutoplay();\n    updateGeonectionsLabel();\n  });\n  document.body.appendChild(geonectionsToggle);\n\n  const extraBtn = document.createElement(\"button\");\n  extraBtn.textContent = \"Extra\";\n  extraBtn.className = \"geonections-extra\";\n  Object.assign(extraBtn.style, {\n    position: \"fixed\",\n    right: \"155px\",\n    bottom: \"60px\",\n    zIndex: \"9998\",\n    padding: \"8px 16px\",\n    borderRadius: \"12px\",\n    border: \"1px solid rgba(255,255,255,0.3)\",\n    background: \"linear-gradient(180deg, #334155 0%, #1e293b 100%)\",\n    color: \"#e2e8f0\",\n    fontSize: \"13px\",\n    fontWeight: \"600\",\n    cursor: \"pointer\",\n    fontFamily: \"system-ui, sans-serif\",\n    boxShadow: \"0 4px 12px rgba(0,0,0,0.3)\",\n  });\n  let extraOpen = false;\n  let extraMenu = null;\n  let shortcutsOverlay = null;\n  function hideExtraMenu() {\n    if (extraMenu && extraMenu.parentNode) extraMenu.remove();\n    extraMenu = null;\n    extraOpen = false;\n    document.removeEventListener(\"click\", hideExtraMenu);\n  }\n  function hideShortcutsOverlay() {\n    if (!shortcutsOverlay) return;\n    if (shortcutsOverlay._esc) document.removeEventListener(\"keydown\", shortcutsOverlay._esc, true);\n    if (shortcutsOverlay.parentNode) shortcutsOverlay.remove();\n    shortcutsOverlay = null;\n  }\n  function showShortcutsView() {\n    hideExtraMenu();\n    if (shortcutsOverlay && shortcutsOverlay.parentNode) return;\n    const isMac = typeof navigator !== \"undefined\" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);\n    const mod = isMac ? \"⌘\" : \"Ctrl\";\n    shortcutsOverlay = document.createElement(\"div\");\n    shortcutsOverlay.style.cssText = \"position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10002;display:flex;align-items:center;justify-content:center;padding:20px;\";\n    const panel = document.createElement(\"div\");\n    panel.style.cssText = \"background:#1e293b;border-radius:12px;padding:20px;max-width:420px;max-height:85vh;overflow:auto;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid #334155;\";\n    panel.innerHTML = \"<div style='font-size:14px;font-weight:600;color:#f1f5f9;margin-bottom:12px;'>Keyboard shortcuts (Geonections Mode ON)</div>\" +\n      \"<table style='width:100%;font-size:12px;color:#e2e8f0;border-collapse:collapse;'>\" +\n      \"<tr><td style='padding:4px 8px 2px 0;color:#94a3b8;'>←</td><td style='padding:4px 0 2px 0;'>Previous location</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>→</td><td style='padding:2px 0;'>Next location</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>Space</td><td style='padding:2px 0;'>Start / stop autoplay</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>1–4</td><td style='padding:2px 0;'>Set difficulty (Easy / Medium / Hard / Expert)</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>T</td><td style='padding:2px 0;'>Tag location with country</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>H</td><td style='padding:2px 0;'>Hide on-screen UI</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>C</td><td style='padding:2px 0;'>Delete current location</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>O</td><td style='padding:2px 0;'>Open selected locations in new tabs</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>U</td><td style='padding:2px 0;'>Tag as Usable</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>A</td><td style='padding:2px 0;'>Select all locations</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>S</td><td style='padding:2px 0;'>Save / close current location</td></tr>\" +\n      \"<tr><td style='padding:2px 8px 2px 0;color:#94a3b8;'>\" + mod + \"+C</td><td style='padding:2px 0;'>Copy Google Maps link</td></tr>\" +\n      \"<tr><td colspan='2' style='padding:12px 0 4px 0;color:#94a3b8;font-size:11px;'>When autoplay is on: 1–9 (1–9 sec), 0 (10 sec), - (15), = (30), ] (60 sec); Shift+2..9 (20–90 sec), Shift+0 (60 sec)</td></tr>\" +\n      \"</table>\" +\n      \"<div style='margin-top:16px;text-align:right;'><button type='button' style='padding:6px 14px;border-radius:8px;border:none;background:#475569;color:#e2e8f0;font-weight:600;cursor:pointer;font-size:12px;'>Close</button></div>\";\n    panel.querySelector(\"button\").addEventListener(\"click\", hideShortcutsOverlay);\n    shortcutsOverlay.appendChild(panel);\n    shortcutsOverlay.addEventListener(\"click\", function (e) { if (e.target === shortcutsOverlay) hideShortcutsOverlay(); });\n    shortcutsOverlay._esc = function (e) { if (e.key === \"Escape\") hideShortcutsOverlay(); };\n    document.addEventListener(\"keydown\", shortcutsOverlay._esc, true);\n    document.body.appendChild(shortcutsOverlay);\n  }\n  function runTagAllMissingCountry() {\n    hideExtraMenu();\n    runFinalCheck(\"tag-country\");\n  }\n  extraBtn.addEventListener(\"click\", (e) => {\n    e.stopPropagation();\n    if (extraOpen) {\n      hideExtraMenu();\n      return;\n    }\n    extraOpen = true;\n    extraMenu = document.createElement(\"div\");\n    Object.assign(extraMenu.style, {\n      position: \"fixed\",\n      right: \"155px\",\n      bottom: \"100px\",\n      zIndex: \"10001\",\n      background: \"#1e293b\",\n      borderRadius: \"12px\",\n      padding: \"12px 10px\",\n      minWidth: \"220px\",\n      boxShadow: \"0 8px 24px rgba(0,0,0,0.4)\",\n      border: \"1px solid #334155\",\n    });\n    const items = [\n      { id: \"view-shortcuts\", label: \"View shortcuts\" },\n      { id: \"tag-all-missing-country\", label: \"Tag all missing country\" },\n    ];\n    items.forEach((opt) => {\n      const row = document.createElement(\"div\");\n      row.style.cssText = \"padding:10px 12px;border-radius:8px;cursor:pointer;font-size:13px;color:#e2e8f0;\";\n      row.textContent = opt.label;\n      row.addEventListener(\"mouseenter\", () => { row.style.background = \"#334155\"; });\n      row.addEventListener(\"mouseleave\", () => { row.style.background = \"transparent\"; });\n      row.addEventListener(\"click\", (ev) => {\n        ev.preventDefault();\n        if (opt.id === \"view-shortcuts\") showShortcutsView();\n        else if (opt.id === \"tag-all-missing-country\") runTagAllMissingCountry();\n      });\n      extraMenu.appendChild(row);\n    });\n    document.body.appendChild(extraMenu);\n    setTimeout(() => document.addEventListener(\"click\", hideExtraMenu), 0);\n  });\n  document.body.appendChild(extraBtn);\n\n  const finalCheckBtn = document.createElement(\"button\");\n  finalCheckBtn.textContent = \"Final Check\";\n  finalCheckBtn.className = \"geonections-final-check\";\n  Object.assign(finalCheckBtn.style, {\n    position: \"fixed\",\n    right: \"20px\",\n    bottom: \"60px\",\n    zIndex: \"9998\",\n    padding: \"8px 16px\",\n    borderRadius: \"12px\",\n    border: \"1px solid rgba(255,255,255,0.3)\",\n    background: \"linear-gradient(180deg, #334155 0%, #1e293b 100%)\",\n    color: \"#e2e8f0\",\n    fontSize: \"13px\",\n    fontWeight: \"600\",\n    cursor: \"pointer\",\n    fontFamily: \"system-ui, sans-serif\",\n    boxShadow: \"0 4px 12px rgba(0,0,0,0.3)\",\n  });\n  let finalCheckOpen = false;\n  let finalCheckMenu = null;\n  const finalCheckChecked = {};\n  function hideFinalCheckMenu() {\n    if (finalCheckMenu && finalCheckMenu.parentNode) finalCheckMenu.remove();\n    finalCheckMenu = null;\n    finalCheckOpen = false;\n    document.removeEventListener(\"click\", hideFinalCheckMenu);\n  }\n  const GEONECTIONS_DISCORD_INVITE = \"https://discord.gg/hGjtMhTTFc\";\n  function runFinalCheck(action) {\n    const locs = typeof locations !== \"undefined\" ? locations : [];\n    if (action !== \"open-discord\" && !locs.length) {\n      alert(\"No locations on this map.\");\n      return;\n    }\n    if (action === \"find-no-pano\") {\n      const noPano = locs.filter((loc) => !loc.panoId || String(loc.panoId).trim() === \"\");\n      if (noPano.length && typeof editor !== \"undefined\") editor.addTag(noPano, \"Missing pano ID\");\n      alert(noPano.length ? \"Tagged \" + noPano.length + \" locations with \\\"Missing pano ID\\\".\" : \"All locations have a pano ID.\");\n    } else if (action === \"find-no-difficulty\") {\n      const noDiff = locs.filter((loc) => {\n        const t = (loc.tags || []);\n        return !t.some((tag) => [\"Easy\", \"Medium\", \"Hard\", \"Expert\"].includes(tag));\n      });\n      if (noDiff.length && typeof editor !== \"undefined\") editor.addTag(noDiff, \"Missing difficulty\");\n      alert(noDiff.length ? \"Tagged \" + noDiff.length + \" locations with \\\"Missing difficulty\\\".\" : \"All locations have a difficulty tag.\");\n    } else if (action === \"find-no-country\") {\n      const noCountry = locs.filter((loc) => {\n        const t = (loc.tags || []);\n        return !t.some((tag) => COUNTRY_NAMES_SET.has(tag));\n      });\n      if (noCountry.length && typeof editor !== \"undefined\") editor.addTag(noCountry, \"Missing country\");\n      alert(noCountry.length ? \"Tagged \" + noCountry.length + \" locations with \\\"Missing country\\\".\" : \"All locations have a country tag.\");\n    } else if (action === \"check-tags\") {\n      const DIFFICULTIES = [\"Easy\", \"Medium\", \"Hard\", \"Expert\"];\n      const byCountry = {};\n      for (const loc of locs) {\n        const t = loc.tags || [];\n        const country = t.find((tag) => COUNTRY_NAMES_SET.has(tag));\n        const difficulty = t.find((tag) => DIFFICULTIES.includes(tag));\n        if (country && difficulty) {\n          if (!byCountry[country]) byCountry[country] = new Set();\n          byCountry[country].add(difficulty);\n        }\n      }\n      const inconsistent = Object.entries(byCountry).filter(([, set]) => set.size > 1);\n      if (inconsistent.length) {\n        const msg = inconsistent\n          .map(([country, set]) => country + \": \" + [...set].sort().join(\", \"))\n          .join(\"\\n\");\n        alert(\"Countries with mixed difficulty tags:\\n\\n\" + msg);\n      } else {\n        alert(\"All countries have consistent difficulty tags.\");\n      }\n    } else if (action === \"tag-country\") {\n      const needCountry = locs.filter((loc) => !(loc.tags || []).some((t) => COUNTRY_NAMES_SET.has(t)));\n      if (!needCountry.length) {\n        alert(\"All locations already have a country tag.\");\n        return;\n      }\n      alert(\"Tagging \" + needCountry.length + \" locations with country… This may take a while.\");\n      (async () => {\n        for (const loc of needCountry) {\n          const res = await fetchGooglePanorama(loc.panoId ? \"GetMetadata\" : \"SingleImageSearch\", loc.panoId || loc.location);\n          if (!res || !res[1]) continue;\n          try {\n            const inner = res[1].length !== 3 ? res[1][0] : res[1];\n            const code = inner && inner[5] && inner[5][0] && inner[5][0][1] ? inner[5][0][1][4] : null;\n            const name = code ? countryNameForCode(code) : null;\n            if (name && typeof editor !== \"undefined\") editor.addTag([loc], name);\n          } catch (e) {}\n        }\n        alert(\"Country tagging finished. Save the map and refresh.\");\n      })();\n    } else if (action === \"open-16-tabs\") {\n      openLocationsInGoogleMapsTabs(locs, 16, { zoom: 0 });\n    } else if (action === \"open-discord\") {\n      window.open(GEONECTIONS_DISCORD_INVITE, \"_blank\", \"noopener,noreferrer\");\n    }\n  }\n  finalCheckBtn.addEventListener(\"click\", (e) => {\n    e.stopPropagation();\n    if (finalCheckOpen) {\n      hideFinalCheckMenu();\n      return;\n    }\n    finalCheckOpen = true;\n    finalCheckMenu = document.createElement(\"div\");\n    Object.assign(finalCheckMenu.style, {\n      position: \"fixed\",\n      right: \"20px\",\n      bottom: \"100px\",\n      zIndex: \"10001\",\n      background: \"#1e293b\",\n      borderRadius: \"12px\",\n      padding: \"12px 10px\",\n      minWidth: \"280px\",\n      boxShadow: \"0 8px 24px rgba(0,0,0,0.4)\",\n      border: \"1px solid #334155\",\n    });\n    const checklistItems = [\n      { id: \"find-no-pano\", label: \"Find missing pano ID\" },\n      { id: \"find-no-difficulty\", label: \"Find missing difficulty tag\" },\n      { id: \"find-no-country\", label: \"Find missing country tag\" },\n      { id: \"check-tags\", label: \"Check tags\" },\n      { id: \"open-16-tabs\", label: \"Open 16 tabs (road names)\" },\n      { id: \"open-discord\", label: \"Submit to Geonections Discord 😎\" },\n    ];\n    checklistItems.forEach((opt, index) => {\n      const row = document.createElement(\"label\");\n      row.style.display = \"flex\";\n      row.style.alignItems = \"center\";\n      row.style.gap = \"8px\";\n      row.style.padding = \"8px 10px\";\n      row.style.marginBottom = \"2px\";\n      row.style.borderRadius = \"8px\";\n      row.style.cursor = \"pointer\";\n      row.style.fontSize = \"13px\";\n      row.style.color = \"#e2e8f0\";\n      row.addEventListener(\"mouseenter\", () => { row.style.background = \"#334155\"; });\n      row.addEventListener(\"mouseleave\", () => { row.style.background = \"transparent\"; });\n      const checkbox = document.createElement(\"input\");\n      checkbox.type = \"checkbox\";\n      checkbox.checked = !!finalCheckChecked[opt.id];\n      checkbox.style.flexShrink = \"0\";\n      checkbox.style.width = \"16px\";\n      checkbox.style.height = \"16px\";\n      checkbox.style.accentColor = \"#3b82f6\";\n      checkbox.addEventListener(\"click\", (ev) => {\n        ev.stopPropagation();\n        finalCheckChecked[opt.id] = checkbox.checked;\n      });\n      const num = document.createElement(\"span\");\n      num.textContent = index + 1 + \".\";\n      num.style.flexShrink = \"0\";\n      num.style.width = \"18px\";\n      num.style.color = \"#94a3b8\";\n      num.style.fontSize = \"12px\";\n      const text = document.createElement(\"span\");\n      text.style.flex = \"1\";\n      if (opt.id === \"open-discord\") {\n        text.appendChild(document.createTextNode(\"Submit to Geonections \"));\n        const discordLink = document.createElement(\"a\");\n        discordLink.href = GEONECTIONS_DISCORD_INVITE;\n        discordLink.target = \"_blank\";\n        discordLink.rel = \"noopener noreferrer\";\n        discordLink.textContent = \"Discord\";\n        discordLink.style.color = \"#60a5fa\";\n        discordLink.style.textDecoration = \"underline\";\n        discordLink.addEventListener(\"click\", (ev) => ev.stopPropagation());\n        text.appendChild(discordLink);\n        text.appendChild(document.createTextNode(\" 😎\"));\n      } else {\n        text.textContent = opt.label;\n      }\n      row.appendChild(checkbox);\n      row.appendChild(num);\n      row.appendChild(text);\n      if (opt.id !== \"open-discord\") {\n        row.addEventListener(\"click\", (ev) => {\n          if (ev.target === checkbox) return;\n          ev.preventDefault();\n          runFinalCheck(opt.id);\n          finalCheckChecked[opt.id] = true;\n          checkbox.checked = true;\n        });\n      }\n      finalCheckMenu.appendChild(row);\n    });\n    document.body.appendChild(finalCheckMenu);\n    setTimeout(() => document.addEventListener(\"click\", hideFinalCheckMenu), 0);\n  });\n  document.body.appendChild(finalCheckBtn);\n\n  document.addEventListener(\"mousedown\", function (e) {\n    if (e.button === 0 && e.shiftKey) {\n      isDrawing = true;\n      startX = e.clientX;\n      startY = e.clientY;\n      document.body.style.userSelect = \"none\";\n      selectionBox = document.createElement(\"div\");\n      Object.assign(selectionBox.style, {\n        position: \"absolute\", border: \"2px solid rgba(0, 128, 255, 0.7)\",\n        backgroundColor: \"rgba(0, 128, 255, 0.2)\",\n      });\n      document.body.appendChild(selectionBox);\n    }\n  });\n\n  document.addEventListener(\"mousemove\", function (e) {\n    if (isDrawing && selectionBox) {\n      endX = e.clientX;\n      endY = e.clientY;\n      const width = Math.abs(endX - startX);\n      const height = Math.abs(endY - startY);\n      selectionBox.style.left = `${Math.min(startX, endX)}px`;\n      selectionBox.style.top = `${Math.min(startY, endY)}px`;\n      selectionBox.style.width = `${width}px`;\n      selectionBox.style.height = `${height}px`;\n      selectionBox.style.zIndex = \"999999\";\n    }\n  });\n\n  document.addEventListener(\"mouseup\", function (e) {\n    if (isDrawing && selectionBox && selectionBox.parentNode) {\n      isDrawing = false;\n      const rect = selectionBox.getBoundingClientRect();\n      document.body.removeChild(selectionBox);\n      document.querySelectorAll(\"ul.tag-list\").forEach((element) => {\n        element.querySelectorAll(\"li.tag.has-button\").forEach((child) => {\n          const childRect = child.getBoundingClientRect();\n          if (childRect.top >= rect.top && childRect.left >= rect.left && childRect.bottom <= rect.bottom && childRect.right <= rect.right) {\n            child.click();\n          }\n        });\n      });\n      document.body.style.userSelect = \"text\";\n    }\n  });\n\n  console.log(\"[Geonections Extension] Loaded on\", window.location.href);\n})();\n";
  run(contentCode);
})();