Extrae coordenadas con clic derecho en el editor de Waze.
// ==UserScript==
// @name WME Coordinate Extractor
// @name:en WME Coordinate Extractor
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Extrae coordenadas con clic derecho en el editor de Waze.
// @description:en Extracts coordinates with a right-click in the Waze editor.
// @author edarague
// @match https://www.waze.com/*editor*
// @run-at document-end
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ─── Estilos ──────────────────────────────────────────────────────────────
function injectStyles() {
const s = document.createElement('style');
s.id = 'wce-styles';
s.textContent = `
.waze-coord-final {
position: fixed !important;
background: #0a0a0a !important;
color: #00ff00 !important;
padding: 14px 18px !important;
border-radius: 6px !important;
font-family: 'Courier New', monospace !important;
font-size: 13px !important;
font-weight: bold !important;
z-index: 999999 !important;
border: 2px solid #00ff00 !important;
min-width: 250px !important;
box-shadow: 0 4px 20px rgba(0,255,0,0.2) !important;
}
.waze-coord-coords {
margin-bottom: 10px !important;
font-size: 14px !important;
}
.waze-coord-actions {
display: flex !important;
gap: 8px !important;
}
.waze-coord-btn {
flex: 1 !important;
padding: 6px 8px !important;
border-radius: 4px !important;
border: 1px solid #00ff00 !important;
background: transparent !important;
color: #00ff00 !important;
font-family: 'Courier New', monospace !important;
font-size: 11px !important;
font-weight: bold !important;
cursor: pointer !important;
text-align: center !important;
transition: background 0.15s !important;
text-decoration: none !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 4px !important;
}
.waze-coord-btn:hover {
background: #00ff00 !important;
color: #0a0a0a !important;
}
`;
document.head.appendChild(s);
}
// ─── Obtención de coordenadas ─────────────────────────────────────────────
// Método primario: API oficial de Waze (W.map / OpenLayers)
function getCoordsFromAPI(e) {
try {
const map = W.map.getOLMap ? W.map.getOLMap() : W.map;
// Convertir posición del evento a píxeles del viewport del mapa
const mapDiv = map.div || map.getDiv();
const rect = mapDiv.getBoundingClientRect();
const px = new OpenLayers.Pixel(
e.clientX - rect.left,
e.clientY - rect.top
);
// getLonLatFromPixel devuelve coordenadas en la proyección nativa del mapa (Mercator)
const mercator = map.getLonLatFromPixel(px);
// Transformar explícitamente desde EPSG:900913 a EPSG:4326
const wgs84 = mercator.clone().transform(
new OpenLayers.Projection('EPSG:900913'),
new OpenLayers.Projection('EPSG:4326')
);
return {
lat: wgs84.lat.toFixed(6),
lng: wgs84.lon.toFixed(6)
};
} catch (_) {
return null;
}
}
// Método de respaldo: regex sobre el texto visible del DOM
function getCoordsFromDOM() {
try {
const m = document.body.innerText.match(/(\d+\.\d{4,})\s+(-?\d+\.\d{4,})/);
return m ? { lat: parseFloat(m[1]).toFixed(6), lng: parseFloat(m[2]).toFixed(6) } : null;
} catch (_) {
return null;
}
}
function getCoords(e) {
return getCoordsFromAPI(e) || getCoordsFromDOM();
}
// ─── Popup ────────────────────────────────────────────────────────────────
function showPopup(c, x, y) {
const old = document.querySelector('.waze-coord-final');
if (old) old.remove();
const mapsUrl = `https://maps.google.com/?q=${c.lat},${c.lng}`;
const p = document.createElement('div');
p.className = 'waze-coord-final';
p.innerHTML = `
<div class="waze-coord-coords">📍 ${c.lat} ${c.lng}</div>
<div class="waze-coord-actions">
<button class="waze-coord-btn" id="wce-copy">📋 Copiar</button>
<a class="waze-coord-btn" href="${mapsUrl}" target="_blank" id="wce-maps">🗺️ Maps</a>
</div>
`;
p.style.left = Math.min(x + 15, window.innerWidth - 240) + 'px';
p.style.top = Math.min(y + 15, window.innerHeight - 120) + 'px';
document.body.appendChild(p);
// Botón Copiar
p.querySelector('#wce-copy').addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(`${c.lat}, ${c.lng}`);
const btn = p.querySelector('#wce-copy');
if (btn) btn.textContent = '✅ Copiado';
removePopup(p);
});
// Botón Maps
p.querySelector('#wce-maps').addEventListener('click', (e) => {
e.stopPropagation();
removePopup(p);
});
// Cierre al hacer clic fuera del popup (sustituye los setTimeout de limpieza)
const outsideClickHandler = (ev) => {
if (!p.contains(ev.target)) {
removePopup(p, outsideClickHandler);
}
};
// Delay mínimo para no capturar el mismo clic derecho que abrió el popup
setTimeout(() => {
document.addEventListener('click', outsideClickHandler, true);
}, 100);
}
function removePopup(p, handler) {
if (p && p.parentNode) p.remove();
if (handler) document.removeEventListener('click', handler, true);
}
// ─── Inicialización ───────────────────────────────────────────────────────
function init() {
injectStyles();
document.addEventListener('contextmenu', (e) => {
const c = getCoords(e);
if (c) {
e.preventDefault();
showPopup(c, e.clientX, e.clientY);
}
}, true);
}
// Esperar al evento wme-ready (estándar SDK moderno).
// Si el evento nunca llega (editor muy viejo), se usa DOMContentLoaded como fallback.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('wme-ready', init, { once: true });
// Fallback: si wme-ready no dispara en 15 s, inicializar de todos modos
setTimeout(() => {
if (!document.getElementById('wce-styles')) init();
}, 15000);
});
} else {
document.addEventListener('wme-ready', init, { once: true });
setTimeout(() => {
if (!document.getElementById('wce-styles')) init();
}, 15000);
}
})();