location panel
// ==UserScript==
// @name GeoIntel
// @namespace http://tampermonkey.net/
// @version 2.7.4
// @description location panel
// @author maiousXO13
// @license MIT
// @match https://www.geoguessr.com/*
// @grant GM_xmlhttpRequest
// @connect us1.locationiq.com
// @connect locationiq.com
// @connect api.bigdatacloud.net
// @connect api-bdc.io
// @connect static-maps.yandex.ru
// @connect flagcdn.com
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEYS = {
autoGuessDelayMs: 'gi_auto_guess_delay_ms',
autoGuessDeviationMs: 'gi_auto_guess_deviation_ms',
guessOffsetBaseM: 'gi_guess_offset_base_m',
guessOffsetDeviationM: 'gi_guess_offset_deviation_m',
hotkeyScan: 'gi_hotkey_scan',
hotkeyPlay: 'gi_hotkey_play',
hotkeyCopy: 'gi_hotkey_copy',
locationIqKeys: 'gi_locationiq_keys'
};
const MAPS_RPC_PATTERN = /google\.internal\.maps\.mapsjs\.v1\.MapsJsInternalService\/[A-Za-z]+\b/;
const XHR_PATCH_FLAG = '__gi_xhr_intercept_patched__';
const FETCH_PATCH_FLAG = '__gi_fetch_intercept_patched__';
const DOM_CACHE_MS = 350;
const COORD_DUPLICATE_WINDOW_MS = 2200;
const STALE_ROUND_SIGNATURE_TTL_MS = 15000;
const DEBUG = false;
const CONFIG = {
toggleKey: 'Insert',
detectKey: 'q',
playKey: 't',
copyKey: 'y',
mapZoomMin: 2,
mapZoomMax: 18,
defaultMapZoom: 11,
autoPlayGuessBaseDelayMs: 20000,
autoPlayGuessDeviationMs: 7000,
autoPlayWorkerTickMs: 700,
autoPlayPinAttempts: 18,
autoPlayPinPauseMs: 220,
autoPlayGuessAttempts: 16,
autoPlayGuessPauseMs: 90,
guessTargetAvoidWaterAttempts: 8,
guessTargetProbePrecision: 2,
guessTargetProbeCacheLimit: 320,
guessOffsetBaseM: 500000,
guessOffsetDeviationM: 200000,
maxMarkDelayMs: 86400000,
maxMarkDeviationMs: 86400000,
maxGuessOffsetM: 10000000,
maxGuessOffsetDeviationM: 10000000,
autoDetectCooldownMs: 1200,
mapLayers: [
{ label: 'Map', param: 'map', marker: 'pm2gnm' },
{ label: 'Sat', param: 'sat', marker: 'pm2orm' },
{ label: 'Hybrid', param: 'sat,skl', marker: 'pm2orm' }
],
locationIqKeys: loadApiKeysSetting(STORAGE_KEYS.locationIqKeys)
};
const state = {
panelVisible: true,
mapZoom: CONFIG.defaultMapZoom,
mapLayerIndex: 0,
autoDetect: false,
autoPlay: false,
autoPlaySubmitGuess: false,
autoGuessDelayMs: loadNumberSetting(STORAGE_KEYS.autoGuessDelayMs, CONFIG.autoPlayGuessBaseDelayMs),
autoGuessDeviationMs: loadNumberSetting(STORAGE_KEYS.autoGuessDeviationMs, CONFIG.autoPlayGuessDeviationMs),
guessOffsetBaseM: loadNumberSetting(STORAGE_KEYS.guessOffsetBaseM, CONFIG.guessOffsetBaseM),
guessOffsetDeviationM: loadNumberSetting(STORAGE_KEYS.guessOffsetDeviationM, CONFIG.guessOffsetDeviationM),
hotkeys: {
scan: loadHotkeySetting(STORAGE_KEYS.hotkeyScan, CONFIG.detectKey),
play: loadHotkeySetting(STORAGE_KEYS.hotkeyPlay, CONFIG.playKey),
copy: loadHotkeySetting(STORAGE_KEYS.hotkeyCopy, CONFIG.copyKey)
},
detectInFlight: false,
coordinates: null,
locationData: null,
apiKeyStartIndex: 0,
lastDetectAt: 0,
autoDetectTimer: null,
autoPlayWorkerTimer: null,
autoPlayInFlight: false,
autoPlayHandledSignature: '',
guessMapCache: [],
guessMapCacheAt: 0,
resizeTimer: null,
lastCoordSignature: '',
lastCoordAt: 0,
pendingRoundCapture: false,
pendingCoordSignature: '',
pendingCoordAt: 0,
guessLandProbeCache: new Map(),
ui: null
};
init();
function init() {
interceptCoordinates();
registerHotkeys();
registerGuessSubmissionWatcher();
window.addEventListener('resize', onResize, { passive: true });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createUI, { once: true });
} else {
createUI();
}
}
function createUI() {
if (state.ui || !document.body) {
return;
}
injectStyles();
const panel = document.createElement('section');
panel.id = 'gi-shell';
panel.innerHTML = `
<header id="gi-hero">
<div id="gi-title">GeoIntel</div>
</header>
<div id="gi-scan-row">
<button type="button" id="gi-scan">SCAN</button>
</div>
<div id="gi-toggle-row">
<button type="button" id="gi-auto" data-active="false">AutoScan Off</button>
<button type="button" id="gi-autoplay" data-active="false">AutoPlay Off</button>
<button type="button" id="gi-autoguess" data-active="false">AutoGuess Off</button>
</div>
<div id="gi-action-row">
<button type="button" id="gi-play">Play</button>
<button type="button" id="gi-layer">Layer</button>
<button type="button" id="gi-copy">Copy</button>
</div>
<section id="gi-guess-controls">
<label class="gi-guess-field">
<span>Mark Delay (s)</span>
<input type="number" min="0" max="86400" step="0.05" id="gi-guess-delay" />
</label>
<label class="gi-guess-field">
<span>Deviation (s)</span>
<input type="number" min="0" max="86400" step="0.05" id="gi-guess-deviation" />
</label>
<label class="gi-guess-field">
<span>Guess Offset (km)</span>
<input type="number" min="0" max="10000" step="0.05" id="gi-offset-base" />
</label>
<label class="gi-guess-field">
<span>Offset Deviation (km)</span>
<input type="number" min="0" max="10000" step="0.05" id="gi-offset-deviation" />
</label>
<div id="gi-guess-range"></div>
<div id="gi-offset-range"></div>
</section>
<section id="gi-intel">
<div class="gi-row">
<span class="gi-label">Country</span>
<span class="gi-value-wrap">
<img id="gi-flag" alt="flag" />
<span class="gi-value" id="gi-country">N/A</span>
</span>
</div>
<div class="gi-row">
<span class="gi-label">State</span>
<span class="gi-value" id="gi-state">N/A</span>
</div>
<div class="gi-row">
<span class="gi-label">City</span>
<span class="gi-value" id="gi-city">N/A</span>
</div>
</section>
<section id="gi-map-stage">
<img id="gi-map" alt="map" draggable="false" />
<div id="gi-empty">Move once, then press Scan or Q.</div>
<div id="gi-map-ui">
<div id="gi-coords">Coordinates: N/A</div>
<div id="gi-zoom-wrap">
<button type="button" id="gi-zin">+</button>
<div id="gi-zoom">Zoom 11</div>
<button type="button" id="gi-zout">-</button>
</div>
</div>
</section>
<section id="gi-hotkeys">
<label class="gi-hotkey-field">
<span>Scan</span>
<input type="text" id="gi-key-scan" maxlength="16" />
</label>
<label class="gi-hotkey-field">
<span>Play</span>
<input type="text" id="gi-key-play" maxlength="16" />
</label>
<label class="gi-hotkey-field">
<span>Copy</span>
<input type="text" id="gi-key-copy" maxlength="16" />
</label>
</section>
`;
document.body.appendChild(panel);
state.ui = {
panel,
header: panel.querySelector('#gi-hero'),
scan: panel.querySelector('#gi-scan'),
auto: panel.querySelector('#gi-auto'),
autoPlay: panel.querySelector('#gi-autoplay'),
autoGuess: panel.querySelector('#gi-autoguess'),
play: panel.querySelector('#gi-play'),
layer: panel.querySelector('#gi-layer'),
copy: panel.querySelector('#gi-copy'),
guessDelay: panel.querySelector('#gi-guess-delay'),
guessDeviation: panel.querySelector('#gi-guess-deviation'),
guessRange: panel.querySelector('#gi-guess-range'),
offsetBase: panel.querySelector('#gi-offset-base'),
offsetDeviation: panel.querySelector('#gi-offset-deviation'),
offsetRange: panel.querySelector('#gi-offset-range'),
keyScan: panel.querySelector('#gi-key-scan'),
keyPlay: panel.querySelector('#gi-key-play'),
keyCopy: panel.querySelector('#gi-key-copy'),
flag: panel.querySelector('#gi-flag'),
country: panel.querySelector('#gi-country'),
region: panel.querySelector('#gi-state'),
city: panel.querySelector('#gi-city'),
mapStage: panel.querySelector('#gi-map-stage'),
map: panel.querySelector('#gi-map'),
empty: panel.querySelector('#gi-empty'),
coords: panel.querySelector('#gi-coords'),
zin: panel.querySelector('#gi-zin'),
zout: panel.querySelector('#gi-zout'),
zoom: panel.querySelector('#gi-zoom')
};
bindUI();
renderAll();
flashMessage('Deck online', 'success');
}
function injectStyles() {
if (document.getElementById('gi-style')) {
return;
}
const style = document.createElement('style');
style.id = 'gi-style';
style.textContent = `
#gi-shell {
position: fixed;
right: 12px;
top: 12px;
width: min(360px, calc(100vw - 16px));
z-index: 2147483647;
border-radius: 8px;
border: 1px solid #444;
background: #1b1b1b;
color: #f3f3f3;
font-family: Segoe UI, Tahoma, sans-serif;
padding: 8px;
overflow: hidden;
user-select: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}
#gi-hero {
display: flex;
align-items: center;
cursor: move;
margin-bottom: 6px;
padding: 0;
}
#gi-title {
font-size: 14px;
font-weight: 700;
color: #fff;
}
#gi-scan-row {
margin-bottom: 4px;
}
#gi-toggle-row,
#gi-action-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
margin-bottom: 6px;
}
#gi-scan-row button,
#gi-toggle-row button,
#gi-action-row button {
min-height: 28px;
border: 1px solid #555;
border-radius: 4px;
background: #2b2b2b;
color: #fff;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, transform 80ms ease;
}
#gi-scan-row button {
width: 100%;
min-height: 34px;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.4px;
}
#gi-scan-row button:hover,
#gi-toggle-row button:hover,
#gi-action-row button:hover {
background: #343434;
}
#gi-scan-row button[disabled],
#gi-toggle-row button[disabled],
#gi-action-row button[disabled] {
opacity: 0.6;
cursor: default;
}
#gi-auto[data-active='true'] {
border-color: #9acb6c;
}
#gi-autoplay[data-active='true'],
#gi-autoguess[data-active='true'] {
border-color: #7ad1ff;
}
#gi-scan-row button[data-pressed='true'],
#gi-toggle-row button[data-pressed='true'],
#gi-action-row button[data-pressed='true'] {
transform: translateY(1px);
filter: brightness(0.82);
}
#gi-guess-controls {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px;
margin-bottom: 6px;
}
.gi-guess-field {
display: grid;
gap: 2px;
font-size: 10px;
color: #c9c9c9;
}
.gi-guess-field input {
width: 100%;
border: 1px solid #4a4a4a;
border-radius: 4px;
background: #252525;
color: #f2f2f2;
font-size: 11px;
padding: 4px 5px;
box-sizing: border-box;
}
#gi-guess-range,
#gi-offset-range {
grid-column: 1 / -1;
font-size: 11px;
color: #b9d9ff;
border: 1px solid #3a4b5f;
border-radius: 4px;
background: #202834;
padding: 4px 6px;
}
#gi-intel {
display: grid;
grid-template-columns: 1fr;
gap: 3px;
margin-bottom: 6px;
}
.gi-row {
display: grid;
grid-template-columns: 70px 1fr;
align-items: center;
gap: 6px;
border: 1px solid #3e3e3e;
border-radius: 4px;
padding: 4px 6px;
background: #252525;
}
.gi-label {
font-size: 11px;
color: #cfcfcf;
}
.gi-value {
font-size: 12px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gi-value-wrap {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
#gi-flag {
width: 18px;
height: 14px;
border-radius: 2px;
border: 1px solid #666;
object-fit: cover;
display: none;
flex: 0 0 auto;
}
#gi-map-stage {
position: relative;
height: 170px;
border-radius: 4px;
border: 1px solid #3e3e3e;
overflow: hidden;
background: #202020;
}
#gi-map {
width: 100%;
height: 100%;
object-fit: cover;
display: none;
pointer-events: none;
}
#gi-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 18px;
text-align: center;
font-size: 12px;
font-weight: 500;
color: #ddd;
background: rgba(20, 20, 20, 0.88);
}
#gi-map-ui {
position: absolute;
inset: 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 6px;
pointer-events: none;
}
#gi-coords {
max-width: 74%;
border: 1px solid #4a4a4a;
border-radius: 4px;
background: rgba(30, 30, 30, 0.9);
color: #e8e8e8;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 3px 6px;
pointer-events: auto;
}
#gi-zoom-wrap {
display: grid;
gap: 4px;
justify-items: center;
pointer-events: auto;
}
#gi-zoom-wrap button {
width: 24px;
height: 24px;
border: 1px solid #555;
border-radius: 4px;
background: #2c2c2c;
color: #fff;
font-size: 16px;
font-weight: 700;
line-height: 1;
padding: 0;
cursor: pointer;
}
#gi-zoom-wrap button:hover {
background: #383838;
}
#gi-zoom {
border: 1px solid #555;
border-radius: 4px;
background: #2a2a2a;
color: #f1f1f1;
font-size: 11px;
padding: 2px 5px;
}
#gi-hotkeys {
margin-top: 6px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
}
.gi-hotkey-field {
display: grid;
gap: 2px;
font-size: 10px;
color: #c9c9c9;
}
.gi-hotkey-field input {
width: 100%;
border: 1px solid #4a4a4a;
border-radius: 4px;
background: #252525;
color: #f2f2f2;
font-size: 11px;
padding: 4px 5px;
box-sizing: border-box;
text-transform: uppercase;
}
@media (max-width: 860px) {
#gi-shell {
left: 8px;
right: 8px;
top: 8px;
width: auto;
padding: 8px;
}
#gi-map-stage {
height: 150px;
}
#gi-hotkeys {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(style);
}
function bindUI() {
const panelUi = state.ui;
if (!panelUi) return;
panelUi.scan.addEventListener('click', async () => {
pulseActionButton(panelUi.scan);
await detectLocation('manual');
});
panelUi.auto.addEventListener('click', () => {
pulseActionButton(panelUi.auto);
toggleAutoDetect();
});
panelUi.autoPlay.addEventListener('click', () => {
pulseActionButton(panelUi.autoPlay);
toggleAutoPlay();
});
panelUi.autoGuess.addEventListener('click', () => {
pulseActionButton(panelUi.autoGuess);
toggleAutoPlaySubmitGuess();
});
panelUi.play.addEventListener('click', async () => {
await playOnce();
});
panelUi.layer.addEventListener('click', () => {
pulseActionButton(panelUi.layer);
cycleLayer();
});
panelUi.copy.addEventListener('click', async () => {
await copyCoords();
});
panelUi.zin.addEventListener('click', () => changeZoom(1));
panelUi.zout.addEventListener('click', () => changeZoom(-1));
panelUi.mapStage.addEventListener('wheel', (event) => {
event.preventDefault();
changeZoom(event.deltaY < 0 ? 1 : -1);
}, { passive: false });
fixGuessTiming();
fixOffsetNumbers();
panelUi.guessDelay.value = secText(state.autoGuessDelayMs);
panelUi.guessDeviation.value = secText(state.autoGuessDeviationMs);
panelUi.offsetBase.value = kmText(state.guessOffsetBaseM);
panelUi.offsetDeviation.value = kmText(state.guessOffsetDeviationM);
panelUi.guessDelay.addEventListener('input', updateGuessTimingFromInputs);
panelUi.guessDeviation.addEventListener('input', updateGuessTimingFromInputs);
panelUi.offsetBase.addEventListener('input', updateGuessTimingFromInputs);
panelUi.offsetDeviation.addEventListener('input', updateGuessTimingFromInputs);
panelUi.guessDelay.addEventListener('change', updateGuessTimingFromInputs);
panelUi.guessDeviation.addEventListener('change', updateGuessTimingFromInputs);
panelUi.offsetBase.addEventListener('change', updateGuessTimingFromInputs);
panelUi.offsetDeviation.addEventListener('change', updateGuessTimingFromInputs);
panelUi.keyScan.value = hotkeyInputText(state.hotkeys.scan);
panelUi.keyPlay.value = hotkeyInputText(state.hotkeys.play);
panelUi.keyCopy.value = hotkeyInputText(state.hotkeys.copy);
panelUi.keyScan.addEventListener('change', updateHotkeysFromInputs);
panelUi.keyPlay.addEventListener('change', updateHotkeysFromInputs);
panelUi.keyCopy.addEventListener('change', updateHotkeysFromInputs);
enableDrag(panelUi.header, panelUi.panel);
}
function enableDrag(handle, panel) {
let dragging = false;
let mouseStartX = 0;
let mouseStartY = 0;
let panelStartX = 0;
let panelStartY = 0;
let pendingDx = 0;
let pendingDy = 0;
let dragFrame = 0;
const applyDragPosition = () => {
dragFrame = 0;
const maxX = Math.max(0, window.innerWidth - panel.offsetWidth);
const maxY = Math.max(0, window.innerHeight - panel.offsetHeight);
panel.style.left = `${clamp(panelStartX + pendingDx, 0, maxX)}px`;
panel.style.top = `${clamp(panelStartY + pendingDy, 0, maxY)}px`;
};
handle.addEventListener('mousedown', (event) => {
const target = event.target instanceof Element ? event.target : null;
if (event.button !== 0 || (target && target.closest('button'))) {
return;
}
dragging = true;
mouseStartX = event.clientX;
mouseStartY = event.clientY;
const rect = panel.getBoundingClientRect();
panelStartX = rect.left;
panelStartY = rect.top;
pendingDx = 0;
pendingDy = 0;
// Fix the current visual position before switching from right-based to left-based layout
panel.style.left = `${panelStartX}px`;
panel.style.top = `${panelStartY}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.willChange = 'left, top';
event.preventDefault();
});
document.addEventListener('mousemove', (event) => {
if (!dragging) {
return;
}
pendingDx = event.clientX - mouseStartX;
pendingDy = event.clientY - mouseStartY;
if (!dragFrame) {
dragFrame = window.requestAnimationFrame(applyDragPosition);
}
});
document.addEventListener('mouseup', () => {
if (dragFrame) {
window.cancelAnimationFrame(dragFrame);
dragFrame = 0;
}
dragging = false;
panel.style.willChange = 'auto';
});
}
function registerHotkeys() {
document.addEventListener('keydown', async (event) => {
if (isEditable(event.target)) {
return;
}
if (isHotkeyMatch(event, CONFIG.toggleKey)) {
event.preventDefault();
state.panelVisible = !state.panelVisible;
renderVisibility();
return;
}
if (isHotkeyMatch(event, state.hotkeys.scan)) {
event.preventDefault();
await detectLocation('hotkey');
return;
}
if (isHotkeyMatch(event, state.hotkeys.play)) {
event.preventDefault();
await playOnce();
return;
}
if (isHotkeyMatch(event, state.hotkeys.copy)) {
event.preventDefault();
await copyCoords();
}
}, true);
}
function registerGuessSubmissionWatcher() {
document.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target : null;
const button = target ? target.closest('button') : null;
if (!isLikelyGuessButton(button)) {
return;
}
markAwaitingNextRound();
}, true);
}
function isEditable(target) {
if (!target) {
return false;
}
const tag = target.tagName;
return target.isContentEditable || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
}
function interceptCoordinates() {
interceptXHR();
interceptFetch();
}
function interceptXHR() {
const currentOpen = XMLHttpRequest.prototype.open;
if (typeof currentOpen !== 'function' || currentOpen[XHR_PATCH_FLAG]) {
return;
}
const wrappedOpen = function (...args) {
const method = String(args[0] || '').toUpperCase();
const url = String(args[1] || '');
if (method === 'POST' && isGeoGuessrMapRequest(url)) {
this.addEventListener('load', () => {
try {
handleCoords(parseCoordinates(this.responseText || ''), 'xhr');
} catch {}
});
}
return currentOpen.apply(this, args);
};
tagPatchedFn(wrappedOpen, XHR_PATCH_FLAG);
XMLHttpRequest.prototype.open = wrappedOpen;
}
function interceptFetch() {
if (typeof window.fetch !== 'function' || window.fetch[FETCH_PATCH_FLAG]) {
return;
}
const originalFetch = window.fetch;
const wrappedFetch = async function (...args) {
const response = await originalFetch.apply(this, args);
try {
const url = getRequestUrl(args[0]);
if (!isGeoGuessrMapRequest(url)) {
return response;
}
const text = await response.clone().text();
handleCoords(parseCoordinates(text), 'fetch');
} catch (_err) {
dbg(`Fetch sniff skipped: ${_err?.message || 'parse error'}`);
}
return response;
};
tagPatchedFn(wrappedFetch, FETCH_PATCH_FLAG);
window.fetch = wrappedFetch;
}
function getRequestUrl(input) {
if (!input) {
return '';
}
if (typeof input === 'string') {
return input;
}
if (typeof input.url === 'string') {
return input.url;
}
return '';
}
function isGeoGuessrMapRequest(url) {
return MAPS_RPC_PATTERN.test(String(url || ''));
}
function parseCoordinates(responseText) {
const text = String(responseText || '');
if (!text) {
return null;
}
const patterns = [
/"lat"\s*:\s*(-?\d+(?:\.\d+)?)\s*,\s*"lng"\s*:\s*(-?\d+(?:\.\d+)?)/g,
/\[\s*null\s*,\s*null\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]/g
];
let picked = null;
let pickedAt = -1;
for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
const lat = Number(match[1]);
const lng = Number(match[2]);
if (!isValidCoordinatePair(lat, lng)) {
continue;
}
const at = Number.isFinite(match.index) ? match.index : -1;
if (at >= pickedAt) {
picked = { lat, lng };
pickedAt = at;
}
}
}
return picked;
}
function handleCoords(coordinates, source) {
if (!coordinates) {
return;
}
const signature = `${coordinates.lat.toFixed(5)},${coordinates.lng.toFixed(5)}`;
const now = Date.now();
if (state.pendingRoundCapture) {
const pendingSignature = String(state.pendingCoordSignature || '');
const pendingElapsed = now - state.pendingCoordAt;
if (pendingSignature && signature === pendingSignature && pendingElapsed < STALE_ROUND_SIGNATURE_TTL_MS) {
return;
}
state.pendingRoundCapture = false;
state.pendingCoordSignature = '';
state.pendingCoordAt = 0;
}
if (signature === state.lastCoordSignature && (now - state.lastCoordAt) < COORD_DUPLICATE_WINDOW_MS) {
return;
}
state.lastCoordSignature = signature;
state.lastCoordAt = now;
state.coordinates = coordinates;
state.locationData = null;
clearMapCache();
dbg(`Coords ${signature} from ${source}`);
renderLocation();
renderMap();
flashMessage(`Captured ${coordsText(coordinates, 5)} from ${source}`, 'success');
if (state.autoDetect) {
scheduleAutoDetect();
}
if (state.autoPlay) {
scheduleAutoPlay('capture');
}
}
function scheduleAutoDetect() {
if (!state.autoDetect || !state.coordinates) {
return;
}
if (state.autoDetectTimer) {
clearTimeout(state.autoDetectTimer);
}
const elapsed = Date.now() - state.lastDetectAt;
const delay = Math.max(250, CONFIG.autoDetectCooldownMs - elapsed);
state.autoDetectTimer = setTimeout(async () => {
state.autoDetectTimer = null;
await detectLocation('auto');
}, delay);
}
function scheduleAutoPlay(trigger) {
if (!state.autoPlay || !state.coordinates) {
return;
}
if (trigger !== 'worker') {
void runAutoPlayForCurrentCoordinates(trigger);
}
ensureAutoPlayWorker();
}
function ensureAutoPlayWorker() {
if (state.autoPlayWorkerTimer) {
return;
}
const tick = async () => {
state.autoPlayWorkerTimer = null;
if (!state.autoPlay) {
return;
}
await runAutoPlayForCurrentCoordinates('worker');
if (!state.autoPlay) {
return;
}
state.autoPlayWorkerTimer = setTimeout(() => {
void tick();
}, CONFIG.autoPlayWorkerTickMs);
};
state.autoPlayWorkerTimer = setTimeout(() => {
void tick();
}, CONFIG.autoPlayWorkerTickMs);
}
function stopAutoPlayWorker() {
if (!state.autoPlayWorkerTimer) {
return;
}
clearTimeout(state.autoPlayWorkerTimer);
state.autoPlayWorkerTimer = null;
}
async function runAutoPlayForCurrentCoordinates(trigger) {
if (!state.autoPlay || !state.coordinates || state.autoPlayInFlight || state.pendingRoundCapture) {
return;
}
const coords = { ...state.coordinates };
const signature = coordinateSignature(coords, 6);
const captureStamp = state.lastCoordAt;
const roundKey = `${signature}|${captureStamp}`;
if (roundKey === state.autoPlayHandledSignature) {
return;
}
const isCaptureCurrent = () => {
if (!state.autoPlay || state.pendingRoundCapture || state.lastCoordAt !== captureStamp) {
return false;
}
const latest = state.coordinates;
if (!latest) {
return false;
}
return coordinateSignature(latest, 6) === signature;
};
state.autoPlayInFlight = true;
if (trigger !== 'worker') {
flashMessage(`Autoplay placing pin (${trigger})`, 'info');
}
try {
const markDelayMs = pickGuessDelayMs();
if (markDelayMs > 0) {
flashMessage(`Mark delay ${secText(markDelayMs)}s`, 'info');
await sleep(markDelayMs);
}
if (!isCaptureCurrent()) {
return;
}
const guessTarget = await buildRandomGuessTarget(coords);
if (!isCaptureCurrent()) {
return;
}
if (guessTarget.offsetM > 0) {
flashMessage(`Guess offset ${kmText(guessTarget.offsetM)}km`, 'info');
}
const placed = await placeGuessMarkerWithRetry(
guessTarget.lat,
guessTarget.lng,
CONFIG.autoPlayPinAttempts,
CONFIG.autoPlayPinPauseMs,
isCaptureCurrent
);
if (!isCaptureCurrent()) {
return;
}
if (!placed) {
if (trigger !== 'worker') {
flashMessage('Autoplay waiting for map...', 'error');
}
return;
}
state.autoPlayHandledSignature = roundKey;
flashMessage('Autoplay pin placed', 'success');
if (!state.autoPlaySubmitGuess) {
return;
}
const submitted = await clickGuessButtonWithRetry(
CONFIG.autoPlayGuessAttempts,
CONFIG.autoPlayGuessPauseMs,
isCaptureCurrent
);
if (!isCaptureCurrent()) {
return;
}
flashMessage(
submitted ? 'Autoplay guess submitted' : 'Autoplay failed: guess button not found',
submitted ? 'success' : 'error'
);
} finally {
state.autoPlayInFlight = false;
}
}
async function clickGuessButtonWithRetry(maxAttempts, pauseMs, continueFn) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
if (typeof continueFn === 'function' && !continueFn()) {
return false;
}
if (clickGuessButton()) {
return true;
}
await sleep(pauseMs);
}
return false;
}
function coordinateSignature(coords, precision) {
return `${coords.lat.toFixed(precision)},${coords.lng.toFixed(precision)}`;
}
async function detectLocation(trigger) {
const point = state.coordinates;
if (!point) {
flashMessage('No coordinates captured yet', 'error');
return;
}
if (state.detectInFlight) {
flashMessage('Scan already in progress', 'info');
return;
}
state.detectInFlight = true;
state.lastDetectAt = Date.now();
renderControls();
const short = coordsText(point, 5);
flashMessage(`Scanning ${short} (${trigger})`, 'info');
try {
const data = await reverseGeocode(point.lat, point.lng);
if (!data) {
flashMessage('Scan failed: geocoder unavailable', 'error');
return;
}
state.locationData = data;
renderLocation();
const place = [data.city, data.state, data.country].filter(Boolean).join(', ') || 'Unknown';
flashMessage(`Decoded: ${place}`, 'success');
} finally {
state.detectInFlight = false;
renderControls();
}
}
async function reverseGeocode(lat, lng) {
if (!isValidCoordinatePair(lat, lng)) {
return null;
}
const locationIqResult = await reverseGeocodeWithLocationIq(lat, lng);
if (locationIqResult) {
return locationIqResult;
}
dbg('Falling back to BigDataCloud geocoder');
return reverseGeocodeWithBigDataCloud(lat, lng);
}
async function reverseGeocodeWithLocationIq(lat, lng) {
const keys = CONFIG.locationIqKeys;
if (!Array.isArray(keys) || !keys.length) {
return null;
}
for (let offset = 0; offset < keys.length; offset += 1) {
const index = (state.apiKeyStartIndex + offset) % keys.length;
const apiKey = String(keys[index] || '').trim();
if (!apiKey) {
continue;
}
const url = `https://us1.locationiq.com/v1/reverse?key=${encodeURIComponent(apiKey)}&lat=${lat}&lon=${lng}&format=json&accept-language=en`;
try {
const response = await gmRequest({ method: 'GET', url });
if (!response || response.status !== 200) {
continue;
}
const body = tryJson(response.responseText);
const parsed = parseAddressPayload(body);
if (!parsed) {
continue;
}
state.apiKeyStartIndex = index;
return parsed;
} catch (_err) {
dbg(`LocationIQ key #${index + 1} failed`);
}
}
return null;
}
async function reverseGeocodeWithBigDataCloud(lat, lng) {
const params = new URLSearchParams({
latitude: String(lat),
longitude: String(lng),
localityLanguage: 'en'
});
const url = `https://api.bigdatacloud.net/data/reverse-geocode-client?${params.toString()}`;
try {
const response = await gmRequest({ method: 'GET', url });
if (!response || response.status !== 200) {
return null;
}
const body = tryJson(response.responseText);
return parseBigDataCloudPayload(body);
} catch (_err) {
return null;
}
}
function parseAddressPayload(payload) {
if (!payload || typeof payload !== 'object') {
return null;
}
const address = (payload.address && typeof payload.address === 'object')
? payload.address
: payload;
const country = firstFilled([address.country, address.country_name]);
const state = firstFilled([address.state, address.region, address.county]);
const city = firstFilled([
address.city,
address.town,
address.village,
address.suburb,
address.hamlet,
address.municipality
]);
if (!country && !state && !city) {
return null;
}
return {
country: country || '',
countryCode: normCC(address.country_code || address.countryCode),
state: state || '',
city: city || '',
_geoTokens: [
payload.display_name,
payload.name,
payload.type,
payload.class,
address.ocean,
address.sea,
address.bay,
address.strait,
address.channel,
address.river,
address.water,
address.waterway,
address.lake
].map((value) => String(value || '').trim()).filter(Boolean).join(' ').toLowerCase()
};
}
function parseBigDataCloudPayload(payload) {
if (!payload || typeof payload !== 'object') {
return null;
}
const admin = Array.isArray(payload.localityInfo?.administrative) ? payload.localityInfo.administrative : [];
const informative = Array.isArray(payload.localityInfo?.informative) ? payload.localityInfo.informative : [];
const country = firstFilled([payload.countryName, payload.country]);
const state = firstFilled([
payload.principalSubdivision,
admin[1]?.name,
admin[0]?.name
]);
const city = firstFilled([
payload.city,
payload.locality,
payload.town,
payload.village,
informative[0]?.name
]);
if (!country && !state && !city) {
return null;
}
return {
country: country || '',
countryCode: normCC(payload.countryCode),
state: state || '',
city: city || '',
_geoTokens: [
payload.locality,
payload.city,
payload.principalSubdivision,
payload.countryName,
...informative.map((item) => item && item.name),
...informative.map((item) => item && item.description)
].map((value) => String(value || '').trim()).filter(Boolean).join(' ').toLowerCase()
};
}
function gmRequest(options) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') {
reject(new Error('GM_xmlhttpRequest unavailable'));
return;
}
GM_xmlhttpRequest({
method: options.method,
url: options.url,
onload: resolve,
onerror: reject,
ontimeout: reject
});
});
}
function toggleAutoDetect() {
state.autoDetect = !state.autoDetect;
renderControls();
if (!state.autoDetect && state.autoDetectTimer) {
clearTimeout(state.autoDetectTimer);
state.autoDetectTimer = null;
}
if (state.autoDetect && state.coordinates) {
scheduleAutoDetect();
}
flashMessage(`Auto ${state.autoDetect ? 'enabled' : 'disabled'}`, 'info');
}
function toggleAutoPlay() {
state.autoPlay = !state.autoPlay;
renderControls();
if (!state.autoPlay) {
stopAutoPlayWorker();
state.autoPlayInFlight = false;
flashMessage('Autoplay disabled', 'info');
return;
}
state.autoPlayHandledSignature = '';
ensureAutoPlayWorker();
if (state.coordinates) {
scheduleAutoPlay('toggle');
}
flashMessage('Autoplay enabled', 'info');
}
function toggleAutoPlaySubmitGuess() {
state.autoPlaySubmitGuess = !state.autoPlaySubmitGuess;
renderControls();
if (state.autoPlay && state.coordinates) {
state.autoPlayHandledSignature = '';
scheduleAutoPlay('guess-toggle');
}
flashMessage(`AutoGuess ${state.autoPlaySubmitGuess ? 'enabled' : 'disabled'}`, 'info');
}
async function playOnce() {
pulseActionButton(state.ui ? state.ui.play : null);
const point = state.coordinates;
if (!point) {
flashMessage('No coordinates captured yet', 'error');
return;
}
const coords = { ...point };
const guessTarget = await buildRandomGuessTarget(coords);
if (guessTarget.offsetM > 0) {
flashMessage(`Guess offset ${kmText(guessTarget.offsetM)}km`, 'info');
}
const placed = await placeGuessMarkerWithRetry(
guessTarget.lat,
guessTarget.lng,
CONFIG.autoPlayPinAttempts,
CONFIG.autoPlayPinPauseMs
);
if (!placed) {
flashMessage('Play failed: map is not ready', 'error');
return;
}
state.autoPlayHandledSignature = `${coordinateSignature(coords, 6)}|${state.lastCoordAt}`;
flashMessage('Point placed', 'success');
}
function updateGuessTimingFromInputs() {
const ui = state.ui;
if (!ui) return;
const secField = (raw, fallbackMs, maxMs) => {
const n = Number(raw);
const ms = Number.isFinite(n) ? Math.round(n * 1000) : fallbackMs;
return clamp(ms, 0, maxMs);
};
const kmField = (raw, fallbackM, maxM) => {
const n = Number(raw);
const m = Number.isFinite(n) ? Math.round(n * 1000) : fallbackM;
return clamp(m, 0, maxM);
};
const delay = secField(ui.guessDelay.value, CONFIG.autoPlayGuessBaseDelayMs, CONFIG.maxMarkDelayMs);
const deviation = secField(ui.guessDeviation.value, CONFIG.autoPlayGuessDeviationMs, CONFIG.maxMarkDeviationMs);
const offsetBase = kmField(ui.offsetBase.value, CONFIG.guessOffsetBaseM, CONFIG.maxGuessOffsetM);
const offsetDeviation = kmField(ui.offsetDeviation.value, CONFIG.guessOffsetDeviationM, CONFIG.maxGuessOffsetDeviationM);
state.autoGuessDelayMs = delay;
state.autoGuessDeviationMs = Math.min(deviation, delay);
state.guessOffsetBaseM = offsetBase;
state.guessOffsetDeviationM = Math.min(offsetDeviation, offsetBase);
ui.guessDelay.value = secText(state.autoGuessDelayMs);
ui.guessDeviation.value = secText(state.autoGuessDeviationMs);
ui.offsetBase.value = kmText(state.guessOffsetBaseM);
ui.offsetDeviation.value = kmText(state.guessOffsetDeviationM);
saveNumberSetting(STORAGE_KEYS.autoGuessDelayMs, state.autoGuessDelayMs);
saveNumberSetting(STORAGE_KEYS.autoGuessDeviationMs, state.autoGuessDeviationMs);
saveNumberSetting(STORAGE_KEYS.guessOffsetBaseM, state.guessOffsetBaseM);
saveNumberSetting(STORAGE_KEYS.guessOffsetDeviationM, state.guessOffsetDeviationM);
renderControls();
}
function updateHotkeysFromInputs() {
const ui = state.ui;
if (!ui) return;
state.hotkeys.scan = readHotkeyField(ui.keyScan.value, CONFIG.detectKey);
state.hotkeys.play = readHotkeyField(ui.keyPlay.value, CONFIG.playKey);
state.hotkeys.copy = readHotkeyField(ui.keyCopy.value, CONFIG.copyKey);
ui.keyScan.value = hotkeyInputText(state.hotkeys.scan);
ui.keyPlay.value = hotkeyInputText(state.hotkeys.play);
ui.keyCopy.value = hotkeyInputText(state.hotkeys.copy);
saveHotkeySetting(STORAGE_KEYS.hotkeyScan, state.hotkeys.scan);
saveHotkeySetting(STORAGE_KEYS.hotkeyPlay, state.hotkeys.play);
saveHotkeySetting(STORAGE_KEYS.hotkeyCopy, state.hotkeys.copy);
}
function fixGuessTiming() {
state.autoGuessDelayMs = boundInt(state.autoGuessDelayMs, CONFIG.autoPlayGuessBaseDelayMs, 0, CONFIG.maxMarkDelayMs);
state.autoGuessDeviationMs = boundInt(state.autoGuessDeviationMs, CONFIG.autoPlayGuessDeviationMs, 0, CONFIG.maxMarkDeviationMs);
state.autoGuessDeviationMs = Math.min(state.autoGuessDeviationMs, state.autoGuessDelayMs);
}
function fixOffsetNumbers() {
state.guessOffsetBaseM = boundInt(state.guessOffsetBaseM, CONFIG.guessOffsetBaseM, 0, CONFIG.maxGuessOffsetM);
state.guessOffsetDeviationM = boundInt(state.guessOffsetDeviationM, CONFIG.guessOffsetDeviationM, 0, CONFIG.maxGuessOffsetDeviationM);
state.guessOffsetDeviationM = Math.min(state.guessOffsetDeviationM, state.guessOffsetBaseM);
}
function pickGuessDelayMs() {
fixGuessTiming();
const base = state.autoGuessDelayMs;
const deviation = state.autoGuessDeviationMs;
const safeDeviation = Math.min(base, deviation);
const min = Math.max(0, base - safeDeviation);
const max = base + safeDeviation;
return randBetween(min, max);
}
function pickGuessOffsetMeters() {
fixOffsetNumbers();
const base = state.guessOffsetBaseM;
const deviation = state.guessOffsetDeviationM;
const safeDeviation = Math.min(base, deviation);
const min = Math.max(0, base - safeDeviation);
const max = base + safeDeviation;
return randBetween(min, max);
}
async function buildRandomGuessTarget(coords) {
const attempts = clamp(
Math.round(CONFIG.guessTargetAvoidWaterAttempts),
1,
30
);
const firstCandidate = buildOffsetGuessTarget(coords);
if (firstCandidate.offsetM <= 0 || attempts <= 1) {
return firstCandidate;
}
if (await isLikelyLandGuessTarget(firstCandidate)) {
return firstCandidate;
}
for (let attempt = 1; attempt < attempts; attempt += 1) {
const candidate = buildOffsetGuessTarget(coords);
if (await isLikelyLandGuessTarget(candidate)) {
return candidate;
}
}
return firstCandidate;
}
function buildOffsetGuessTarget(coords) {
const offsetM = pickGuessOffsetMeters();
if (offsetM <= 0) {
return { lat: coords.lat, lng: coords.lng, offsetM: 0 };
}
const bearingDeg = Math.random() * 360;
const moved = destinationPoint(coords.lat, coords.lng, bearingDeg, offsetM / 1000);
return {
lat: moved.lat,
lng: moved.lng,
offsetM
};
}
async function isLikelyLandGuessTarget(target) {
if (!target || target.offsetM <= 0) {
return true;
}
return isLikelyLandCoordinate(target.lat, target.lng);
}
async function isLikelyLandCoordinate(lat, lng) {
if (!isValidCoordinatePair(lat, lng)) {
return false;
}
const precision = clamp(Math.round(CONFIG.guessTargetProbePrecision), 1, 4);
const key = `${lat.toFixed(precision)},${lng.toFixed(precision)}`;
if (state.guessLandProbeCache.has(key)) {
return state.guessLandProbeCache.get(key);
}
let isLand = false;
try {
const geo = await reverseGeocode(lat, lng);
isLand = !isWaterLikeLocation(geo);
} catch (_err) {
isLand = false;
}
state.guessLandProbeCache.set(key, isLand);
trimGuessProbeCache();
return isLand;
}
function trimGuessProbeCache() {
const limit = clamp(Math.round(CONFIG.guessTargetProbeCacheLimit), 40, 2000);
while (state.guessLandProbeCache.size > limit) {
const oldest = state.guessLandProbeCache.keys().next().value;
if (!oldest) {
break;
}
state.guessLandProbeCache.delete(oldest);
}
}
function isWaterLikeLocation(data) {
if (!data || typeof data !== 'object') {
return true;
}
const summary = [
data.country,
data.state,
data.city,
data._geoTokens
].map((value) => String(value || '').trim().toLowerCase()).filter(Boolean).join(' ');
if (!summary) {
return true;
}
const waterWords = [
'ocean',
'sea',
'gulf',
'bay',
'strait',
'channel',
'lake',
'lagoon',
'reservoir',
'river',
'water',
'harbor',
'harbour',
'fjord',
'sound',
'inlet'
];
if (waterWords.some((word) => summary.includes(word))) {
return true;
}
if (!data.state && !data.city) {
return true;
}
return false;
}
function destinationPoint(lat, lng, bearingDeg, distanceKm) {
const earthRadiusKm = 6371;
const distanceRad = distanceKm / earthRadiusKm;
const bearingRad = bearingDeg * (Math.PI / 180);
const latRad = lat * (Math.PI / 180);
const lngRad = lng * (Math.PI / 180);
const newLatRad = Math.asin(
(Math.sin(latRad) * Math.cos(distanceRad)) +
(Math.cos(latRad) * Math.sin(distanceRad) * Math.cos(bearingRad))
);
const newLngRad = lngRad + Math.atan2(
Math.sin(bearingRad) * Math.sin(distanceRad) * Math.cos(latRad),
Math.cos(distanceRad) - (Math.sin(latRad) * Math.sin(newLatRad))
);
return {
lat: newLatRad * (180 / Math.PI),
lng: normalizeLongitude(newLngRad * (180 / Math.PI))
};
}
function normalizeLongitude(lng) {
let normalized = lng;
while (normalized < -180) {
normalized += 360;
}
while (normalized > 180) {
normalized -= 360;
}
return normalized;
}
function cycleLayer() {
state.mapLayerIndex = (state.mapLayerIndex + 1) % CONFIG.mapLayers.length;
renderControls();
renderMap();
const layer = activeLayer();
flashMessage(`Layer: ${layer.label}`, 'info');
}
async function copyCoords() {
pulseActionButton(state.ui ? state.ui.copy : null);
const point = state.coordinates;
if (!point) {
flashMessage('Nothing to copy yet', 'error');
return;
}
const f6 = (n) => Number(n).toFixed(6);
const text = `${f6(point.lat)}, ${f6(point.lng)}`;
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(text);
} else {
fallbackCopy(text);
}
flashMessage('Coordinates copied', 'success');
} catch (_err) {
flashMessage('Clipboard access failed', 'error');
}
}
function fallbackCopy(text) {
const area = document.createElement('textarea');
area.value = text;
area.setAttribute('readonly', 'readonly');
area.style.position = 'absolute';
area.style.left = '-9999px';
document.body.appendChild(area);
area.select();
document.execCommand('copy');
area.remove();
}
function markAwaitingNextRound() {
const point = state.coordinates;
state.pendingRoundCapture = true;
state.pendingCoordSignature = point ? coordinateSignature(point, 5) : String(state.lastCoordSignature || '');
state.pendingCoordAt = Date.now();
state.autoPlayHandledSignature = '';
}
async function placeGuessMarkerWithRetry(lat, lng, maxAttempts, pauseMs, continueFn) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
if (typeof continueFn === 'function' && !continueFn()) {
return false;
}
if (placeGuessMarker(lat, lng)) {
return true;
}
await sleep(pauseMs);
}
return false;
}
function placeGuessMarker(lat, lng) {
const mapCandidates = getGuessMapElements();
if (!mapCandidates.length) {
return false;
}
const guessWasReady = isGuessReadyToSubmit();
for (const mapElement of mapCandidates) {
const clickStores = getClickStoresNearElement(mapElement);
for (const clickStore of clickStores) {
const invoked = invokeGoogleMapClickStore(clickStore, lat, lng);
if (!invoked) {
continue;
}
if (guessWasReady || isGuessReadyToSubmit()) {
return true;
}
}
}
return false;
}
function getGuessMapElements() {
const nowMs = Date.now();
if (state.guessMapCacheAt && (nowMs - state.guessMapCacheAt) <= DOM_CACHE_MS) {
return state.guessMapCache.filter((node) => isElementVisible(node));
}
// Geoguessr UI classes change often
const selectors = [
'[class^="guess-map_canvas__"]',
'[class*="guess-map_canvas__"]',
'[class^="region-map_mapCanvas__"]',
'[class*="region-map_mapCanvas__"]',
'[data-qa="guess-map"]',
'[data-qa*="guess-map"]',
'[class*="guess-map"]',
'[class*="region-map"] canvas',
'[class*="guess-map"] canvas'
];
const seen = new Set();
const hits = [];
const looksUsable = (el) => {
const box = el.getBoundingClientRect();
return box.width >= 40 && box.height >= 40;
};
for (const sel of selectors) {
for (const node of document.querySelectorAll(sel)) {
const el = (node instanceof HTMLCanvasElement || node instanceof HTMLImageElement) && node.parentElement
? node.parentElement
: node;
if (!(el instanceof HTMLElement) || seen.has(el)) {
continue;
}
if (!isElementVisible(el)) {
continue;
}
if (!looksUsable(el)) {
continue;
}
seen.add(el);
hits.push(el);
}
}
// Prefer map elements likely belonging to the active in-round mini-map
const picked = hits.sort((a, b) => scoreGuessMapCandidate(b) - scoreGuessMapCandidate(a));
state.guessMapCache = picked;
state.guessMapCacheAt = nowMs;
dbg(`Map candidates: ${picked.length}`);
return picked;
}
function getClickStoresNearElement(element) {
const stores = [];
const seenStores = new Set();
const nodesToCheck = [];
let current = element;
for (let index = 0; index < 6 && current; index += 1) {
nodesToCheck.push(current);
current = current.parentElement;
}
for (const node of nodesToCheck) {
const directStore = node?.__e3_?.click;
if (isClickStoreObject(directStore) && !seenStores.has(directStore)) {
seenStores.add(directStore);
stores.push(directStore);
}
const reactKeys = Object.keys(node).filter((key) =>
key.startsWith('__reactFiber$') || key.startsWith('__reactProps$')
);
for (const reactKey of reactKeys) {
const root = node[reactKey];
for (const store of collectClickStores(root)) {
if (!seenStores.has(store)) {
seenStores.add(store);
stores.push(store);
}
}
}
}
return stores;
}
function collectClickStores(root) {
if (!root || typeof root !== 'object') {
return [];
}
const stores = [];
const seenStores = new Set();
const queue = [{ value: root, depth: 0 }];
const seen = new Set();
for (let queueIndex = 0; queueIndex < queue.length; queueIndex += 1) {
const { value, depth } = queue[queueIndex];
if (!value || typeof value !== 'object' || seen.has(value)) {
continue;
}
if (value instanceof Element || value === window || value === document) {
continue;
}
if (depth > 6 || seen.size > 360) {
continue;
}
seen.add(value);
const fromMap = value?.map?.__e3_?.click;
const fromDirect = value?.__e3_?.click;
if (isClickStoreObject(fromMap) && !seenStores.has(fromMap)) {
seenStores.add(fromMap);
stores.push(fromMap);
}
if (isClickStoreObject(fromDirect) && !seenStores.has(fromDirect)) {
seenStores.add(fromDirect);
stores.push(fromDirect);
}
if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === 'object') {
queue.push({ value: item, depth: depth + 1 });
}
}
continue;
}
for (const key of Object.keys(value)) {
if (key === 'ownerDocument' || key === 'parentElement' || key === 'parentNode') {
continue;
}
const next = value[key];
if (next && typeof next === 'object') {
queue.push({ value: next, depth: depth + 1 });
}
}
}
return stores;
}
function isClickStoreObject(value) {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function invokeGoogleMapClickStore(clickStore, lat, lng) {
const eventLike = {
latLng: {
lat: () => lat,
lng: () => lng
}
};
let invoked = false;
for (const key of Object.keys(clickStore)) {
const branch = clickStore[key];
if (!branch || typeof branch !== 'object') {
continue;
}
for (const subKey of Object.keys(branch)) {
if (typeof branch[subKey] !== 'function') {
continue;
}
try {
branch[subKey](eventLike);
invoked = true;
} catch {}
}
}
return invoked;
}
function clickGuessButton() {
const button = findGuessButton(false);
if (!button) {
return false;
}
markAwaitingNextRound();
button.click();
return true;
}
function isGuessReadyToSubmit() {
const button = findGuessButton(true);
return !!button && !button.disabled;
}
function findGuessButton(allowDisabled) {
const selectors = [
'[data-qa="perform-guess"]',
'[data-qa="guess-button"]',
'button[data-qa*="guess"]',
'button[class*="guess-actions_guess"]',
'button[class*="guess-button"]'
];
const pickBySelector = (selector) => Array.from(document.querySelectorAll(selector)).find((node) =>
isGuessButtonCandidate(node, allowDisabled)
);
for (const selector of selectors) {
const btn = pickBySelector(selector);
if (btn) {
return btn;
}
}
return Array.from(document.querySelectorAll('button')).find((node) => {
if (!isGuessButtonCandidate(node, allowDisabled)) {
return false;
}
const label = String(node.textContent || '').trim().toLowerCase();
return label === 'guess' || label.includes('make guess');
}) || null;
}
function isLikelyGuessButton(node) {
if (!(node instanceof HTMLButtonElement)) {
return false;
}
if (!isGuessButtonCandidate(node, true)) {
return false;
}
const dataQa = String(node.getAttribute('data-qa') || '').trim().toLowerCase();
const className = String(node.className || '').trim().toLowerCase();
const label = String(node.textContent || '').trim().toLowerCase();
return dataQa.includes('guess') || className.includes('guess') || label === 'guess' || label.includes('make guess');
}
function isGuessButtonCandidate(node, allowDisabled) {
if (!(node instanceof HTMLButtonElement)) {
return false;
}
if (!allowDisabled && node.disabled) {
return false;
}
if (!isElementVisible(node)) {
return false;
}
const style = window.getComputedStyle(node);
return style.pointerEvents !== 'none';
}
function scoreGuessMapCandidate(element) {
const rect = element.getBoundingClientRect();
const area = Math.max(0, rect.width * rect.height);
const bottomRightBias = (rect.right > (window.innerWidth * 0.5) && rect.bottom > (window.innerHeight * 0.5)) ? 1000000 : 0;
return area + bottomRightBias;
}
function isElementVisible(node) {
if (!(node instanceof Element) || !node.isConnected) {
return false;
}
const rect = node.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) {
return false;
}
if (rect.right < 0 || rect.bottom < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight) {
return false;
}
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
return true;
}
function changeZoom(delta) {
const next = clamp(state.mapZoom + delta, CONFIG.mapZoomMin, CONFIG.mapZoomMax);
if (next === state.mapZoom) {
return;
}
state.mapZoom = next;
renderMap();
}
function renderAll() {
renderVisibility();
renderControls();
renderLocation();
renderMap();
}
function renderVisibility() {
const uiState = state.ui;
if (uiState) {
uiState.panel.style.display = state.panelVisible ? 'block' : 'none';
}
}
function renderControls() {
const panelUi = state.ui;
if (!panelUi) return;
const mkRange = (min, max, fmt, unit) => `${fmt(min)}-${fmt(max)} ${unit}`;
panelUi.layer.textContent = `Layer ${state.mapLayerIndex + 1}`;
panelUi.auto.textContent = state.autoDetect ? 'AutoScan On' : 'AutoScan Off';
panelUi.auto.dataset.active = state.autoDetect ? 'true' : 'false';
panelUi.autoPlay.textContent = state.autoPlay ? 'AutoPlay On' : 'AutoPlay Off';
panelUi.autoPlay.dataset.active = state.autoPlay ? 'true' : 'false';
panelUi.autoGuess.textContent = state.autoPlaySubmitGuess ? 'AutoGuess On' : 'AutoGuess Off';
panelUi.autoGuess.dataset.active = state.autoPlaySubmitGuess ? 'true' : 'false';
const minDelay = Math.max(0, state.autoGuessDelayMs - state.autoGuessDeviationMs);
const maxDelay = state.autoGuessDelayMs + state.autoGuessDeviationMs;
panelUi.guessRange.textContent = `Mark delay range: ${mkRange(minDelay, maxDelay, secText, 's')}`;
const minOffset = Math.max(0, state.guessOffsetBaseM - state.guessOffsetDeviationM);
const maxOffset = state.guessOffsetBaseM + state.guessOffsetDeviationM;
panelUi.offsetRange.textContent = `Guess offset range: ${mkRange(minOffset, maxOffset, kmText, 'km')}`;
panelUi.scan.disabled = state.detectInFlight;
panelUi.scan.textContent = state.detectInFlight ? 'SCANNING...' : 'SCAN';
}
function renderLocation() {
const view = state.ui;
if (!view) return;
const pending = state.coordinates ? 'Scan needed' : 'N/A';
const data = state.locationData;
view.country.textContent = data && data.country ? data.country : pending;
view.region.textContent = data && data.state ? data.state : pending;
view.city.textContent = data && data.city ? data.city : pending;
if (data && data.countryCode) {
view.flag.src = `https://flagcdn.com/24x18/${data.countryCode}.png`;
view.flag.style.display = 'block';
} else {
view.flag.style.display = 'none';
view.flag.removeAttribute('src');
}
}
function renderMap() {
const ui = state.ui;
if (!ui) return;
ui.zoom.textContent = `Zoom ${state.mapZoom}`;
const point = state.coordinates;
if (!point) {
ui.map.style.display = 'none';
ui.empty.style.display = 'flex';
ui.coords.textContent = 'Coordinates: N/A';
} else {
const { lat, lng } = point;
const w = ui.mapStage.clientWidth || 520;
const h = ui.mapStage.clientHeight || 292;
ui.coords.textContent = `Coordinates: ${coordsText({ lat, lng }, 5)}`;
ui.map.src = buildMapUrl(lat, lng, state.mapZoom, w, h);
ui.map.style.display = 'block';
ui.empty.style.display = 'none';
}
}
function buildMapUrl(lat, lng, zoom, width, height) {
const layer = activeLayer();
const fx = lng.toFixed(6);
const fy = lat.toFixed(6);
const w = clamp(Math.round(width), 260, 650);
const h = clamp(Math.round(height), 160, 360);
return `https://static-maps.yandex.ru/1.x/?ll=${fx},${fy}&z=${zoom}&size=${w},${h}&l=${layer.param}&lang=en_US&pt=${fx},${fy},${layer.marker}`;
}
function flashMessage(text, tone) {
if (tone === 'error') {
console.warn(`[GeoIntel] ${text}`);
return;
}
console.info(`[GeoIntel] ${text}`);
}
function pulseActionButton(button, durationMs) {
if (!(button instanceof HTMLButtonElement)) {
return;
}
if (button.__giFlashTimer) {
clearTimeout(button.__giFlashTimer);
button.__giFlashTimer = null;
}
button.dataset.pressed = 'true';
const hold = Number.isFinite(durationMs) ? Math.max(0, Math.round(durationMs)) : 140;
button.__giFlashTimer = setTimeout(() => {
delete button.dataset.pressed;
button.__giFlashTimer = null;
}, hold);
}
function clearMapCache() {
state.guessMapCache = [];
state.guessMapCacheAt = 0;
}
function dbg(message) {
if (!DEBUG) {
return;
}
console.debug(`[GeoIntel:debug] ${message}`);
}
function tagPatchedFn(fn, patchFlag) {
try {
Object.defineProperty(fn, patchFlag, {
value: true,
configurable: false,
enumerable: false,
writable: false
});
} catch (_err) {
try {
fn[patchFlag] = true;
} catch (_err2) {
// Ignore inability to write marker
}
}
}
function activeLayer() {
return CONFIG.mapLayers[state.mapLayerIndex] || CONFIG.mapLayers[0];
}
function coordsText(coords, p) {
return `${coords.lat.toFixed(p)}, ${coords.lng.toFixed(p)}`;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function isValidCoordinatePair(lat, lng) {
return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180;
}
function tryJson(text) {
try {
return JSON.parse(String(text || '{}'));
} catch (_err) {
return null;
}
}
function firstFilled(values) {
for (const value of values) {
const asString = String(value || '').trim();
if (asString) {
return asString;
}
}
return '';
}
function normCC(value) {
return String(value || '').trim().toLowerCase();
}
function loadApiKeysSetting(primaryKey) {
try {
const current = parseApiKeysFromStorage(window.localStorage.getItem(primaryKey));
return current;
} catch (_err) {
dbg('Failed to read API keys from storage');
return [];
}
}
function parseApiKeysFromStorage(raw) {
if (raw === null || raw === undefined) {
return [];
}
let values = [];
const text = String(raw).trim();
if (!text) {
return [];
}
if (text.startsWith('[')) {
const parsed = tryJson(text);
if (Array.isArray(parsed)) {
values = parsed;
}
} else {
values = text.split(/[,\n;]+/);
}
const result = [];
const seen = new Set();
for (const value of values) {
const key = String(value || '').trim();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
result.push(key);
}
return result;
}
function loadNumberSetting(key, fallback) {
try {
const raw = window.localStorage.getItem(key);
if (raw === null || raw === '') {
return fallback;
}
const parsed = Number(raw);
return Number.isFinite(parsed) ? Math.round(parsed) : fallback;
} catch (_err) {
return fallback;
}
}
function loadHotkeySetting(key, fallback) {
try {
const raw = window.localStorage.getItem(key);
return normalizeHotkey(raw || fallback, normalizeHotkey(fallback, ''));
} catch (_err) {
return normalizeHotkey(fallback, '');
}
}
function saveNumberSetting(key, value) {
try {
window.localStorage.setItem(key, String(value));
} catch {}
}
function saveHotkeySetting(key, hotkey) {
try {
window.localStorage.setItem(key, String(hotkey || ''));
} catch (_err) {
dbg(`Hotkey "${key}" not persisted`);
}
}
function normalizeHotkey(value, fallback) {
const raw = String(value || '').trim();
if (!raw) {
return fallback || '';
}
const lower = raw.toLowerCase();
if (lower === ' ' || lower === 'spacebar' || lower === 'space') {
return 'space';
}
if (lower === 'esc') {
return 'escape';
}
if (lower === 'del') {
return 'delete';
}
return lower;
}
function readHotkeyField(value, fallback) {
return normalizeHotkey(value, normalizeHotkey(fallback, ''));
}
function hotkeyInputText(hotkey) {
const normalized = normalizeHotkey(hotkey, '');
if (!normalized) {
return '';
}
if (normalized.length === 1) {
return normalized.toUpperCase();
}
if (normalized === 'space') {
return 'SPACE';
}
if (normalized.startsWith('arrow')) {
return normalized.replace('arrow', 'ARROW ').toUpperCase();
}
return normalized.toUpperCase();
}
function normEventKey(eventKey) {
return normalizeHotkey(eventKey, '');
}
function isHotkeyMatch(event, hotkey) {
if (!hotkey || event.repeat || event.ctrlKey || event.altKey || event.metaKey) {
return false;
}
return normEventKey(event.key) === normalizeHotkey(hotkey, '');
}
function boundInt(value, fallback, min, max) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return clamp(Math.round(fallback), min, max);
}
return clamp(Math.round(parsed), min, max);
}
function secText(ms) {
const seconds = Number(ms) / 1000;
if (!Number.isFinite(seconds)) {
return '0';
}
return seconds.toFixed(2).replace(/\.?0+$/, '');
}
function kmText(meters) {
const km = Number(meters) / 1000;
if (!Number.isFinite(km)) {
return '0';
}
return km.toFixed(2).replace(/\.?0+$/, '');
}
function randBetween(min, max) {
const low = Math.ceil(Math.min(min, max));
const high = Math.floor(Math.max(min, max));
if (low >= high) {
return low;
}
return low + Math.floor(Math.random() * ((high - low) + 1));
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function onResize() {
if (state.resizeTimer) {
clearTimeout(state.resizeTimer);
}
state.resizeTimer = setTimeout(() => {
state.resizeTimer = null;
clearMapCache();
renderMap();
}, 120);
}
})();