Geonections Extension

Geonections Mode and Final Check for map-making.app

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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