Geonections Mode and Final Check for map-making.app
// ==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);
})();