Advanced geolocation utility with AI-powered hints, coordinate detection, and map analysis tools for GeoGuessr
Size
19.4 KB
Version
1.0.1
Created
Jan 28, 2026
Updated
7 days ago
1// ==UserScript==
2// @name GeoGuessr Location Helper
3// @description Advanced geolocation utility with AI-powered hints, coordinate detection, and map analysis tools for GeoGuessr
4// @version 1.0.1
5// @match https://*.geoguessr.com/*
6// @icon https://www.geoguessr.com/_next/static/media/favicon.bffdd9d3.png
7// @grant GM.xmlhttpRequest
8// @grant GM.getValue
9// @grant GM.setValue
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 console.log('GeoGuessr Location Helper initialized');
15
16 // Utility panel state
17 let utilityPanel = null;
18 let isPanelVisible = false;
19 let currentCoordinates = null;
20 let isAnalyzing = false;
21
22 // Debounce function for performance
23 function debounce(func, wait) {
24 let timeout;
25 return function executedFunction(...args) {
26 const later = () => {
27 clearTimeout(timeout);
28 func(...args);
29 };
30 clearTimeout(timeout);
31 timeout = setTimeout(later, wait);
32 };
33 }
34
35 // Create the utility panel UI
36 function createUtilityPanel() {
37 const panel = document.createElement('div');
38 panel.id = 'geoguessr-utility-panel';
39 panel.style.cssText = `
40 position: fixed;
41 top: 80px;
42 right: 20px;
43 width: 320px;
44 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
45 border-radius: 12px;
46 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
47 z-index: 10000;
48 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
49 color: white;
50 display: ${isPanelVisible ? 'block' : 'none'};
51 backdrop-filter: blur(10px);
52 `;
53
54 panel.innerHTML = `
55 <div style="padding: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.2);">
56 <div style="display: flex; justify-content: space-between; align-items: center;">
57 <h3 style="margin: 0; font-size: 18px; font-weight: 600;">π Location Helper</h3>
58 <button id="close-utility-panel" style="background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center;">Γ</button>
59 </div>
60 </div>
61
62 <div style="padding: 16px;">
63 <!-- Coordinates Section -->
64 <div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
65 <div style="font-size: 12px; opacity: 0.9; margin-bottom: 6px; font-weight: 500;">π Current Coordinates</div>
66 <div id="coordinates-display" style="font-size: 14px; font-family: monospace; word-break: break-all;">
67 Detecting location...
68 </div>
69 <button id="copy-coordinates" style="margin-top: 8px; background: rgba(255, 255, 255, 0.25); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; width: 100%; font-weight: 500;">
70 π Copy Coordinates
71 </button>
72 </div>
73
74 <!-- Map Analysis Section -->
75 <div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
76 <div style="font-size: 12px; opacity: 0.9; margin-bottom: 6px; font-weight: 500;">πΊοΈ Map Analysis</div>
77 <div id="map-info" style="font-size: 13px; line-height: 1.6;">
78 <div style="margin-bottom: 4px;">β’ <span id="heading-info">Heading: N/A</span></div>
79 <div style="margin-bottom: 4px;">β’ <span id="zoom-info">Zoom: N/A</span></div>
80 <div>β’ <span id="pov-info">POV: N/A</span></div>
81 </div>
82 </div>
83
84 <!-- AI Hints Section -->
85 <div style="background: rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
86 <div style="font-size: 12px; opacity: 0.9; margin-bottom: 8px; font-weight: 500;">π€ AI Location Hints</div>
87 <button id="get-ai-hints" style="background: rgba(255, 255, 255, 0.25); border: none; color: white; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; width: 100%; font-weight: 500; margin-bottom: 8px;">
88 β¨ Get AI Hints
89 </button>
90 <div id="ai-hints-display" style="font-size: 12px; line-height: 1.6; max-height: 200px; overflow-y: auto;">
91 Click the button to get AI-powered location hints
92 </div>
93 </div>
94
95 <!-- Quick Actions -->
96 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
97 <button id="refresh-data" style="background: rgba(255, 255, 255, 0.25); border: none; color: white; padding: 8px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500;">
98 π Refresh
99 </button>
100 <button id="open-maps" style="background: rgba(255, 255, 255, 0.25); border: none; color: white; padding: 8px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500;">
101 πΊοΈ Open Maps
102 </button>
103 </div>
104 </div>
105 `;
106
107 document.body.appendChild(panel);
108 console.log('Utility panel created');
109
110 // Add event listeners
111 document.getElementById('close-utility-panel').addEventListener('click', togglePanel);
112 document.getElementById('copy-coordinates').addEventListener('click', copyCoordinates);
113 document.getElementById('get-ai-hints').addEventListener('click', getAIHints);
114 document.getElementById('refresh-data').addEventListener('click', refreshData);
115 document.getElementById('open-maps').addEventListener('click', openInGoogleMaps);
116
117 return panel;
118 }
119
120 // Create toggle button
121 function createToggleButton() {
122 const button = document.createElement('button');
123 button.id = 'geoguessr-utility-toggle';
124 button.innerHTML = 'π';
125 button.title = 'Toggle Location Helper';
126 button.style.cssText = `
127 position: fixed;
128 top: 20px;
129 right: 20px;
130 width: 50px;
131 height: 50px;
132 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
133 border: none;
134 border-radius: 50%;
135 color: white;
136 font-size: 24px;
137 cursor: pointer;
138 z-index: 10001;
139 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
140 transition: transform 0.2s, box-shadow 0.2s;
141 `;
142
143 button.addEventListener('mouseenter', () => {
144 button.style.transform = 'scale(1.1)';
145 button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.4)';
146 });
147
148 button.addEventListener('mouseleave', () => {
149 button.style.transform = 'scale(1)';
150 button.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)';
151 });
152
153 button.addEventListener('click', togglePanel);
154 document.body.appendChild(button);
155 console.log('Toggle button created');
156 }
157
158 // Toggle panel visibility
159 function togglePanel() {
160 isPanelVisible = !isPanelVisible;
161 if (utilityPanel) {
162 utilityPanel.style.display = isPanelVisible ? 'block' : 'none';
163 if (isPanelVisible) {
164 refreshData();
165 }
166 }
167 console.log('Panel toggled:', isPanelVisible);
168 }
169
170 // Extract coordinates from Google Street View
171 function extractCoordinates() {
172 try {
173 // Try to get coordinates from the URL
174 const urlParams = new URLSearchParams(window.location.search);
175
176 // Check for Google Maps iframe or embedded content
177 const panoramaContainer = document.querySelector('#panorama-container');
178 if (panoramaContainer) {
179 // Try to extract from Google Maps API
180 const canvas = document.querySelector('.widget-scene-canvas');
181 if (canvas) {
182 // Look for coordinate data in the page
183 const scripts = document.querySelectorAll('script');
184 for (const script of scripts) {
185 const content = script.textContent;
186 // Look for coordinate patterns
187 const latLngMatch = content.match(/[-+]?\d{1,3}\.\d+,\s*[-+]?\d{1,3}\.\d+/);
188 if (latLngMatch) {
189 const [lat, lng] = latLngMatch[0].split(',').map(s => parseFloat(s.trim()));
190 if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
191 currentCoordinates = { lat, lng };
192 console.log('Coordinates extracted:', currentCoordinates);
193 return currentCoordinates;
194 }
195 }
196 }
197 }
198 }
199
200 // Try to get from window object
201 if (window.google && window.google.maps) {
202 console.log('Google Maps API detected');
203 }
204
205 console.log('Could not extract coordinates automatically');
206 return null;
207 } catch (error) {
208 console.error('Error extracting coordinates:', error);
209 return null;
210 }
211 }
212
213 // Update coordinates display
214 function updateCoordinatesDisplay() {
215 const display = document.getElementById('coordinates-display');
216 if (!display) return;
217
218 const coords = extractCoordinates();
219 if (coords) {
220 display.innerHTML = `
221 <div style="margin-bottom: 4px;">Lat: ${coords.lat.toFixed(6)}</div>
222 <div>Lng: ${coords.lng.toFixed(6)}</div>
223 `;
224 } else {
225 display.textContent = 'Coordinates not available in this view';
226 }
227 }
228
229 // Update map analysis info
230 function updateMapAnalysis() {
231 const headingInfo = document.getElementById('heading-info');
232 const zoomInfo = document.getElementById('zoom-info');
233 const povInfo = document.getElementById('pov-info');
234
235 if (!headingInfo || !zoomInfo || !povInfo) return;
236
237 try {
238 // Try to detect Street View orientation
239 const canvas = document.querySelector('.widget-scene-canvas');
240 if (canvas) {
241 headingInfo.textContent = 'Heading: Street View Active';
242 zoomInfo.textContent = 'Zoom: Interactive';
243 povInfo.textContent = 'POV: 360Β° Available';
244 } else {
245 headingInfo.textContent = 'Heading: N/A';
246 zoomInfo.textContent = 'Zoom: N/A';
247 povInfo.textContent = 'POV: N/A';
248 }
249 } catch (error) {
250 console.error('Error updating map analysis:', error);
251 }
252 }
253
254 // Copy coordinates to clipboard
255 async function copyCoordinates() {
256 const coords = currentCoordinates || extractCoordinates();
257 if (coords) {
258 const text = `${coords.lat}, ${coords.lng}`;
259 try {
260 await GM.setClipboard(text);
261 showNotification('β
Coordinates copied to clipboard!');
262 console.log('Coordinates copied:', text);
263 } catch (error) {
264 console.error('Error copying coordinates:', error);
265 showNotification('β Failed to copy coordinates');
266 }
267 } else {
268 showNotification('β οΈ No coordinates available');
269 }
270 }
271
272 // Get AI-powered location hints
273 async function getAIHints() {
274 const button = document.getElementById('get-ai-hints');
275 const display = document.getElementById('ai-hints-display');
276
277 if (!button || !display || isAnalyzing) return;
278
279 isAnalyzing = true;
280 button.disabled = true;
281 button.textContent = 'β³ Analyzing...';
282 display.innerHTML = '<div style="text-align: center; padding: 12px;">π AI is analyzing the scene...</div>';
283
284 try {
285 // Get visible elements and context from the page
286 const visibleText = document.body.innerText.substring(0, 2000);
287 const coords = currentCoordinates || extractCoordinates();
288
289 let prompt = 'You are a GeoGuessr expert. Analyze this game context and provide helpful location hints:\n\n';
290
291 if (coords) {
292 prompt += `Coordinates detected: ${coords.lat}, ${coords.lng}\n`;
293 }
294
295 prompt += `\nProvide 3-5 specific, actionable hints that would help identify the location. Focus on:\n`;
296 prompt += `- Geographic region indicators\n`;
297 prompt += `- Climate and vegetation clues\n`;
298 prompt += `- Architecture and infrastructure\n`;
299 prompt += `- Cultural markers\n`;
300 prompt += `- Language and signage\n\n`;
301 prompt += `Format as a JSON object with a "hints" array of strings.`;
302
303 console.log('Requesting AI hints...');
304
305 const response = await RM.aiCall(prompt, {
306 type: "json_schema",
307 json_schema: {
308 name: "location_hints",
309 schema: {
310 type: "object",
311 properties: {
312 hints: {
313 type: "array",
314 items: { type: "string" },
315 minItems: 3,
316 maxItems: 5
317 },
318 confidence: {
319 type: "string",
320 enum: ["high", "medium", "low"]
321 }
322 },
323 required: ["hints"]
324 }
325 }
326 });
327
328 console.log('AI response received:', response);
329
330 // Display hints
331 let hintsHTML = '<div style="line-height: 1.8;">';
332 response.hints.forEach((hint, index) => {
333 hintsHTML += `<div style="margin-bottom: 8px;">π‘ ${hint}</div>`;
334 });
335 hintsHTML += '</div>';
336
337 if (response.confidence) {
338 hintsHTML += `<div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.2); font-size: 11px; opacity: 0.8;">Confidence: ${response.confidence}</div>`;
339 }
340
341 display.innerHTML = hintsHTML;
342 showNotification('β¨ AI hints generated!');
343
344 } catch (error) {
345 console.error('Error getting AI hints:', error);
346 display.innerHTML = '<div style="color: #ffcccc;">β Failed to generate hints. Please try again.</div>';
347 showNotification('β Failed to get AI hints');
348 } finally {
349 isAnalyzing = false;
350 button.disabled = false;
351 button.textContent = 'β¨ Get AI Hints';
352 }
353 }
354
355 // Refresh all data
356 function refreshData() {
357 console.log('Refreshing data...');
358 updateCoordinatesDisplay();
359 updateMapAnalysis();
360 showNotification('π Data refreshed');
361 }
362
363 // Open current location in Google Maps
364 async function openInGoogleMaps() {
365 const coords = currentCoordinates || extractCoordinates();
366 if (coords) {
367 const url = `https://www.google.com/maps?q=${coords.lat},${coords.lng}`;
368 await GM.openInTab(url, true);
369 showNotification('πΊοΈ Opened in Google Maps');
370 console.log('Opened in Google Maps:', url);
371 } else {
372 showNotification('β οΈ No coordinates available');
373 }
374 }
375
376 // Show notification
377 function showNotification(message) {
378 const notification = document.createElement('div');
379 notification.style.cssText = `
380 position: fixed;
381 top: 20px;
382 left: 50%;
383 transform: translateX(-50%);
384 background: rgba(0, 0, 0, 0.9);
385 color: white;
386 padding: 12px 24px;
387 border-radius: 8px;
388 z-index: 10002;
389 font-size: 14px;
390 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
391 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
392 animation: slideDown 0.3s ease-out;
393 `;
394 notification.textContent = message;
395 document.body.appendChild(notification);
396
397 setTimeout(() => {
398 notification.style.animation = 'slideUp 0.3s ease-out';
399 setTimeout(() => notification.remove(), 300);
400 }, 2500);
401 }
402
403 // Add CSS animations
404 const style = document.createElement('style');
405 style.textContent = `
406 @keyframes slideDown {
407 from {
408 transform: translateX(-50%) translateY(-20px);
409 opacity: 0;
410 }
411 to {
412 transform: translateX(-50%) translateY(0);
413 opacity: 1;
414 }
415 }
416 @keyframes slideUp {
417 from {
418 transform: translateX(-50%) translateY(0);
419 opacity: 1;
420 }
421 to {
422 transform: translateX(-50%) translateY(-20px);
423 opacity: 0;
424 }
425 }
426 #geoguessr-utility-panel::-webkit-scrollbar {
427 width: 6px;
428 }
429 #geoguessr-utility-panel::-webkit-scrollbar-track {
430 background: rgba(255, 255, 255, 0.1);
431 border-radius: 3px;
432 }
433 #geoguessr-utility-panel::-webkit-scrollbar-thumb {
434 background: rgba(255, 255, 255, 0.3);
435 border-radius: 3px;
436 }
437 #geoguessr-utility-panel::-webkit-scrollbar-thumb:hover {
438 background: rgba(255, 255, 255, 0.5);
439 }
440 `;
441 document.head.appendChild(style);
442
443 // Initialize the extension
444 function init() {
445 console.log('Initializing GeoGuessr Location Helper...');
446
447 // Wait for the game to load
448 const checkGameLoaded = setInterval(() => {
449 const panoramaContainer = document.querySelector('#panorama-container');
450 if (panoramaContainer) {
451 clearInterval(checkGameLoaded);
452 console.log('Game loaded, creating UI...');
453
454 createToggleButton();
455 utilityPanel = createUtilityPanel();
456
457 // Set up periodic updates
458 setInterval(() => {
459 if (isPanelVisible) {
460 updateCoordinatesDisplay();
461 updateMapAnalysis();
462 }
463 }, 3000);
464
465 console.log('GeoGuessr Location Helper ready!');
466 }
467 }, 1000);
468
469 // Clear interval after 30 seconds if game doesn't load
470 setTimeout(() => {
471 clearInterval(checkGameLoaded);
472 }, 30000);
473 }
474
475 // Start the extension when DOM is ready
476 if (document.readyState === 'loading') {
477 document.addEventListener('DOMContentLoaded', init);
478 } else {
479 init();
480 }
481
482})();