您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia
当前为
// ==UserScript== // @name WME Places Name Normalizer // @namespace https://gf.qytechs.cn/en/users/mincho77 // @version 3.1 // @description Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia // @author mincho77 // @match https://www.waze.com/*editor* // @match https://beta.waze.com/*user/editor* // @grant GM_xmlhttpRequest // @connect api.languagetool.org // @connect * // @grant unsafeWindow // @license MIT // @run-at document-end // ==/UserScript== /*global W*/ (() => { "use strict"; try { // Insertar estilos globales en el <head> const styles = ` <style> /* Estilos para los botones */ .apply-suggestion-btn { background-color: #4CAF50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background-color 0.3s ease; } .apply-suggestion-btn:hover { background-color: #45a049; } #apply-changes-btn { background-color: #28a745; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; } #apply-changes-btn:hover { background-color: #218838; } #cancel-btn { background-color: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; } #cancel-btn:hover { background-color: #c82333; } </style> `; document.head.insertAdjacentHTML('beforeend', styles); // Capturar todos los eventos de drag & drop a nivel de <body> // Agregar al inicio del script, justo después del "use strict" // Prevenir comportamiento por defecto de drag & drop a nivel global document.addEventListener("dragover", function(e) { const dropZone = document.getElementById("drop-zone"); if (e.target === dropZone || dropZone?.contains(e.target)) { return; // Permitir el drop en la zona designada } e.preventDefault(); e.stopPropagation(); }, { passive: false }); // Agrega { passive: false } document.addEventListener("drop", function(e) { const dropZone = document.getElementById("drop-zone"); if (e.target === dropZone || dropZone?.contains(e.target)) { return; // Permitir el drop en la zona designada } e.preventDefault(); e.stopPropagation(); }, { passive: false }); // Agrega { passive: false } //************************************************************************** //Nombre: evaluarOrtografiaConTildes //Fecha modificación: 2025-04-02 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: objeto con errores detectados //Descripción: // Evalúa palabra por palabra si falta una tilde en las palabras que lo requieren, // según las reglas del español. Primero normaliza el nombre y luego verifica // si las palabras necesitan una tilde. //************************************************************************** function evaluarOrtografiaConTildes(name) { // Si el nombre está vacío, retornar inmediatamente una promesa resuelta if (!name) { return Promise.resolve({ hasSpellingWarning: false, spellingWarnings: [] }); } const palabras = name.trim().split(/\s+/); const spellingWarnings = []; console.log(`[evaluarOrtografiaConTildes] Verificando ortografía de: ${name}`); palabras.forEach((palabra, index) => { // Normalizar la palabra antes de cualquier verificación let normalizada = normalizePlaceNameOnly(palabra); // Ignorar palabras con "&" o que sean emoticonos if (/^[A-Za-z]&[A-Za-z]$/.test(normalizada) || /^[\u263a-\u263c\u2764\u1f600-\u1f64f\u1f680-\u1f6ff]+$/.test(normalizada)) { return; // No verificar ortografía } // Excluir palabras específicas como "y" o "Y" if (normalizada.toLowerCase() === "y" || /^\d+$/.test(normalizada) || normalizada === "-") { return; // Ignorar } // Verificar si la palabra está en la lista de excluidas if (excludeWords.some(w => w.toLowerCase() === normalizada.toLowerCase())) { return; // Ignorar palabra excluida } // Validar que no tenga más de una tilde const cantidadTildes = (normalizada.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) { spellingWarnings.push({ original: palabra, sugerida: null, // No hay sugerencia válida tipo: "Error de tildes", posicion: index }); return; } // Verificar ortografía usando la API de LanguageTool checkSpellingWithAPI(normalizada).then(errores => { errores.forEach(error => { spellingWarnings.push({ original: error.palabra, sugerida: error.sugerencia, tipo: "LanguageTool", posicion: index }); }); }).catch(err => { console.error("Error al verificar ortografía con LanguageTool:", err); }); }); return { hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }; } // Función auxiliar para corregir tildes function corregirTilde(palabra) { // Implementar lógica para corregir tildes según las reglas del español // Por ejemplo: "medellin" → "Medellín" return palabra; // Retornar la palabra corregida } //************************************************************************** //Nombre: toggleSpinner //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // show (boolean) - true para mostrar el spinner, false para ocultarlo // message (string, opcional) - mensaje personalizado a mostrar junto al spinner //Salidas: ninguna (modifica el DOM) //Prerrequisitos: debe existir el estilo CSS del spinner en el documento //Descripción: // Muestra u oculta un indicador visual de carga con un mensaje opcional. // El spinner usa un emoji de reloj de arena (⏳) con animación de rotación // para indicar que el proceso está en curso. //************************************************************************** function toggleSpinner(show, message = 'Revisando ortografía...', progress = null) { let existingSpinner = document.querySelector('.spinner-overlay'); if (existingSpinner) { if (show) { // Actualizar el mensaje y el progreso si el spinner ya existe const spinnerMessage = existingSpinner.querySelector('.spinner-message'); spinnerMessage.innerHTML = ` ${message} ${progress !== null ? `<br><strong>${progress}% completado</strong>` : ''} `; } else { existingSpinner.remove(); // Ocultar el spinner } return; } if (show) { const spinner = document.createElement('div'); spinner.className = 'spinner-overlay'; spinner.innerHTML = ` <div class="spinner-content"> <div class="spinner-icon">⏳</div> <div class="spinner-message"> ${message} ${progress !== null ? `<br><strong>${progress}% completado</strong>` : ''} </div> </div> `; document.body.appendChild(spinner); } } // Agregar los estilos CSS necesarios const spinnerStyles = ` <style> .spinner-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .spinner-content { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } .spinner-icon { font-size: 24px; margin-bottom: 10px; animation: spin 1s linear infinite; /* Aseguramos que la animación esté activa */ display: inline-block; } .spinner-message { color: #333; font-size: 14px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>`; // Insertar los estilos al inicio del documento document.head.insertAdjacentHTML('beforeend', spinnerStyles); if (!Array.prototype.flat) { Array.prototype.flat = function(depth = 1) { return this.reduce(function (flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten); }, []); }; } const SCRIPT_NAME = "PlacesNameNormalizer"; const VERSION = "3.1"; let placesToNormalize = []; let excludeWords = []; let maxPlaces = 50; let normalizeArticles = true; // Expresión regular para detectar siglas (por ejemplo, "S.a.s", "L.T.D.A") const siglaRegex = /^[A-Za-z](\.[A-Za-z])+\.?$/; //************************************************************************** //Nombre: waitForSidebar //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // retries (número, opcional) – Número máximo de intentos de verificación del sidebar (default: 20). // delay (número, opcional) – Tiempo en milisegundos entre intentos (default: 1000ms). //Salidas: // Promesa que se resuelve con el elemento del sidebar si se encuentra, o se rechaza si no se encuentra después de los intentos. //Descripción: // Esta función espera a que el DOM cargue completamente el elemento con ID "sidebar". // Realiza múltiples intentos con intervalos definidos, y resuelve la promesa cuando el sidebar esté disponible. // Es útil para asegurarse de que el entorno de WME esté completamente cargado antes de continuar. //************************************************************************** function waitForSidebar(retries = 20, delay = 1000) { return new Promise((resolve, reject) => { const check = (attempt = 1) => { const sidebar = document.querySelector("#sidebar"); if (sidebar) { console.log("✅ Sidebar disponible."); resolve(sidebar); } else if (attempt <= retries) { console.warn(`⚠️ Sidebar no disponible aún. Reintentando... (${attempt})`); setTimeout(() => check(attempt + 1), delay); } else { reject("❌ Sidebar no disponible después de múltiples intentos."); } }; check(); }); } //************************************************************************** //Nombre: initializeExcludeWords //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos: excludeWords debe estar declarado globalmente. //Descripción: Inicializa la lista de palabras excluidas desde localStorage sin borrar entradas ya existentes. //************************************************************************** function initializeExcludeWords() { const saved = JSON.parse(localStorage.getItem("excludeWords")) || []; // Combinar con las actuales sin duplicar const merged = [...new Set([...saved, ...excludeWords])].sort((a, b) => a.localeCompare(b)); // Solo guardar si hay diferencias const originalString = JSON.stringify(saved.sort()); const newString = JSON.stringify(merged); if (originalString !== newString) { localStorage.setItem("excludeWords", newString); console.log(`[initializeExcludeWords] 💾 excludeWords actualizado con ${merged.length} palabras.`); } else { console.log(`[initializeExcludeWords] 🟢 Sin cambios en excludeWords.`); } // Actualizar variable global excludeWords = merged; } //Modulo de Ortografía // Insertar los estilos al inicio del documento document.head.insertAdjacentHTML('beforeend', spinnerStyles); if (!Array.prototype.flat) { Array.prototype.flat = function(depth = 1) { return this.reduce(function (flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten); }, []); }; } //Modulo de Ortografía //************************************************************************** //Nombre: getUbicacionAcento //Fecha modificación: 2025-03-27 //Autor: mincho77 //Entradas: palabra (string) - Palabra en español a evaluar //Salidas: Posición del acento (última, penúltima, antepenúltima) o null si no aplica //Prerrequisitos si existen: Ninguno //Descripción: Determina la posición silábica del acento en una palabra según la ortografía del español. //************************************************************************** function getUbicacionAcento(palabra) { const original = palabra; const normalized = palabra.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Remove diacritics const silabas = normalized .replace(/[^aeiou]/gi, "") .match(/[aeiou]+/gi); if (!silabas || silabas.length === 0) return null; const conTilde = silabas.findIndex((s, i) => { const originalSyllable = original.slice( normalized.indexOf(s), normalized.indexOf(s) + s.length ); return /[áéíóú]/.test(originalSyllable); }); if (conTilde !== -1) { const posicion = silabas.length - 1 - conTilde; if (posicion === 0) return "última"; if (posicion === 1) return "penúltima"; if (posicion === 2) return "antepenúltima"; return "otras"; } return "sin tilde"; } //************************************************************************** //Nombre: checkSpellingWithAPI //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: text (string) – Texto a evaluar ortográficamente. //Salidas: Promise – Resuelve con lista de errores ortográficos detectados. //Prerrequisitos: Requiere permisos GM_xmlhttpRequest y @connect a api.languagetool.org //Descripción: // Consulta la API de LanguageTool para verificar ortografía del texto. //************************************************************************** function checkSpellingWithAPI(text) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: `language=es&text=${encodeURIComponent(text)}`, onload: function(response) { if (response.status === 200) { const result = JSON.parse(response.responseText); const errores = result.matches .filter(match => match.rule.issueType === "misspelling") .map(match => ({ palabra: match.context.text.substring(match.context.offset, match.context.offset + match.context.length), sugerencia: match.replacements.length > 0 ? match.replacements[0].value : "(sin sugerencia)" })); resolve(errores); } else { reject("❌ Error en respuesta de LanguageTool"); } }, onerror: function(err) { reject("❌ Error de red al contactar LanguageTool"); } }); }); } window.checkSpellingWithAPI = checkSpellingWithAPI; // Combinar la lógica de checkSpelling function checkSpelling(text) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", data: `text=${encodeURIComponent(text)}&language=es`, headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function (response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const errores = data.matches .filter(match => match.rule.issueType === "misspelling") .map(match => ({ palabra: match.context.text.substring(match.context.offset, match.context.offset + match.context.length), sugerencia: match.replacements.length > 0 ? match.replacements[0].value : "(sin sugerencia)" })); resolve(errores); } catch (err) { reject(err); } } else { reject(`Error HTTP: ${response.status}`); } }, onerror: function (err) { reject(err); } }); }); } //************************************************************************** //Nombre: validarWordSpelling //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: palabra (string) - Palabra en español a validar ortográficamente //Salidas: true si cumple reglas ortográficas básicas, false si no //Descripción: // Evalúa si una palabra tiene el uso correcto de tilde o si le falta una tilde // según las reglas del español: esdrújulas siempre con tilde, agudas con tilde // si terminan en n, s o vocal, y llanas con tildse si NO terminan en n, s o vocal. // Se asegura que solo haya una tilde por palabra. //************************************************************************** function validarWordSpelling(palabra) { if (!palabra) return false; // Ignorar siglas con formato X&X if (/^[A-Za-z]&[A-Za-z]$/.test(palabra)) return true; // Si la palabra es un número, no necesita validación if (/^\d+$/.test(palabra)) return true; const tieneTilde = /[áéíóú]/.test(palabra); const cantidadTildes = (palabra.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) return false; // Solo se permite una tilde const silabas = palabra.normalize("NFD").replace(/[^aeiouAEIOU\u0300-\u036f]/g, "").match(/[aeiouáéíóú]+/gi); if (!silabas || silabas.length === 0) return false; const totalSilabas = silabas.length; const ultimaLetra = palabra.slice(-1).toLowerCase(); let tipo = ""; if (totalSilabas >= 3 && /[áéíóú]/.test(palabra)) { tipo = "esdrújula"; } else if (totalSilabas >= 2) { const penultimaSilaba = silabas[totalSilabas - 2]; if (/[áéíóú]/.test(penultimaSilaba)) tipo = "grave"; } if (!tipo) tipo = /[áéíóú]/.test(silabas[totalSilabas - 1]) ? "aguda" : "sin tilde"; if (tipo === "esdrújula") return tieneTilde; if (tipo === "aguda") { return (/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (!/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde); } if (tipo === "grave") { return (!/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde); } return true; } ///************************************************************************** //Nombre: separarSilabas //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: palabra (string) – Palabra en español. //Salidas: array de sílabas (aproximado). //Descripción: // Separa una palabra en sílabas usando reglas heurísticas. // Esta versión simplificada considera diptongos y combinaciones comunes. //************************************************************************** function separarSilabas(palabra) { // Normaliza y quita acentos para facilitar la segmentación const limpia = palabra.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); // Divide por vocales agrupadas como aproximación de sílabas const silabas = limpia.match(/[bcdfghjklmnñpqrstvwxyz]*[aeiou]{1,2}[bcdfghjklmnñpqrstvwxyz]*/g); return silabas || [palabra]; // fallback si no separa nada } //************************************************************************** //Nombre: clasificarPalabra //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: silabas (array) – Arreglo con las sílabas de la palabra. //Salidas: string – 'aguda', 'grave' (llana), 'esdrújula'. //Descripción: // Determina el tipo de palabra según el número de sílabas y la posición // de la tilde (si existe). //************************************************************************** function clasificarPalabra(silabas) { const palabra = silabas.join(""); const tieneTilde = /[áéíóúÁÉÍÓÚ]/.test(palabra); if (tieneTilde) { const posicionTilde = silabas.findIndex(s => /[áéíóúÁÉÍÓÚ]/.test(s)); const posicionDesdeFinal = silabas.length - 1 - posicionTilde; if (posicionDesdeFinal === 0) return "aguda"; if (posicionDesdeFinal === 1) return "grave"; if (posicionDesdeFinal >= 2) return "esdrújula"; } // Si no tiene tilde, asumimos que es: if (silabas.length === 1) return "aguda"; if (silabas.length === 2) return "grave"; return "grave"; // por convención para 3+ sin tilde } //************************************************************************** //Nombre: evaluarOrtografiaNombre //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: objeto con errores detectados //Descripción: Evalúa palabra por palabra si hay errores ortográficos o falta de tildes. // Ya no utiliza sugerencias automáticas para correcciones. //************************************************************************** function evaluarOrtografiaNombre(name) { if (!name) return { hasSpellingWarning: false, spellingWarnings: [] }; const checkOnlyTildes = document.getElementById("checkOnlyTildes")?.checked; const palabras = name.trim().split(/\s+/); const spellingWarnings = []; console.log(`[evaluarOrtografiaNombre] Verificando ortografía de: ${name}`); if (checkOnlyTildes) { palabras.forEach((palabra, index) => { // Verificar si la palabra está en la lista de exclusiones if (excludeWords.some(w => w.toLowerCase() === palabra.toLowerCase()) || /^\d+$/.test(palabra)) { return; // Ignorar palabra excluida } if (!validarWordSpelling(palabra)) { spellingWarnings.push({ original: palabra, sugerida: null, tipo: "Tilde", posicion: index // Guardar la posición en la frase }); } }); return Promise.resolve({ hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }); } else { return checkSpellingWithAPI(name) .then(errores => { errores.forEach(error => { // Verificar si la palabra está en exclusiones if (!excludeWords.some(w => w.toLowerCase() === error.palabra.toLowerCase())) { spellingWarnings.push({ original: error.palabra, sugerida: error.sugerencia, tipo: "LanguageTool", posicion: name.indexOf(error.palabra) }); } }); return { hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }; }); } } //************************************************************************** //Nombre: handleImportList //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: ninguna directa, depende del input file y checkbox en el DOM //Salidas: actualiza excludeWords en localStorage y visualmente //Prerrequisitos: existir un input file con id="importListInput" y checkbox con id="replaceExcludeListCheckbox" //Descripción: Importa una lista de palabras para excluir, opcionalmente reemplazando la lista actual //************************************************************************** function handleImportList() { const fileInput = document.getElementById("importListInput"); const replaceCheckbox = document.getElementById("replaceExcludeListCheckbox"); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { alert("No se seleccionó ningún archivo."); return; } const reader = new FileReader(); reader.onload = function (event) { const rawLines = event.target.result.split(/\r?\n/); const lines = rawLines .map(line => line.replace(/[^\p{L}\p{N}().\s-]/gu, "").trim()) .filter(line => line.length > 0); const eliminadas = rawLines.length - lines.length; if (eliminadas > 0) { console.warn(`[Importar Lista] ⚠️ ${eliminadas} líneas inválidas fueron ignoradas (vacías, caracteres no permitidos o basura).`); } if (replaceCheckbox && replaceCheckbox.checked) { // Si se marcó reemplazar, limpiar todo excludeWords = []; } else { // Si no, recuperar la actual del localStorage excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || excludeWords || []; } // Unificar, eliminar duplicados y ordenar excludeWords = [...new Set([...excludeWords, ...lines])] .filter(w => w.trim().length > 0) .sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); console.log("[handleImportList] Palabras actuales excluidas:", excludeWords); renderExcludedWordsPanel(); // O renderExcludeWordList() si usas otro nombre setupDragAndDropImport(); // Activa drag and drop alert(`✅ Palabras excluidas importadas correctamente: ${excludeWords.length}`); //Limpia el input para permitir recarga posterior fileInput.value = ""; }; reader.readAsText(fileInput.files[0]); } //************************************************************************** //Nombre: setupDragAndDropImport //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: ninguna //Salidas: habilita funcionalidad de arrastrar y soltar archivos al panel //Descripción: // Permite arrastrar y soltar un archivo .txt sobre el panel lateral (#normalizer-sidebar). // Extrae las palabras del archivo y las agrega a excludeWords sin duplicados. //************************************************************************** function setupDragAndDropImport() { const dropArea = document.getElementById("drop-zone"); if (!dropArea) { console.warn("[setupDragAndDropImport] No se encontró la zona de drop"); return; } // Highlight drop zone when dragging over it const highlight = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.add("drag-over"); dropArea.style.backgroundColor = "#e8f5e9"; dropArea.style.borderColor = "#4CAF50"; }; // Remove highlighting const unhighlight = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.remove("drag-over"); dropArea.style.backgroundColor = ""; dropArea.style.borderColor = "#ccc"; }; // Handle the dropped files const handleDrop = (e) => { console.log("[handleDrop] Evento drop detectado"); e.preventDefault(); e.stopPropagation(); unhighlight(e); const file = e.dataTransfer.files[0]; if (!file) { alert("❌ No se detectó ningún archivo"); return; } // Validar extensión del archivo if (!file.name.match(/\.(txt|xml)$/i)) { alert("❌ Por favor, arrastra un archivo .txt o .xml"); return; } console.log(`[handleDrop] Procesando archivo: ${file.name}`); const reader = new FileReader(); reader.onload = (event) => { try { const content = event.target.result; const isXML = file.name.toLowerCase().endsWith('.xml'); let words = []; if (isXML) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, "text/xml"); if (xmlDoc.getElementsByTagName("parsererror").length > 0) { throw new Error("XML inválido"); } words = Array.from(xmlDoc.getElementsByTagName("word")) .map(node => node.textContent.trim()) .filter(w => w.length > 0); } else { words = content.split(/\r?\n/) .map(line => line.trim()) .filter(line => line.length > 0); } if (words.length === 0) { alert("⚠️ No se encontraron palabras válidas en el archivo"); return; } // Actualizar lista de palabras excluidas excludeWords = [...new Set([...excludeWords, ...words])].sort(); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); alert(`✅ Se importaron ${words.length} palabras exitosamente`); console.log(`[handleDrop] Importadas ${words.length} palabras`); } catch (error) { console.error("[handleDrop] Error procesando archivo:", error); alert("❌ Error procesando el archivo"); } }; reader.onerror = () => { console.error("[handleDrop] Error leyendo archivo"); alert("❌ Error leyendo el archivo"); }; reader.readAsText(file); }; // Attach the event listeners dropArea.addEventListener("dragenter", highlight, false); dropArea.addEventListener("dragover", highlight, false); dropArea.addEventListener("dragleave", unhighlight, false); dropArea.addEventListener("drop", handleDrop, false); console.log("[setupDragAndDropImport] Eventos de drag & drop configurados"); } // Función auxiliar para procesar el archivo function handleImportedFile(content, isXML) { let words = []; if (isXML) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, "text/xml"); const nodes = xmlDoc.getElementsByTagName("word"); words = Array.from(nodes).map(node => node.textContent.trim()); } else { words = content.split(/\r?\n/).map(line => line.trim()).filter(line => line); } if (words.length === 0) { alert("No se encontraron palabras válidas en el archivo"); return; } // Actualizar lista de palabras excluidas excludeWords = [...new Set([...excludeWords, ...words])].sort(); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); alert(`✅ Se importaron ${words.length} palabras exitosamente`); } //************************************************************************** //Nombre: renderExcludedWordsPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: ninguna (usa la variable global excludeWords) //Salidas: ninguna (actualiza el DOM y el localStorage) //Prerrequisitos si existen: Debe existir un contenedor con id="normalizer-sidebar" //Descripción: // Esta función limpia y vuelve a renderizar la lista de palabras excluidas // dentro del panel lateral del normalizador. Muestra una lista ordenada // alfabéticamente, evitando palabras vacías. Además, asegura que el localStorage // se actualice correctamente con la lista limpia y depurada. //************************************************************************** function renderExcludedWordsPanel() { const container = document.getElementById("normalizer-sidebar"); if (!container) { console.warn("[renderExcludedWordsPanel] ❌ No se encontró el contenedor 'normalizer-sidebar'"); return; } //Limpiar el contenedor visual container.innerHTML = ""; console.log("[renderExcludedWordsPanel] ✅ Contenedor limpiado."); //Limpiar palabras vacías y ordenar const sortedWords = excludeWords.filter(w => !!w).sort((a, b) => a.localeCompare(b)); console.log(`[renderExcludedWordsPanel] 📋 Lista excluida depurada: ${sortedWords.length} palabras`, sortedWords); const excludeListSection = document.createElement("div"); excludeListSection.style.marginTop = "20px"; excludeListSection.innerHTML = ` <h4 style="margin-bottom: 5px;">Palabras Excluidas</h4> <div style="max-height: 150px; overflow-y: auto; border: 1px solid #ccc; padding: 8px; font-size: 13px; border-radius: 4px;"> <ul style="margin: 0; padding-left: 18px;" id="excludeWordsList"> ${sortedWords.map(w => `<li>${w}</li>`).join("")} </ul> </div> `; container.appendChild(excludeListSection); console.log("[renderExcludedWordsPanel] ✅ Lista renderizada en el DOM."); //Guardar en localStorage localStorage.setItem("excludeWords", JSON.stringify(sortedWords)); console.log("[renderExcludedWordsPanel] 💾 excludeWords actualizado en localStorage."); } //************************************************************************** //Nombre: normalizePlaceName (unsafeWindow) //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - name (string): el nombre original del lugar. //Salidas: // - string: nombre normalizado, respetando exclusiones y opciones del usuario. //Prerrequisitos si existen: // - Debe estar cargada la lista global excludeWords. // - Debe existir un checkbox con id “normalizeArticles” para definir si se normalizan artículos. //Descripción: // Esta versión expuesta globalmente permite acceder a la normalización básica del nombre de un lugar // desde otros contextos como el navegador o Tampermonkey. Capitaliza cada palabra, respeta las excluidas // y no aplica normalización a artículos si el checkbox lo indica. // Realiza limpieza básica: reemplazo de pipes, eliminación de espacios dobles y trim final. //************************************************************************** unsafeWindow.normalizePlaceName = function(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y", "o"]; const words = name.trim().split(/\s+/); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Saltar palabras excluidas if (excludeWords.includes(word)) return word; // Saltar artículos si el checkbox está activo y no es la primera palabra if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) { return lowerWord; } //Mayúsculas return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); name = normalizedWords.join(" "); name = name.replace(/\s*\|\s*/g, " - "); name = name.replace(/\s{2,}/g, " ").trim(); return name; }; //************************************************************************** //Nombre: normalizePlaceNameOnly //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) – Nombre del lugar a normalizar. //Salidas: texto normalizado sin validación ortográfica. //Descripción: // Realiza normalización visual del nombre: capitaliza, ajusta espacios, // formatea guiones, paréntesis, y símbolos. No evalúa ortografía ni acentos. //************************************************************************** function normalizePlaceNameOnly(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y"]; const words = name.trim().split(/\s+/); const isRoman = word => /^(i{1,3}|iv|v|vi{0,3}|ix|x|xi{0,3}|xiv|xv|xvi{0,3}|xix|xx|xxi{0,3}|xxiv|xxv)$/i.test(word); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Si contiene "&", convertir a mayúsculas if (/^[A-Za-z]&[A-Za-z]$/.test(word)) return word.toUpperCase(); // Verificar si está en la lista de excluidas const matchExcluded = excludeWords.find(w => w.toLowerCase() === lowerWord); if (matchExcluded) return matchExcluded; // Si es un número romano, convertir a mayúsculas if (isRoman(word)) return word.toUpperCase(); // Si no se deben normalizar artículos y es un artículo, mantener en minúsculas if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) return lowerWord; // Si es un número o un símbolo como "-", no modificar if (/^\d+$/.test(word) || word === "-") return word; // Verificar ortografía usando la API de LanguageTool return checkSpellingWithAPI(word) .then(errors => { if (errors.length > 0) { const suggestion = errors[0].sugerencia || word; return suggestion.charAt(0).toUpperCase() + suggestion.slice(1).toLowerCase(); } return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .catch(() => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); }); let newName = normalizedWords.join(" ") .replace(/\s*\|\s*/g, " - ") .replace(/([(["'])\s*([\p{L}])/gu, (match, p1, p2) => p1 + p2.toUpperCase()) .replace(/\s*-\s*/g, " - ") .replace(/\b(\d+)([A-Z])\b/g, (match, num, letter) => num + letter.toUpperCase()) .replace(/\.$/, "") .replace(/&(\s*)([A-Z])/g, (match, space, letter) => "&" + space + letter.toUpperCase()); return newName.replace(/\s{2,}/g, " ").trim(); } //************************************************************************** //Nombre: validarOrtografiaConAPI //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: texto (string) - palabra o frase a evaluar //Salidas: Promesa con resultado de corrección ortográfica //Descripción: Consulta la API pública de LanguageTool para identificar errores ortográficos //************************************************************************** function validarOrtografiaConAPI2(texto) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: `text=${encodeURIComponent(texto)}&language=es`, onload: function (response) { if (response.status === 200) { const result = JSON.parse(response.responseText); resolve(result.matches || []); } else { reject("Error en la API de LanguageTool"); } }, onerror: function () { reject("Fallo la solicitud a la API de LanguageTool"); } }); }); } //************************************************************************** //Nombre: importExcludeList //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // file (File) – Archivo cargado por el usuario, que contiene una lista de palabras excluidas. //Salidas: // Ninguna (actualiza el array global excludeWords y el localStorage). //Prerrequisitos: // Debe existir un panel visual para mostrar la lista y una función renderExcludeList(). //Descripción: // Lee un archivo .txt línea por línea, limpia espacios, elimina duplicados y vacíos, // ordena alfabéticamente la lista resultante y actualiza la lista global de palabras // excluidas (excludeWords). Guarda la lista en localStorage y actualiza el panel visual. //************************************************************************** function importExcludeList(file) { const reader = new FileReader(); reader.onload = function (e) { const newWords = e.target.result .split(/\r?\n/) .map(w => w.trim()) .filter(w => w.length > 0); // eliminar vacíos excludeWords = [...new Set(newWords)].sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludeList(); // actualiza el panel visual }; reader.readAsText(file); } //************************************************************************** //Nombre: applySpellCorrection //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // text (string) – Texto que se desea corregir automáticamente con sugerencias ortográficas. //Salidas: // Promise<string> – Texto corregido con las mejores sugerencias aplicadas. //Prerrequisitos: // Debe existir la función checkSpelling que retorna los errores detectados por LanguageTool. //Descripción: // Llama a checkSpelling y aplica la primera sugerencia de reemplazo para cada error ortográfico, // procesando los reemplazos de atrás hacia adelante (para evitar desajustes de índice). // Devuelve el texto corregido como resultado final. //************************************************************************** function applySpellCorrection(text) { return checkSpelling(text).then(data => { let corrected = text; // Ordenar los matches de mayor a menor offset const matches = data.matches.sort((a, b) => b.offset - a.offset); matches.forEach(match => { if (match.replacements && match.replacements.length > 0) { const replacement = match.replacements[0].value; corrected = corrected.substring(0, match.offset) + replacement + corrected.substring(match.offset + match.length); } }); return corrected; }); } //************************************************************************** //Nombre: createSidebarTab //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: Ninguna directa. Usa `W.userscripts` para registrar el tab. //Salidas: Renderiza un nuevo tab lateral personalizado con interfaz del normalizador. //Prerrequisitos: // - `W.userscripts.registerSidebarTab` debe estar disponible. // - `getSidebarHTML()` debe retornar el HTML necesario. //Descripción: // Crea y registra una nueva pestaña en el sidebar de WME con el título // "Places Name Normalizer". Al cargar correctamente, inserta el HTML // generado por `getSidebarHTML()` y espera a que se renderice completamente // para ejecutar los eventos mediante `waitForDOM`. //************************************************************************** function createSidebarTab() { try { // Check if the sidebar system is ready if (!W || !W.userscripts) { console.error(`[${SCRIPT_NAME}] WME not ready for sidebar creation`); return; } // Check for existing tab and clean up if needed const existingTab = document.getElementById("normalizer-tab"); if (existingTab) { console.log(`[${SCRIPT_NAME}] Removing existing tab...`); existingTab.remove(); } // Register new tab with error handling let registration; try { registration = W.userscripts.registerSidebarTab("PlacesNormalizer"); } catch (e) { if (e.message.includes("already been registered")) { console.warn(`[${SCRIPT_NAME}] Tab registration conflict, attempting cleanup...`); // Additional cleanup could go here return; } throw e; } const { tabLabel, tabPane } = registration; if (!tabLabel || !tabPane) { throw new Error("Tab registration failed to return required elements"); } // Configure tab tabLabel.innerHTML = ` <img src="data:image/jpeg;base64,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" style="height: 16px; vertical-align: middle; margin-right: 5px;"> NrmliZer `; tabLabel.title = "Places Name Normalizer"; // Set content and attach events tabPane.innerHTML = getSidebarHTML(); waitForDOM("#normalizer-tab", (element) => { attachEvents(); console.log(`[${SCRIPT_NAME}] Tab created and events attached`); }, 500, 10); } catch (error) { console.error(`[${SCRIPT_NAME}] Error in createSidebarTab:`, error); } } //************************************************************************** //Nombre: attachEvents //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Deben existir en el DOM los elementos con los siguientes IDs: // "normalizeArticles", "maxPlacesInput", "addExcludeWord", "scanPlaces", // "hiddenImportInput", "importExcludeWordsUnifiedBtn" y "exportExcludeWords". // - Debe existir la función handleImportList y la función scanPlaces. // - Debe estar definida la variable global excludeWords y la función renderExcludedWordsPanel. //Descripción: // Esta función adjunta los event listeners necesarios para gestionar la interacción del usuario // con el panel del normalizador de nombres. Se encargan de: // - Actualizar la opción de normalizar artículos al cambiar el estado del checkbox. // - Modificar el número máximo de lugares a procesar a través de un input. // - Exportar la lista de palabras excluidas a un archivo XML. // - Añadir nuevas palabras a la lista de palabras excluidas, evitando duplicados, y actualizar el panel. // - Activar el botón unificado para la importación de palabras excluidas mediante un input oculto. // - Ejecutar la función de escaneo de lugares al hacer clic en el botón correspondiente. //************************************************************************** function attachEvents() { console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`); const normalizeArticlesCheckbox = document.getElementById("normalizeArticles"); const maxPlacesInput = document.getElementById("maxPlacesInput"); const addExcludeWordButton = document.getElementById("addExcludeWord"); const scanPlacesButton = document.getElementById("scanPlaces"); const hiddenInput = document.getElementById("hiddenImportInput"); const importButtonUnified = document.getElementById("importExcludeWordsUnifiedBtn"); // Validación de elementos necesarios if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) { console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`); return; } // ✅ Evento: cambiar estado de "no normalizar artículos" normalizeArticlesCheckbox.addEventListener("change", (e) => { normalizeArticles = e.target.checked; }); // ✅ Evento: cambiar número máximo de places maxPlacesInput.addEventListener("input", (e) => { maxPlaces = parseInt(e.target.value, 10); }); // ✅ Evento: exportar palabras excluidas a XML document.getElementById("exportExcludeWords").addEventListener("click", () => { const savedWords = JSON.parse(localStorage.getItem("excludeWords")) || []; if (savedWords.length === 0) { alert("No hay palabras excluidas para exportar."); return; } const sortedWords = [...savedWords].sort((a, b) => a.localeCompare(b)); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${sortedWords.map(word => ` <word>${word}</word>`).join("\n ")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "excluded_words.xml"; document.body.appendChild(link); // Correctly appends the link link.click(); document.body.removeChild(link); // Correctly removes the link }); // ✅ Evento: añadir palabra excluida sin duplicados addExcludeWordButton.addEventListener("click", () => { const wordInput = document.getElementById("excludeWord") || document.getElementById("excludedWord"); const word = wordInput?.value.trim(); if (!word) return; const lowerWord = word.toLowerCase(); const alreadyExists = excludeWords.some(w => w.toLowerCase() === lowerWord); if (!alreadyExists) { excludeWords.push(word); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Usar renderExcludedWordsPanel en lugar de updateExcludeList } wordInput.value = ""; }); // ✅ Evento: nuevo botón unificado de importación importButtonUnified.addEventListener("click", () => { hiddenInput.click(); // abre el file input oculto }); hiddenInput.addEventListener("change", () => { handleImportList(); // ✅ Llama a la función handleImportList al importar }); // ✅ Evento: escanear lugares scanPlacesButton.addEventListener("click", scanPlaces); } function updateExcludeList() { const list = document.getElementById("excludedWordsList"); if (!list) return; // Ordena una copia del array para no alterar el original const sortedWords = [...excludeWords].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); list.innerHTML = sortedWords.map(word => `<li>${word}</li>`).join(""); } //************************************************************************** //Nombre: scanPlaces //Fecha modificación: 2025-04-02 //Autor: mincho77 //Entradas: ninguna directamente (usa datos del modelo WME y del input #maxPlacesInput) //Salidas: abre panel flotante con lugares que deben ser normalizados //Descripción: // Escanea los lugares visibles en el WME, normaliza los nombres y verifica // si les falta una tilde en las palabras que lo requieren. Si se selecciona // "Revisar solo tildes", utiliza la función evaluarOrtografiaConTildes. //************************************************************************** // ==UserScript== // @name WME Places Name Normalizer // @namespace https://gf.qytechs.cn/en/users/mincho77 // @version 2.1 // @description Normaliza nombres de lugares en Waze Map Editor (WME) por ahora solo con reglas de Colombia // @author mincho77 // @match https://www.waze.com/*editor* // @match https://beta.waze.com/*user/editor* // @grant GM_xmlhttpRequest // @connect api.languagetool.org // @connect * // @grant unsafeWindow // @license MIT // @run-at document-end // @downloadURL https://update.gf.qytechs.cn/scripts/530268/WME%20Places%20Name%20Normalizer.user.js // @updateURL https://update.gf.qytechs.cn/scripts/530268/WME%20Places%20Name%20Normalizer.meta.js // ==/UserScript== /*global W*/ (() => { "use strict"; try { // Insertar estilos globales en el <head> const styles = ` <style> /* Estilos para los botones */ .apply-suggestion-btn { background-color: #4CAF50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background-color 0.3s ease; } .apply-suggestion-btn:hover { background-color: #45a049; } #apply-changes-btn { background-color: #28a745; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; } #apply-changes-btn:hover { background-color: #218838; } #cancel-btn { background-color: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; } #cancel-btn:hover { background-color: #c82333; } </style> `; document.head.insertAdjacentHTML('beforeend', styles); // Capturar todos los eventos de drag & drop a nivel de <body> // Agregar al inicio del script, justo después del "use strict" // Prevenir comportamiento por defecto de drag & drop a nivel global document.addEventListener("dragover", function(e) { const dropZone = document.getElementById("drop-zone"); if (e.target === dropZone || dropZone?.contains(e.target)) { return; // Permitir el drop en la zona designada } e.preventDefault(); e.stopPropagation(); }, { passive: false }); // Agrega { passive: false } document.addEventListener("drop", function(e) { const dropZone = document.getElementById("drop-zone"); if (e.target === dropZone || dropZone?.contains(e.target)) { return; // Permitir el drop en la zona designada } e.preventDefault(); e.stopPropagation(); }, { passive: false }); // Agrega { passive: false } //************************************************************************** //Nombre: evaluarOrtografiaConTildes //Fecha modificación: 2025-04-02 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: objeto con errores detectados //Descripción: // Evalúa palabra por palabra si falta una tilde en las palabras que lo requieren, // según las reglas del español. Primero normaliza el nombre y luego verifica // si las palabras necesitan una tilde. //************************************************************************** function evaluarOrtografiaConTildes(name) { return new Promise((resolve) => { if (!name) { return resolve({ hasSpellingWarning: false, spellingWarnings: [] }); } const palabras = name.trim().split(/\s+/); const spellingWarnings = []; const totalPalabras = palabras.length; let procesadas = 0; // Función para procesar cada palabra secuencialmente function procesarPalabra(index) { if (index >= palabras.length) { // Todas las palabras procesadas toggleSpinner(false); return resolve({ hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }); } const palabra = palabras[index]; let normalizada = normalizePlaceNameOnly(palabra); // Actualizar progreso procesadas++; const progress = Math.round((procesadas / totalPalabras) * 100); toggleSpinner(true, 'Revisando tildes...', progress); // Lógica de verificación existente if (/^[A-Za-z]&[A-Za-z]$/.test(normalizada) || /^[\u263a-\u263c\u2764\u1f600-\u1f64f\u1f680-\u1f6ff]+$/.test(normalizada)) { return procesarPalabra(index + 1); } if (normalizada.toLowerCase() === "y" || /^\d+$/.test(normalizada) || normalizada === "-") { return procesarPalabra(index + 1); } if (excludeWords.some(w => w.toLowerCase() === normalizada.toLowerCase())) { return procesarPalabra(index + 1); } const cantidadTildes = (normalizada.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) { spellingWarnings.push({ original: palabra, sugerida: null, tipo: "Error de tildes", posicion: index }); } // Procesar siguiente palabra procesarPalabra(index + 1); } // Iniciar el procesamiento toggleSpinner(true, 'Revisando tildes...', 0); procesarPalabra(0); }); } // Función auxiliar para corregir tildes function corregirTilde(palabra) { // Implementar lógica para corregir tildes según las reglas del español // Por ejemplo: "medellin" → "Medellín" return palabra; // Retornar la palabra corregida } //************************************************************************** //Nombre: toggleSpinner //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // show (boolean) - true para mostrar el spinner, false para ocultarlo // message (string, opcional) - mensaje personalizado a mostrar junto al spinner //Salidas: ninguna (modifica el DOM) //Prerrequisitos: debe existir el estilo CSS del spinner en el documento //Descripción: // Muestra u oculta un indicador visual de carga con un mensaje opcional. // El spinner usa un emoji de reloj de arena (⏳) con animación de rotación // para indicar que el proceso está en curso. //************************************************************************** function toggleSpinner(show, message = 'Revisando ortografía...', progress = null) { let existingSpinner = document.querySelector('.spinner-overlay'); if (existingSpinner) { if (show) { // Actualizar el mensaje y el progreso si el spinner ya existe const spinnerMessage = existingSpinner.querySelector('.spinner-message'); spinnerMessage.innerHTML = ` ${message} ${progress !== null ? `<br><strong>${progress}% completado</strong>` : ''} `; } else { existingSpinner.remove(); // Ocultar el spinner } return; } if (show) { const spinner = document.createElement('div'); spinner.className = 'spinner-overlay'; spinner.innerHTML = ` <div class="spinner-content"> <div class="spinner-icon">⏳</div> <div class="spinner-message"> ${message} ${progress !== null ? `<br><strong>${progress}% completado</strong>` : ''} </div> </div> `; document.body.appendChild(spinner); } } // Agregar los estilos CSS necesarios const spinnerStyles = ` <style> .spinner-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .spinner-content { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } .spinner-icon { font-size: 24px; margin-bottom: 10px; animation: spin 1s linear infinite; /* Aseguramos que la animación esté activa */ display: inline-block; } .spinner-message { color: #333; font-size: 14px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>`; // Insertar los estilos al inicio del documento document.head.insertAdjacentHTML('beforeend', spinnerStyles); if (!Array.prototype.flat) { Array.prototype.flat = function(depth = 1) { return this.reduce(function (flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten); }, []); }; } const SCRIPT_NAME = "PlacesNameNormalizer"; const VERSION = "2.1"; let placesToNormalize = []; let excludeWords = []; let maxPlaces = 100; let normalizeArticles = true; // Expresión regular para detectar siglas (por ejemplo, "S.a.s", "L.T.D.A") const siglaRegex = /^[A-Za-z](\.[A-Za-z])+\.?$/; //************************************************************************** //Nombre: waitForSidebar //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // retries (número, opcional) – Número máximo de intentos de verificación del sidebar (default: 20). // delay (número, opcional) – Tiempo en milisegundos entre intentos (default: 1000ms). //Salidas: // Promesa que se resuelve con el elemento del sidebar si se encuentra, o se rechaza si no se encuentra después de los intentos. //Descripción: // Esta función espera a que el DOM cargue completamente el elemento con ID "sidebar". // Realiza múltiples intentos con intervalos definidos, y resuelve la promesa cuando el sidebar esté disponible. // Es útil para asegurarse de que el entorno de WME esté completamente cargado antes de continuar. //************************************************************************** function waitForSidebar(retries = 20, delay = 1000) { return new Promise((resolve, reject) => { const check = (attempt = 1) => { const sidebar = document.querySelector("#sidebar"); if (sidebar) { console.log("✅ Sidebar disponible."); resolve(sidebar); } else if (attempt <= retries) { console.warn(`⚠️ Sidebar no disponible aún. Reintentando... (${attempt})`); setTimeout(() => check(attempt + 1), delay); } else { reject("❌ Sidebar no disponible después de múltiples intentos."); } }; check(); }); } //************************************************************************** //Nombre: initializeExcludeWords //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos: excludeWords debe estar declarado globalmente. //Descripción: Inicializa la lista de palabras excluidas desde localStorage sin borrar entradas ya existentes. //************************************************************************** function initializeExcludeWords() { const saved = JSON.parse(localStorage.getItem("excludeWords")) || []; // Combinar con las actuales sin duplicar const merged = [...new Set([...saved, ...excludeWords])].sort((a, b) => a.localeCompare(b)); // Solo guardar si hay diferencias const originalString = JSON.stringify(saved.sort()); const newString = JSON.stringify(merged); if (originalString !== newString) { localStorage.setItem("excludeWords", newString); console.log(`[initializeExcludeWords] 💾 excludeWords actualizado con ${merged.length} palabras.`); } else { console.log(`[initializeExcludeWords] 🟢 Sin cambios en excludeWords.`); } // Actualizar variable global excludeWords = merged; } //Modulo de Ortografía // Insertar los estilos al inicio del documento document.head.insertAdjacentHTML('beforeend', spinnerStyles); if (!Array.prototype.flat) { Array.prototype.flat = function(depth = 1) { return this.reduce(function (flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten); }, []); }; } //Modulo de Ortografía //************************************************************************** //Nombre: getUbicacionAcento //Fecha modificación: 2025-03-27 //Autor: mincho77 //Entradas: palabra (string) - Palabra en español a evaluar //Salidas: Posición del acento (última, penúltima, antepenúltima) o null si no aplica //Prerrequisitos si existen: Ninguno //Descripción: Determina la posición silábica del acento en una palabra según la ortografía del español. //************************************************************************** function getUbicacionAcento(palabra) { const original = palabra; const normalized = palabra.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Remove diacritics const silabas = normalized .replace(/[^aeiou]/gi, "") .match(/[aeiou]+/gi); if (!silabas || silabas.length === 0) return null; const conTilde = silabas.findIndex((s, i) => { const originalSyllable = original.slice( normalized.indexOf(s), normalized.indexOf(s) + s.length ); return /[áéíóú]/.test(originalSyllable); }); if (conTilde !== -1) { const posicion = silabas.length - 1 - conTilde; if (posicion === 0) return "última"; if (posicion === 1) return "penúltima"; if (posicion === 2) return "antepenúltima"; return "otras"; } return "sin tilde"; } //************************************************************************** //Nombre: checkSpellingWithAPI //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: text (string) – Texto a evaluar ortográficamente. //Salidas: Promise – Resuelve con lista de errores ortográficos detectados. //Prerrequisitos: Requiere permisos GM_xmlhttpRequest y @connect a api.languagetool.org //Descripción: // Consulta la API de LanguageTool para verificar ortografía del texto. //************************************************************************** function checkSpellingWithAPI(text) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: `language=es&text=${encodeURIComponent(text)}`, onload: function(response) { if (response.status === 200) { const result = JSON.parse(response.responseText); const errores = result.matches .filter(match => match.rule.issueType === "misspelling") .map(match => ({ palabra: match.context.text.substring(match.context.offset, match.context.offset + match.context.length), sugerencia: match.replacements.length > 0 ? match.replacements[0].value : "(sin sugerencia)" })); resolve(errores); } else { reject("❌ Error en respuesta de LanguageTool"); } }, onerror: function(err) { reject("❌ Error de red al contactar LanguageTool"); } }); }); } window.checkSpellingWithAPI = checkSpellingWithAPI; // Combinar la lógica de checkSpelling function checkSpelling(text) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", data: `text=${encodeURIComponent(text)}&language=es`, headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function (response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const errores = data.matches .filter(match => match.rule.issueType === "misspelling") .map(match => ({ palabra: match.context.text.substring(match.context.offset, match.context.offset + match.context.length), sugerencia: match.replacements.length > 0 ? match.replacements[0].value : "(sin sugerencia)" })); resolve(errores); } catch (err) { reject(err); } } else { reject(`Error HTTP: ${response.status}`); } }, onerror: function (err) { reject(err); } }); }); } //************************************************************************** //Nombre: validarWordSpelling //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: palabra (string) - Palabra en español a validar ortográficamente //Salidas: true si cumple reglas ortográficas básicas, false si no //Descripción: // Evalúa si una palabra tiene el uso correcto de tilde o si le falta una tilde // según las reglas del español: esdrújulas siempre con tilde, agudas con tilde // si terminan en n, s o vocal, y llanas con tildse si NO terminan en n, s o vocal. // Se asegura que solo haya una tilde por palabra. //************************************************************************** function validarWordSpelling(palabra) { if (!palabra) return false; // Ignorar siglas con formato X&X if (/^[A-Za-z]&[A-Za-z]$/.test(palabra)) return true; // Si la palabra es un número, no necesita validación if (/^\d+$/.test(palabra)) return true; const tieneTilde = /[áéíóú]/.test(palabra); const cantidadTildes = (palabra.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) return false; // Solo se permite una tilde const silabas = palabra.normalize("NFD").replace(/[^aeiouAEIOU\u0300-\u036f]/g, "").match(/[aeiouáéíóú]+/gi); if (!silabas || silabas.length === 0) return false; const totalSilabas = silabas.length; const ultimaLetra = palabra.slice(-1).toLowerCase(); let tipo = ""; if (totalSilabas >= 3 && /[áéíóú]/.test(palabra)) { tipo = "esdrújula"; } else if (totalSilabas >= 2) { const penultimaSilaba = silabas[totalSilabas - 2]; if (/[áéíóú]/.test(penultimaSilaba)) tipo = "grave"; } if (!tipo) tipo = /[áéíóú]/.test(silabas[totalSilabas - 1]) ? "aguda" : "sin tilde"; if (tipo === "esdrújula") return tieneTilde; if (tipo === "aguda") { return (/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (!/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde); } if (tipo === "grave") { return (!/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde); } return true; } ///************************************************************************** //Nombre: separarSilabas //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: palabra (string) – Palabra en español. //Salidas: array de sílabas (aproximado). //Descripción: // Separa una palabra en sílabas usando reglas heurísticas. // Esta versión simplificada considera diptongos y combinaciones comunes. //************************************************************************** function separarSilabas(palabra) { // Normaliza y quita acentos para facilitar la segmentación const limpia = palabra.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); // Divide por vocales agrupadas como aproximación de sílabas const silabas = limpia.match(/[bcdfghjklmnñpqrstvwxyz]*[aeiou]{1,2}[bcdfghjklmnñpqrstvwxyz]*/g); return silabas || [palabra]; // fallback si no separa nada } //************************************************************************** //Nombre: clasificarPalabra //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: silabas (array) – Arreglo con las sílabas de la palabra. //Salidas: string – 'aguda', 'grave' (llana), 'esdrújula'. //Descripción: // Determina el tipo de palabra según el número de sílabas y la posición // de la tilde (si existe). //************************************************************************** function clasificarPalabra(silabas) { const palabra = silabas.join(""); const tieneTilde = /[áéíóúÁÉÍÓÚ]/.test(palabra); if (tieneTilde) { const posicionTilde = silabas.findIndex(s => /[áéíóúÁÉÍÓÚ]/.test(s)); const posicionDesdeFinal = silabas.length - 1 - posicionTilde; if (posicionDesdeFinal === 0) return "aguda"; if (posicionDesdeFinal === 1) return "grave"; if (posicionDesdeFinal >= 2) return "esdrújula"; } // Si no tiene tilde, asumimos que es: if (silabas.length === 1) return "aguda"; if (silabas.length === 2) return "grave"; return "grave"; // por convención para 3+ sin tilde } //************************************************************************** //Nombre: evaluarOrtografiaNombre //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: objeto con errores detectados //Descripción: Evalúa palabra por palabra si hay errores ortográficos o falta de tildes. // Ya no utiliza sugerencias automáticas para correcciones. //************************************************************************** function evaluarOrtografiaNombre(name) { if (!name) return { hasSpellingWarning: false, spellingWarnings: [] }; const checkOnlyTildes = document.getElementById("checkOnlyTildes")?.checked; const palabras = name.trim().split(/\s+/); const spellingWarnings = []; console.log(`[evaluarOrtografiaNombre] Verificando ortografía de: ${name}`); if (checkOnlyTildes) { palabras.forEach((palabra, index) => { // Verificar si la palabra está en la lista de exclusiones if (excludeWords.some(w => w.toLowerCase() === palabra.toLowerCase()) || /^\d+$/.test(palabra)) { return; // Ignorar palabra excluida } if (!validarWordSpelling(palabra)) { spellingWarnings.push({ original: palabra, sugerida: null, tipo: "Tilde", posicion: index // Guardar la posición en la frase }); } }); return Promise.resolve({ hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }); } else { return checkSpellingWithAPI(name) .then(errores => { errores.forEach(error => { // Verificar si la palabra está en exclusiones if (!excludeWords.some(w => w.toLowerCase() === error.palabra.toLowerCase())) { spellingWarnings.push({ original: error.palabra, sugerida: error.sugerencia, tipo: "LanguageTool", posicion: name.indexOf(error.palabra) }); } }); return { hasSpellingWarning: spellingWarnings.length > 0, spellingWarnings }; }); } } //************************************************************************** //Nombre: handleImportList //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: ninguna directa, depende del input file y checkbox en el DOM //Salidas: actualiza excludeWords en localStorage y visualmente //Prerrequisitos: existir un input file con id="importListInput" y checkbox con id="replaceExcludeListCheckbox" //Descripción: Importa una lista de palabras para excluir, opcionalmente reemplazando la lista actual //************************************************************************** function handleImportList() { const fileInput = document.getElementById("importListInput"); const replaceCheckbox = document.getElementById("replaceExcludeListCheckbox"); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { alert("No se seleccionó ningún archivo."); return; } const reader = new FileReader(); reader.onload = function (event) { const rawLines = event.target.result.split(/\r?\n/); const lines = rawLines .map(line => line.replace(/[^\p{L}\p{N}().\s-]/gu, "").trim()) .filter(line => line.length > 0); const eliminadas = rawLines.length - lines.length; if (eliminadas > 0) { console.warn(`[Importar Lista] ⚠️ ${eliminadas} líneas inválidas fueron ignoradas (vacías, caracteres no permitidos o basura).`); } if (replaceCheckbox && replaceCheckbox.checked) { // Si se marcó reemplazar, limpiar todo excludeWords = []; } else { // Si no, recuperar la actual del localStorage excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || excludeWords || []; } // Unificar, eliminar duplicados y ordenar excludeWords = [...new Set([...excludeWords, ...lines])] .filter(w => w.trim().length > 0) .sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); console.log("[handleImportList] Palabras actuales excluidas:", excludeWords); renderExcludedWordsPanel(); // O renderExcludeWordList() si usas otro nombre setupDragAndDropImport(); // Activa drag and drop alert(`✅ Palabras excluidas importadas correctamente: ${excludeWords.length}`); //Limpia el input para permitir recarga posterior fileInput.value = ""; }; reader.readAsText(fileInput.files[0]); } //************************************************************************** //Nombre: setupDragAndDropImport //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: ninguna //Salidas: habilita funcionalidad de arrastrar y soltar archivos al panel //Descripción: // Permite arrastrar y soltar un archivo .txt sobre el panel lateral (#normalizer-sidebar). // Extrae las palabras del archivo y las agrega a excludeWords sin duplicados. //************************************************************************** function setupDragAndDropImport() { const dropArea = document.getElementById("drop-zone"); if (!dropArea) { console.warn("[setupDragAndDropImport] No se encontró la zona de drop"); return; } // Highlight drop zone when dragging over it const highlight = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.add("drag-over"); dropArea.style.backgroundColor = "#e8f5e9"; dropArea.style.borderColor = "#4CAF50"; }; // Remove highlighting const unhighlight = (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.remove("drag-over"); dropArea.style.backgroundColor = ""; dropArea.style.borderColor = "#ccc"; }; // Handle the dropped files const handleDrop = (e) => { console.log("[handleDrop] Evento drop detectado"); e.preventDefault(); e.stopPropagation(); unhighlight(e); const file = e.dataTransfer.files[0]; if (!file) { alert("❌ No se detectó ningún archivo"); return; } // Validar extensión del archivo if (!file.name.match(/\.(txt|xml)$/i)) { alert("❌ Por favor, arrastra un archivo .txt o .xml"); return; } console.log(`[handleDrop] Procesando archivo: ${file.name}`); const reader = new FileReader(); reader.onload = (event) => { try { const content = event.target.result; const isXML = file.name.toLowerCase().endsWith('.xml'); let words = []; if (isXML) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, "text/xml"); if (xmlDoc.getElementsByTagName("parsererror").length > 0) { throw new Error("XML inválido"); } words = Array.from(xmlDoc.getElementsByTagName("word")) .map(node => node.textContent.trim()) .filter(w => w.length > 0); } else { words = content.split(/\r?\n/) .map(line => line.trim()) .filter(line => line.length > 0); } if (words.length === 0) { alert("⚠️ No se encontraron palabras válidas en el archivo"); return; } // Actualizar lista de palabras excluidas excludeWords = [...new Set([...excludeWords, ...words])].sort(); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); alert(`✅ Se importaron ${words.length} palabras exitosamente`); console.log(`[handleDrop] Importadas ${words.length} palabras`); } catch (error) { console.error("[handleDrop] Error procesando archivo:", error); alert("❌ Error procesando el archivo"); } }; reader.onerror = () => { console.error("[handleDrop] Error leyendo archivo"); alert("❌ Error leyendo el archivo"); }; reader.readAsText(file); }; // Attach the event listeners dropArea.addEventListener("dragenter", highlight, false); dropArea.addEventListener("dragover", highlight, false); dropArea.addEventListener("dragleave", unhighlight, false); dropArea.addEventListener("drop", handleDrop, false); console.log("[setupDragAndDropImport] Eventos de drag & drop configurados"); } // Función auxiliar para procesar el archivo function handleImportedFile(content, isXML) { let words = []; if (isXML) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, "text/xml"); const nodes = xmlDoc.getElementsByTagName("word"); words = Array.from(nodes).map(node => node.textContent.trim()); } else { words = content.split(/\r?\n/).map(line => line.trim()).filter(line => line); } if (words.length === 0) { alert("No se encontraron palabras válidas en el archivo"); return; } // Actualizar lista de palabras excluidas excludeWords = [...new Set([...excludeWords, ...words])].sort(); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); alert(`✅ Se importaron ${words.length} palabras exitosamente`); } //************************************************************************** //Nombre: renderExcludedWordsPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: ninguna (usa la variable global excludeWords) //Salidas: ninguna (actualiza el DOM y el localStorage) //Prerrequisitos si existen: Debe existir un contenedor con id="normalizer-sidebar" //Descripción: // Esta función limpia y vuelve a renderizar la lista de palabras excluidas // dentro del panel lateral del normalizador. Muestra una lista ordenada // alfabéticamente, evitando palabras vacías. Además, asegura que el localStorage // se actualice correctamente con la lista limpia y depurada. //************************************************************************** function renderExcludedWordsPanel() { const container = document.getElementById("normalizer-sidebar"); if (!container) { console.warn("[renderExcludedWordsPanel] ❌ No se encontró el contenedor 'normalizer-sidebar'"); return; } //Limpiar el contenedor visual container.innerHTML = ""; console.log("[renderExcludedWordsPanel] ✅ Contenedor limpiado."); //Limpiar palabras vacías y ordenar const sortedWords = excludeWords.filter(w => !!w).sort((a, b) => a.localeCompare(b)); console.log(`[renderExcludedWordsPanel] 📋 Lista excluida depurada: ${sortedWords.length} palabras`, sortedWords); const excludeListSection = document.createElement("div"); excludeListSection.style.marginTop = "20px"; excludeListSection.innerHTML = ` <h4 style="margin-bottom: 5px;">Palabras Excluidas</h4> <div style="max-height: 150px; overflow-y: auto; border: 1px solid #ccc; padding: 8px; font-size: 13px; border-radius: 4px;"> <ul style="margin: 0; padding-left: 18px;" id="excludeWordsList"> ${sortedWords.map(w => `<li>${w}</li>`).join("")} </ul> </div> `; container.appendChild(excludeListSection); console.log("[renderExcludedWordsPanel] ✅ Lista renderizada en el DOM."); //Guardar en localStorage localStorage.setItem("excludeWords", JSON.stringify(sortedWords)); console.log("[renderExcludedWordsPanel] 💾 excludeWords actualizado en localStorage."); } //************************************************************************** //Nombre: normalizePlaceName (unsafeWindow) //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - name (string): el nombre original del lugar. //Salidas: // - string: nombre normalizado, respetando exclusiones y opciones del usuario. //Prerrequisitos si existen: // - Debe estar cargada la lista global excludeWords. // - Debe existir un checkbox con id “normalizeArticles” para definir si se normalizan artículos. //Descripción: // Esta versión expuesta globalmente permite acceder a la normalización básica del nombre de un lugar // desde otros contextos como el navegador o Tampermonkey. Capitaliza cada palabra, respeta las excluidas // y no aplica normalización a artículos si el checkbox lo indica. // Realiza limpieza básica: reemplazo de pipes, eliminación de espacios dobles y trim final. //************************************************************************** unsafeWindow.normalizePlaceName = function(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y", "o"]; const words = name.trim().split(/\s+/); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Saltar palabras excluidas if (excludeWords.includes(word)) return word; // Saltar artículos si el checkbox está activo y no es la primera palabra if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) { return lowerWord; } //Mayúsculas return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); name = normalizedWords.join(" "); name = name.replace(/\s*\|\s*/g, " - "); name = name.replace(/\s{2,}/g, " ").trim(); return name; }; //************************************************************************** //Nombre: normalizePlaceNameOnly //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) – Nombre del lugar a normalizar. //Salidas: texto normalizado sin validación ortográfica. //Descripción: // Realiza normalización visual del nombre: capitaliza, ajusta espacios, // formatea guiones, paréntesis, y símbolos. No evalúa ortografía ni acentos. //************************************************************************** function normalizePlaceNameOnly(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y"]; const words = name.trim().split(/\s+/); const isRoman = word => /^(i{1,3}|iv|v|vi{0,3}|ix|x|xi{0,3}|xiv|xv|xvi{0,3}|xix|xx|xxi{0,3}|xxiv|xxv)$/i.test(word); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Si contiene "&", convertir a mayúsculas if (/^[A-Za-z]&[A-Za-z]$/.test(word)) return word.toUpperCase(); // Verificar si está en la lista de excluidas const matchExcluded = excludeWords.find(w => w.toLowerCase() === lowerWord); if (matchExcluded) return matchExcluded; // Si es un número romano, convertir a mayúsculas if (isRoman(word)) return word.toUpperCase(); // Si no se deben normalizar artículos y es un artículo, mantener en minúsculas if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) return lowerWord; // Si es un número o un símbolo como "-", no modificar if (/^\d+$/.test(word) || word === "-") return word; // Verificar ortografía usando la API de LanguageTool return checkSpellingWithAPI(word) .then(errors => { if (errors.length > 0) { const suggestion = errors[0].sugerencia || word; return suggestion.charAt(0).toUpperCase() + suggestion.slice(1).toLowerCase(); } return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .catch(() => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); }); let newName = normalizedWords.join(" ") .replace(/\s*\|\s*/g, " - ") .replace(/([(["'])\s*([\p{L}])/gu, (match, p1, p2) => p1 + p2.toUpperCase()) .replace(/\s*-\s*/g, " - ") .replace(/\b(\d+)([A-Z])\b/g, (match, num, letter) => num + letter.toUpperCase()) .replace(/\.$/, "") .replace(/&(\s*)([A-Z])/g, (match, space, letter) => "&" + space + letter.toUpperCase()); return newName.replace(/\s{2,}/g, " ").trim(); } //************************************************************************** //Nombre: validarOrtografiaConAPI //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: texto (string) - palabra o frase a evaluar //Salidas: Promesa con resultado de corrección ortográfica //Descripción: Consulta la API pública de LanguageTool para identificar errores ortográficos //************************************************************************** function validarOrtografiaConAPI2(texto) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.languagetool.org/v2/check", headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: `text=${encodeURIComponent(texto)}&language=es`, onload: function (response) { if (response.status === 200) { const result = JSON.parse(response.responseText); resolve(result.matches || []); } else { reject("Error en la API de LanguageTool"); } }, onerror: function () { reject("Fallo la solicitud a la API de LanguageTool"); } }); }); } //************************************************************************** //Nombre: importExcludeList //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // file (File) – Archivo cargado por el usuario, que contiene una lista de palabras excluidas. //Salidas: // Ninguna (actualiza el array global excludeWords y el localStorage). //Prerrequisitos: // Debe existir un panel visual para mostrar la lista y una función renderExcludeList(). //Descripción: // Lee un archivo .txt línea por línea, limpia espacios, elimina duplicados y vacíos, // ordena alfabéticamente la lista resultante y actualiza la lista global de palabras // excluidas (excludeWords). Guarda la lista en localStorage y actualiza el panel visual. //************************************************************************** function importExcludeList(file) { const reader = new FileReader(); reader.onload = function (e) { const newWords = e.target.result .split(/\r?\n/) .map(w => w.trim()) .filter(w => w.length > 0); // eliminar vacíos excludeWords = [...new Set(newWords)].sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludeList(); // actualiza el panel visual }; reader.readAsText(file); } //************************************************************************** //Nombre: applySpellCorrection //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // text (string) – Texto que se desea corregir automáticamente con sugerencias ortográficas. //Salidas: // Promise<string> – Texto corregido con las mejores sugerencias aplicadas. //Prerrequisitos: // Debe existir la función checkSpelling que retorna los errores detectados por LanguageTool. //Descripción: // Llama a checkSpelling y aplica la primera sugerencia de reemplazo para cada error ortográfico, // procesando los reemplazos de atrás hacia adelante (para evitar desajustes de índice). // Devuelve el texto corregido como resultado final. //************************************************************************** function applySpellCorrection(text) { return checkSpelling(text).then(data => { let corrected = text; // Ordenar los matches de mayor a menor offset const matches = data.matches.sort((a, b) => b.offset - a.offset); matches.forEach(match => { if (match.replacements && match.replacements.length > 0) { const replacement = match.replacements[0].value; corrected = corrected.substring(0, match.offset) + replacement + corrected.substring(match.offset + match.length); } }); return corrected; }); } //************************************************************************** //Nombre: createSidebarTab //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: Ninguna directa. Usa `W.userscripts` para registrar el tab. //Salidas: Renderiza un nuevo tab lateral personalizado con interfaz del normalizador. //Prerrequisitos: // - `W.userscripts.registerSidebarTab` debe estar disponible. // - `getSidebarHTML()` debe retornar el HTML necesario. //Descripción: // Crea y registra una nueva pestaña en el sidebar de WME con el título // "Places Name Normalizer". Al cargar correctamente, inserta el HTML // generado por `getSidebarHTML()` y espera a que se renderice completamente // para ejecutar los eventos mediante `waitForDOM`. //************************************************************************** function createSidebarTab() { try { // Check if the sidebar system is ready if (!W || !W.userscripts) { console.error(`[${SCRIPT_NAME}] WME not ready for sidebar creation`); return; } // Check for existing tab and clean up if needed const existingTab = document.getElementById("normalizer-tab"); if (existingTab) { console.log(`[${SCRIPT_NAME}] Removing existing tab...`); existingTab.remove(); } // Register new tab with error handling let registration; try { registration = W.userscripts.registerSidebarTab("PlacesNormalizer"); } catch (e) { if (e.message.includes("already been registered")) { console.warn(`[${SCRIPT_NAME}] Tab registration conflict, attempting cleanup...`); // Additional cleanup could go here return; } throw e; } const { tabLabel, tabPane } = registration; if (!tabLabel || !tabPane) { throw new Error("Tab registration failed to return required elements"); } // Configure tab tabLabel.innerHTML = ` <img src="data:image/jpeg;base64,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" style="height: 16px; vertical-align: middle; margin-right: 5px;"> NrmliZer `; tabLabel.title = "Places Name Normalizer"; // Set content and attach events tabPane.innerHTML = getSidebarHTML(); waitForDOM("#normalizer-tab", (element) => { attachEvents(); console.log(`[${SCRIPT_NAME}] Tab created and events attached`); }, 500, 10); } catch (error) { console.error(`[${SCRIPT_NAME}] Error in createSidebarTab:`, error); } } //************************************************************************** //Nombre: attachEvents //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Deben existir en el DOM los elementos con los siguientes IDs: // "normalizeArticles", "maxPlacesInput", "addExcludeWord", "scanPlaces", // "hiddenImportInput", "importExcludeWordsUnifiedBtn" y "exportExcludeWords". // - Debe existir la función handleImportList y la función scanPlaces. // - Debe estar definida la variable global excludeWords y la función renderExcludedWordsPanel. //Descripción: // Esta función adjunta los event listeners necesarios para gestionar la interacción del usuario // con el panel del normalizador de nombres. Se encargan de: // - Actualizar la opción de normalizar artículos al cambiar el estado del checkbox. // - Modificar el número máximo de lugares a procesar a través de un input. // - Exportar la lista de palabras excluidas a un archivo XML. // - Añadir nuevas palabras a la lista de palabras excluidas, evitando duplicados, y actualizar el panel. // - Activar el botón unificado para la importación de palabras excluidas mediante un input oculto. // - Ejecutar la función de escaneo de lugares al hacer clic en el botón correspondiente. //************************************************************************** function attachEvents() { console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`); const normalizeArticlesCheckbox = document.getElementById("normalizeArticles"); const maxPlacesInput = document.getElementById("maxPlacesInput"); const addExcludeWordButton = document.getElementById("addExcludeWord"); const scanPlacesButton = document.getElementById("scanPlaces"); const hiddenInput = document.getElementById("hiddenImportInput"); const importButtonUnified = document.getElementById("importExcludeWordsUnifiedBtn"); // Validación de elementos necesarios if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) { console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`); return; } // ✅ Evento: cambiar estado de "no normalizar artículos" normalizeArticlesCheckbox.addEventListener("change", (e) => { normalizeArticles = e.target.checked; }); // ✅ Evento: cambiar número máximo de places maxPlacesInput.addEventListener("input", (e) => { maxPlaces = parseInt(e.target.value, 10); }); // ✅ Evento: exportar palabras excluidas a XML document.getElementById("exportExcludeWords").addEventListener("click", () => { const savedWords = JSON.parse(localStorage.getItem("excludeWords")) || []; if (savedWords.length === 0) { alert("No hay palabras excluidas para exportar."); return; } const sortedWords = [...savedWords].sort((a, b) => a.localeCompare(b)); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${sortedWords.map(word => ` <word>${word}</word>`).join("\n ")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "excluded_words.xml"; document.body.appendChild(link); // Correctly appends the link link.click(); document.body.removeChild(link); // Correctly removes the link }); // ✅ Evento: añadir palabra excluida sin duplicados addExcludeWordButton.addEventListener("click", () => { const wordInput = document.getElementById("excludeWord") || document.getElementById("excludedWord"); const word = wordInput?.value.trim(); if (!word) return; const lowerWord = word.toLowerCase(); const alreadyExists = excludeWords.some(w => w.toLowerCase() === lowerWord); if (!alreadyExists) { excludeWords.push(word); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Usar renderExcludedWordsPanel en lugar de updateExcludeList } wordInput.value = ""; }); // ✅ Evento: nuevo botón unificado de importación importButtonUnified.addEventListener("click", () => { hiddenInput.click(); // abre el file input oculto }); hiddenInput.addEventListener("change", () => { handleImportList(); // ✅ Llama a la función handleImportList al importar }); // ✅ Evento: escanear lugares scanPlacesButton.addEventListener("click", scanPlaces); } function updateExcludeList() { const list = document.getElementById("excludedWordsList"); if (!list) return; // Ordena una copia del array para no alterar el original const sortedWords = [...excludeWords].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); list.innerHTML = sortedWords.map(word => `<li>${word}</li>`).join(""); } //************************************************************************** //Nombre: scanPlaces //Fecha modificación: 2025-04-02 //Autor: mincho77 //Entradas: ninguna directamente (usa datos del modelo WME y del input #maxPlacesInput) //Salidas: abre panel flotante con lugares que deben ser normalizados //Descripción: // Escanea los lugares visibles en el WME, normaliza los nombres y verifica // si les falta una tilde en las palabras que lo requieren. Si se selecciona // "Revisar solo tildes", utiliza la función evaluarOrtografiaConTildes. //************************************************************************** function scanPlaces() { console.log("scanPlaces ejecutado"); if (!W || !W.model || !W.model.venues || !W.model.venues.objects) { console.error(`[${SCRIPT_NAME}] WME no está listo.`); return; } const allPlaces = Object.values(W.model.venues.objects); console.log(`[${SCRIPT_NAME}] Total de lugares encontrados: ${allPlaces.length}`); const maxPlaces = parseInt(document.getElementById("maxPlacesInput").value, 10) || 20; const checkOnlyTildes = document.getElementById("checkOnlyTildes")?.checked; // Verificar el estado del checkbox const placesToNormalize = allPlaces .filter(p => { let isValid = true; if (!p) { console.log("Lugar descartado: p es null o undefined", p); isValid = false; } else if (typeof p.getID !== "function") { console.log("Lugar descartado: p.getID no es una función", p); isValid = false; } else if (!p.attributes) { console.log("Lugar descartado: p.attributes es null o undefined", p); isValid = false; } else if (typeof p.attributes.name !== "string") { console.log("Lugar descartado: p.attributes.name no es una cadena", p); isValid = false; } else if (!p.attributes.name.trim()) { console.log("Lugar descartado: p.attributes.name está vacío después de trim", p); isValid = false; } return isValid; }) .slice(0, maxPlaces) .map(place => ({ id: place.getID(), name: place.attributes.name, attributes: place.attributes, place })); console.log("placesToNormalize:", placesToNormalize); toggleSpinner(true, 'Revisando ortografía...', 0); // Mostrar spinner al inicio con 0% const totalPlaces = placesToNormalize.length; let processedPlaces = 0; let results = []; // Almacenar resultados parciales function processPlace(index) { if (index >= totalPlaces) { toggleSpinner(false); // Ocultar spinner al terminar if (results.length === 0) { alert("No se encontraron Places que requieran cambio."); } else { openFloatingPanel(results); // Mostrar resultados parciales } return; } const place = placesToNormalize[index]; const originalName = place.name; const normalized = normalizePlaceName(originalName); const ortografia = checkOnlyTildes ? evaluarOrtografiaConTildes(normalized) // Usar la función para revisar solo tildes : evaluarOrtografiaNombre(normalized); Promise.resolve(ortografia) .then(ortografiaResult => { processedPlaces++; const progress = Math.round((processedPlaces / totalPlaces) * 100); toggleSpinner(true, 'Revisando ortografía...', progress); // Actualizar progreso results.push({ id: place.id, originalName, newName: normalized, hasSpellingWarning: ortografiaResult.hasSpellingWarning, spellingWarnings: ortografiaResult.spellingWarnings }); processPlace(index + 1); // Procesar el siguiente lugar }) .catch(error => { console.error("Error durante el escaneo de lugares:", error); alert(`Error durante el escaneo de lugares: ${error}`); toggleSpinner(false); // Ocultar spinner en caso de error openFloatingPanel(results); // Mostrar resultados parciales }); } processPlace(0); // Iniciar el procesamiento desde el primer lugar } //************************************************************************** //Nombre: applyNormalization //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Debe existir un elemento en el DOM con las clases .normalize-checkbox, .delete-checkbox y .new-name-input. // - El objeto global W debe estar disponible, incluyendo W.model.venues, W.model.actionManager y W.controller. // - Deben estar definidos los módulos "Waze/Action/UpdateObject" y "Waze/Action/DeleteObject" (accesibles mediante require()). // - Debe existir la variable placesToNormalize, que contiene datos de los lugares a normalizar, incluyendo sugerencias ortográficas. //Descripción: // Esta función aplica la normalización y/o eliminación de nombres de lugares en el Waze Map Editor // según las selecciones realizadas en el panel flotante. Primero, obtiene los checkboxes seleccionados // para normalización y eliminación. Si no hay ningún elemento seleccionado, se informa y se cancela la operación. // Si se han seleccionado TODOS los checkboxes de eliminación, se solicita una confirmación adicional. // Para cada checkbox de normalización seleccionado, se verifica si se debe aplicar la sugerencia ortográfica // (cuando se ha hecho clic en el botón correspondiente) o el nombre completo modificado, y se actualiza el lugar // mediante la acción de actualización. Posteriormente, se procesan los checkboxes de eliminación aplicando la // acción de eliminación a los lugares correspondientes. Si se realizaron cambios, se marca el modelo como modificado. // Finalmente, se cierra el panel flotante. //************************************************************************** function applyNormalization() { const normalizeCheckboxes = document.querySelectorAll(".normalize-checkbox:checked"); const deleteCheckboxes = document.querySelectorAll(".delete-checkbox:checked"); let changesMade = false; if (normalizeCheckboxes.length === 0 && deleteCheckboxes.length === 0) { alert("No hay lugares seleccionados para normalizar o eliminar."); return; } // Procesar normalización normalizeCheckboxes.forEach(cb => { const index = cb.dataset.index; const input = document.querySelector(`.new-name-input[data-index="${index}"]`); const newName = input?.value?.trim(); const placeId = input?.getAttribute("data-place-id"); const place = W.model.venues.getObjectById(placeId); if (!place || !place.attributes?.name) { console.warn(`No se encontró el lugar con ID: ${placeId}`); return; } const currentName = place.attributes.name.trim(); if (currentName !== newName) { try { const UpdateObject = require("Waze/Action/UpdateObject"); const action = new UpdateObject(place, { name: newName }); W.model.actionManager.add(action); console.log(`Nombre actualizado: "${currentName}" → "${newName}"`); changesMade = true; } catch (error) { console.error("Error aplicando la acción de actualización:", error); } } }); // Procesar eliminación deleteCheckboxes.forEach(cb => { const index = cb.dataset.index; const placeId = document.querySelector(`.new-name-input[data-index="${index}"]`)?.getAttribute("data-place-id"); const place = W.model.venues.getObjectById(placeId); if (!place) { console.warn(`No se encontró el lugar con ID para eliminar: ${placeId}`); return; } try { const DeleteObject = require("Waze/Action/DeleteObject"); const deleteAction = new DeleteObject(place); W.model.actionManager.add(deleteAction); console.log(`Lugar eliminado: ${placeId}`); changesMade = true; } catch (error) { console.error("Error eliminando el lugar:", error); } }); if (changesMade) { alert("Cambios aplicados correctamente."); } else { alert("No se realizaron cambios."); } // Cerrar el panel flotante const panel = document.getElementById("normalizer-floating-panel"); if (panel) panel.remove(); } //************************************************************************** //Nombre: isSimilar //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - a (string): Primera palabra a comparar. // - b (string): Segunda palabra a comparar. //Salidas: // - boolean: Retorna true si las palabras son consideradas similares de forma leve; de lo contrario, retorna false. //Prerrequisitos si existen: Ninguno. //Descripción: // Esta función evalúa la similitud leve entre dos palabras. Primero, verifica si ambas palabras son // idénticas, en cuyo caso retorna true. Luego, comprueba si la diferencia en la cantidad de caracteres // entre ambas es mayor a 2; si es así, retorna false. Posteriormente, compara carácter por carácter // hasta el largo mínimo de las palabras, contando las diferencias. Si el número de discrepancias // excede 2, se considera que las palabras no son similares y retorna false; en caso contrario, retorna true. //************************************************************************** function isSimilar(a, b) { if (a === b) return true; if (Math.abs(a.length - b.length) > 2) return false; let mismatches = 0; for (let i = 0; i < Math.min(a.length, b.length); i++) { if (a[i] !== b[i]) mismatches++; if (mismatches > 2) return false; } return true; } //************************************************************************** //Nombre: normalizePlaceName //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: texto normalizado (string) //Descripción: Normaliza un nombre aplicando capitalización, manejo de artículos, números y paréntesis. //************************************************************************** function normalizePlaceName(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y"]; const words = name.trim().split(/\s+/); const isRoman = word => /^(i{1,3}|iv|v|vi{0,3}|ix|x|xi{0,3}|xiv|xv|xvi{0,3}|xix|xx|xxi{0,3}|xxiv|xxv)$/i.test(word); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Si la palabra es un número, no la analizamos if (/^\d+$/.test(word)) return word; // Si la palabra está en la lista de exclusión, no la modificamos if (excludeWords.some(w => w.toLowerCase() === lowerWord)) return word; // Si es un número romano, lo dejamos en mayúsculas if (isRoman(word)) return word.toUpperCase(); // Si es una sigla con estructura T&T o a&A, convertirla a mayúsculas if (/^[A-Za-z]&[A-Za-z]$/.test(word)) return word.toUpperCase(); // Si es una sigla con apóstrofe como "E's", también la dejamos igual if (/^[A-Z]'[A-Z][a-z]+$/.test(word)) return word; // Si no se deben normalizar artículos y es un artículo, lo dejamos en minúsculas (excepto la primera palabra) if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) return lowerWord; // Si es un número seguido de letras, lo dejamos igual if (/^\d+[A-Z][a-zA-Z]*$/.test(word)) return word; // Si está entre paréntesis y es todo mayúsculas o minúsculas, lo dejamos igual if (/^\(.*\)$/.test(word)) { const inner = word.slice(1, -1); if (inner === inner.toUpperCase() || inner === inner.toLowerCase()) return word; } // Capitalizamos la palabra (primera letra en mayúscula, el resto en minúscula) return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); let newName = normalizedWords.join(" ") .replace(/\s*\|\s*/g, " - ") .replace(/([(["'])\s*([\p{L}])/gu, (match, p1, p2) => p1 + p2.toUpperCase()) .replace(/\s*-\s*/g, " - ") .replace(/\b(\d+)([A-Z])\b/g, (match, num, letter) => num + letter.toUpperCase()) .replace(/\.$/, "") .replace(/&(\s*)([A-Z])/g, (match, space, letter) => "&" + space + letter.toUpperCase()); return newName.replace(/\s{2,}/g, " ").trim(); } // Para exponer al contexto global real desde Tampermonkey unsafeWindow.normalizePlaceName = normalizePlaceName; //************************************************************************** //Nombre: openFloatingPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: placesToNormalize (array de lugares con nombre original y sugerencias) //Salidas: Panel flotante con opciones de normalización y eliminación //Prerrequisitos si existen: Debe haberse definido normalizePlaceName y cargado excludeWords correctamente //Descripción: Crea un panel interactivo donde se presentan los lugares que requieren cambios, // permitiendo su corrección o eliminación. Solo muestra lugares que requieren cambio // y errores ortográficos verdaderos. //************************************************************************** function openFloatingPanel(placesToNormalize) { const panel = document.createElement("div"); panel.id = "normalizer-floating-panel"; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; max-height: 80vh; overflow-y: auto; min-width: 800px; `; let panelContent = ` <style> #normalizer-table { width: 100%; border-collapse: collapse; } #normalizer-table th, #normalizer-table td { padding: 8px; border: 1px solid #ddd; } #normalizer-table tr:nth-child(even) { background-color: #f9f9f9; } .warning-row { background-color: #fff3cd !important; } .close-btn { position: absolute; top: 10px; right: 10px; background: #ccc; color: black; border: none; border-radius: 4px; width: 25px; height: 25px; font-size: 16px; font-weight: bold; cursor: pointer; text-align: center; line-height: 25px; transition: background-color 0.3s ease; } .close-btn:hover { background: #bbb; } .footer-buttons { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } </style> <button class="close-btn" id="close-panel-btn">X</button> <h3>Places to Normalize</h3> <table id="normalizer-table"> <thead> <tr> <th>Normalizar</th> <th>Eliminar</th> <th>Estado</th> <th>Nombre Original</th> <th>Nombre Sugerido</th> <th>Corrección</th> <th>Acción</th> </tr> </thead> <tbody> `; // Procesar cada lugar y sus advertencias placesToNormalize.forEach((place, index) => { const { originalName, newName, spellingWarnings = [] } = place; // Crear una fila por cada advertencia ortográfica spellingWarnings.forEach((warning, warningIndex) => { const suggestionId = `suggestion-${index}-${warningIndex}`; panelContent += ` <tr class="warning-row"> <td style="text-align: center;"> <input type="checkbox" class="normalize-checkbox" data-index="${index}" data-warning-index="${warningIndex}" data-suggestion-id="${suggestionId}"> </td> <td style="text-align: center;"> <input type="checkbox" class="delete-checkbox" data-index="${index}" data-warning-index="${warningIndex}"> </td> <td style="text-align: center;">⚠️</td> <td id="name-cell-${index}-${warningIndex}">${originalName}</td> <td> <input type="text" class="new-name-input" data-index="${index}" data-warning-index="${warningIndex}" data-place-id="${place.id}" data-suggestion-id="${suggestionId}" value="${newName}"> </td> <td>${warning.original} → ${warning.sugerida} (${warning.tipo})</td> <td style="text-align: center;"> <button class="apply-suggestion-btn" data-index="${index}" data-warning-index="${warningIndex}" title="Corregir ortografía de la palabra" style="background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Fix </button> <button class="add-exclude-btn" data-word="${warning.original}" title="Adicionar palabra excluida nueva" style="background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Add </button> </td> </tr>`; }); }); panelContent += ` </tbody> </table> <div class="footer-buttons"> <button class="apply-changes-btn" id="apply-changes-btn">✔️ Apply Changes</button> <button class="cancel-btn" id="cancel-btn">❌ Cancel</button> </div> `; panel.innerHTML = panelContent; document.body.appendChild(panel); // Eventos para los botones document.getElementById('close-panel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); document.getElementById('apply-changes-btn').addEventListener('click', () => { window.applyNormalization(); // Llamar a la función global para aplicar cambios }); document.getElementById('cancel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); // Asignar eventos a los botones "Fix" document.querySelectorAll('.apply-suggestion-btn').forEach(button => { button.addEventListener('click', function () { const index = this.dataset.index; const warningIndex = this.dataset.warningIndex; const input = document.querySelector( `.new-name-input[data-index="${index}"][data-warning-index="${warningIndex}"]` ); const checkbox = document.querySelector( `.normalize-checkbox[data-index="${index}"][data-warning-index="${warningIndex}"]` ); if (input && checkbox) { // Aplicar la corrección ortográfica al campo de entrada const warning = placesToNormalize[index].spellingWarnings[warningIndex]; input.value = input.value.replace(warning.original, warning.sugerida); checkbox.checked = true; // Marcar el checkbox } }); }); // Asignar eventos a los botones "Add Excld Word" document.querySelectorAll('.add-exclude-btn').forEach(button => { button.addEventListener('click', function () { const word = this.dataset.word; // Verificar si es una sigla con formato X&X if (/^[A-Za-z]&[A-Za-z]$/.test(word)) { alert("⚠️ No es necesario adicionar palabras excluidas que tengan '&'."); return; } if (!excludeWords.includes(word)) { excludeWords.push(word); excludeWords.sort((a, b) => a.localeCompare(b)); // Ordenar alfabéticamente localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Actualizar el panel lateral // Mostrar popup en el centro del panel flotante const panel = document.getElementById("normalizer-floating-panel"); const popup = document.createElement('div'); popup.textContent = `✅ The Word "${word}" has been added to the exclusion list.`; popup.style.position = 'absolute'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.backgroundColor = '#4CAF50'; popup.style.color = 'white'; popup.style.padding = '10px 20px'; popup.style.borderRadius = '5px'; popup.style.zIndex = '10000'; popup.style.opacity = '1'; popup.style.transition = 'opacity 1s ease-in-out'; panel.appendChild(popup); setTimeout(() => { popup.style.opacity = '0'; setTimeout(() => panel.removeChild(popup), 1000); }, 2000); // Bloquear el botón y cambiar su texto this.textContent = 'Added'; this.disabled = true; this.style.backgroundColor = '#6c757d'; // Cambiar color a gris this.style.cursor = 'not-allowed'; } else { alert(`⚠️ The word "${word}" is already on the exclusion list.`); } }); }); } //************************************************************************** //Nombre: loadExcludeWordsFromXML //Fecha modificación: //Autor: mincho77 //Entradas: // - callback: función opcional que se ejecuta una vez se cargan y procesan las palabras excluidas. //Salidas: ninguna directa. Actualiza la variable global `excludeWords`. //Prerrequisitos: // - Debe existir un archivo llamado 'excludeWords.xml' accesible por fetch. // - Debe estar definida la variable global `excludeWords`. //Descripción: // Carga un archivo XML que contiene una lista de palabras excluidas. // Combina las palabras nuevas con las que ya están guardadas en localStorage // y actualiza la lista global `excludeWords`. Si el XML no puede ser cargado, // se usa únicamente el contenido almacenado localmente como respaldo. //************************************************************************** function loadExcludeWordsFromXML(callback) { fetch("excludeWords.xml") .then(response => response.text()) // Corrected the syntax here .then(xmlText => { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const wordNodes = xmlDoc.getElementsByTagName("word"); const wordsFromXML = Array.from(wordNodes).map(node => node.textContent.trim()); const existing = JSON.parse(localStorage.getItem("excludeWords")) || []; excludeWords = [...new Set([...existing, ...wordsFromXML])].sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); if (callback) callback(); }) .catch(() => { console.warn("⚠️ No se pudo cargar excludeWords.xml. Solo se usará localStorage."); excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || ["EDS", "IPS", "McDonald's", "EPS"]; localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); if (callback) callback(); }); } function exportExcludeWordsToXML() { const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${excludeWords.map(word => ` <word>${word}</word>`).join("\n")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); // ✅ ESTA LÍNEA ESTABA FALTANDO const link = document.createElement("a"); link.href = url; link.download = "excludeWords.xml"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function showFloatingMessage(message) { const msg = document.createElement("div"); msg.textContent = message; msg.style.position = "fixed"; msg.style.bottom = "30px"; msg.style.left = "50%"; msg.style.transform = "translateX(-50%)"; msg.style.backgroundColor = "#333"; msg.style.color = "#fff"; msg.style.padding = "10px 20px"; msg.style.borderRadius = "5px"; msg.style.zIndex = 9999; msg.style.opacity = "0.95"; msg.style.transition = "opacity 1s ease-in-out"; document.body.appendChild(msg); setTimeout(() => { msg.style.opacity = "0"; setTimeout(() => document.body.removeChild(msg), 1000); }, 3000); } //************************************************************************** //Nombre: waitForWME //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab y renderExcludedWordsPanel. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. // Una vez disponible, inicializa la lista de palabras excluidas desde localStorage, // crea el tab lateral personalizado y renderiza la lista visual de palabras excluidas. // Si WME aún no está listo, vuelve a intentar cada 1000 ms. //************************************************************************** function waitForWME() { if (W && W.userscripts && W.model && W.model.venues) { console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); // ⚠️ Usa solo localStorage createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[waitForWME] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); // Muestra las palabras setupDragAndDropImport(); // Activa drag & drop //Evita que se abra el archivo si cae fuera del área // window.addEventListener("dragover", e => e.preventDefault(), false); // window.addEventListener("drop", e => e.preventDefault(), false); }); } else { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(waitForWME, 1000); } } //************************************************************************** //Nombre: cleanupEventListeners //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Debe existir un elemento en el DOM con el id "normalizer-floating-panel". //Descripción: // Esta función limpia los event listeners asociados al panel flotante de normalización. // Lo hace clonando el nodo del panel y reemplazándolo en el DOM, lo cual elimina todos // los listeners previamente asignados a ese nodo, evitando posibles fugas de memoria // o comportamientos inesperados. //************************************************************************** function cleanupEventListeners() { const panel = document.getElementById("normalizer-floating-panel"); if (panel) { const clone = panel.cloneNode(true); panel.parentNode.replaceChild(clone, panel); } } //************************************************************************** //Nombre: normalizePlaceName (unsafeWindow) //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - name (string): el nombre original del lugar. //Salidas: // - string: nombre normalizado, respetando exclusiones y opciones del usuario. //Prerrequisitos si existen: // - Debe estar cargada la lista global excludeWords. // - Debe existir un checkbox con id “normalizeArticles” para definir si se normalizan artículos. //Descripción: // Esta versión expuesta globalmente permite acceder a la normalización básica del nombre de un lugar // desde otros contextos como el navegador o Tampermonkey. Capitaliza cada palabra, respeta las excluidas // y no aplica normalización a artículos si el checkbox lo indica. // Realiza limpieza básica: reemplazo de pipes, eliminación de espacios dobles y trim final. //************************************************************************** unsafeWindow.normalizePlaceName = function(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y", "o"]; const words = name.trim().split(/\s+/); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Saltar palabras excluidas if (excludeWords.includes(word)) return word; // Saltar artículos si el checkbox está activo y no es la primera palabra if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) { return lowerWord; } //Mayúsculas return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); name = normalizedWords.join(" "); name = name.replace(/\s*\|\s*/g, " - "); name = name.replace(/\s{2,}/g, " ").trim(); return name; }; //************************************************************************** //Nombre: waitForDOM //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // selector (string): Selector CSS del nodo a esperar. // callback (function): Función a ejecutar cuando el nodo esté disponible. // interval (int, opcional): Tiempo entre intentos en milisegundos. Default: 500ms. // maxAttempts (int, opcional): Máximo de intentos antes de abortar. Default: 10. //Salidas: Ejecuta el callback si encuentra el selector dentro del tiempo. //Descripción: // Esta función monitorea el DOM en intervalos constantes hasta que // encuentra un nodo que coincida con el selector. Si lo encuentra, // ejecuta el callback con ese nodo. Si no, muestra un error por consola. //************************************************************************** function waitForDOM(selector, callback, interval = 500, maxAttempts = 10) { let attempts = 0; const checkExist = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(checkExist); callback(element); } else if (attempts >= maxAttempts) { clearInterval(checkExist); console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM después de ${maxAttempts} intentos.`); } attempts++; }, interval); } //************************************************************************** //Nombre: getSidebarHTML //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna. //Salidas: Retorna un string con HTML que define el contenido del panel lateral del script. //Prerrequisitos si existen: Debe estar disponible el valor de las variables globales: // - normalizeArticles (boolean): Define si los artículos deben ser normalizados o no. // - maxPlaces (number): Número máximo de lugares a escanear. //Descripción: // Esta función construye el HTML que se inyecta en el panel lateral (sidebar) de WME. // Incluye controles para: // - Activar o desactivar normalización de artículos ("el", "la", etc). // - Definir la cantidad máxima de lugares a procesar. // - Agregar palabras excluidas manualmente. // - Exportar palabras excluidas a un archivo XML. // - Importar una lista de palabras excluidas desde archivo XML. // - Disparar el escaneo de lugares para normalización. // La lista de palabras excluidas **no se renderiza aquí directamente**, sino en el div // con id "normalizer-sidebar" para permitir que sea manejada dinámicamente. //************************************************************************** function getSidebarHTML() { return ` <div id="normalizer-tab"> <h4>Places Name Normalizer <span style="font-size:11px;">v${VERSION}</span></h4> <div> <input type="checkbox" id="normalizeArticles" ${normalizeArticles ? "checked" : ""}> <label for="normalizeArticles">No Normalizar artículos (el, la, los, ...)</label> </div> <div> <label>Máximo de Places a buscar: </label> <input type='number' id='maxPlacesInput' value='${maxPlaces}' min='1' max='800' style='width: 60px;'> </div> <div> <label>Palabras Excluidas:</label> <input type='text' id='excludeWord' style='width: 120px;'> <button id='addExcludeWord'>Add Word</button> <div id="normalizer-sidebar" style="margin-top: 20px;"></div> <button id="exportExcludeWords" style="margin-top: 5px;">Export Words</button> <br> <button id="importExcludeWordsUnifiedBtn" style="margin-top: 5px;">Import List...</button> <input type="file" id="hiddenImportInput" accept=".xml" style="display: none;"> </div> <div> <input type="checkbox" id="checkOnlyTildes" checked> <label for="checkOnlyTildes">Revisar solo tildes</label> </div> <div id="drop-zone" style="border: 2px dashed #ccc; border-radius: 6px; padding: 15px; margin: 15px 0; text-align: center; font-style: italic; color: #555; background-color: #f8f9fa; transition: all 0.3s ease;"> 📂 Arrastra aquí tu archivo .txt o .xml para importar palabras excluidas </div> <hr> <button id="scanPlaces">Scan...</button> </div> `; } //************************************************************************** //Nombre: attachEvents //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Deben existir en el DOM los elementos con los siguientes IDs: // "normalizeArticles", "maxPlacesInput", "addExcludeWord", "scanPlaces", // "hiddenImportInput", "importExcludeWordsUnifiedBtn" y "exportExcludeWords". // - Debe existir la función handleImportList y la función scanPlaces. // - Debe estar definida la variable global excludeWords y la función renderExcludedWordsPanel. //Descripción: // Esta función adjunta los event listeners necesarios para gestionar la interacción del usuario // con el panel del normalizador de nombres. Se encargan de: // - Actualizar la opción de normalizar artículos al cambiar el estado del checkbox. // - Modificar el número máximo de lugares a procesar a través de un input. // - Exportar la lista de palabras excluidas a un archivo XML. // - Añadir nuevas palabras a la lista de palabras excluidas, evitando duplicados, y actualizar el panel. // - Activar el botón unificado para la importación de palabras excluidas mediante un input oculto. // - Ejecutar la función de escaneo de lugares al hacer clic en el botón correspondiente. //************************************************************************** function attachEvents2() { console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`); const normalizeArticlesCheckbox = document.getElementById("normalizeArticles"); const maxPlacesInput = document.getElementById("maxPlacesInput"); const addExcludeWordButton = document.getElementById("addExcludeWord"); const scanPlacesButton = document.getElementById("scanPlaces"); const hiddenInput = document.getElementById("hiddenImportInput"); const importButtonUnified = document.getElementById("importExcludeWordsUnifiedBtn"); // Validación de elementos necesarios if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) { console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`); return; } // ✅ Evento: cambiar estado de "no normalizar artículos" normalizeArticlesCheckbox.addEventListener("change", (e) => { normalizeArticles = e.target.checked; }); // ✅ Evento: cambiar número máximo de places maxPlacesInput.addEventListener("input", (e) => { maxPlaces = parseInt(e.target.value, 10); }); // ✅ Evento: exportar palabras excluidas a XML document.getElementById("exportExcludeWords").addEventListener("click", () => { const savedWords = JSON.parse(localStorage.getItem("excludeWords")) || []; if (savedWords.length === 0) { alert("No hay palabras excluidas para exportar."); return; } const sortedWords = [...savedWords].sort((a, b) => a.localeCompare(b)); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${sortedWords.map(word => ` <word>${word}</word>`).join("\n ")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "excluded_words.xml"; document.body.appendChild(link); // Correctly appends the link link.click(); document.body.removeChild(link); // Correctly removes the link }); // ✅ Evento: añadir palabra excluida sin duplicados addExcludeWordButton.addEventListener("click", () => { const wordInput = document.getElementById("excludeWord") || document.getElementById("excludedWord"); const word = wordInput?.value.trim(); if (!word) return; const lowerWord = word.toLowerCase(); const alreadyExists = excludeWords.some(w => w.toLowerCase() === lowerWord); if (!alreadyExists) { excludeWords.push(word); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Usar renderExcludedWordsPanel en lugar de updateExcludeList } wordInput.value = ""; }); // ✅ Evento: nuevo botón unificado de importación importButtonUnified.addEventListener("click", () => { hiddenInput.click(); // abre el file input oculto }); hiddenInput.addEventListener("change", () => { handleImportList(); // ✅ Llama a la función handleImportList al importar }); // ✅ Evento: escanear lugares scanPlacesButton.addEventListener("click", scanPlaces); } //************************************************************************** //Nombre: NameChangeAction //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - venue (object): Objeto Place que contiene la información del lugar a modificar. // - oldName (string): Nombre actual del lugar. // - newName (string): Nuevo nombre sugerido para el lugar. //Salidas: Ninguna (función constructora que crea un objeto de acción). //Prerrequisitos si existen: // - El objeto venue debe contar con la propiedad attributes, y dentro de ésta, con el campo id. //Descripción: // Esta función constructora crea un objeto que representa la acción de cambio de nombre // de un Place en el Waze Map Editor (WME). Asigna las propiedades correspondientes: // - Guarda el objeto venue, el nombre original (oldName) y el nuevo nombre (newName). // - Extrae y guarda el ID único del lugar desde venue.attributes.id. // - Establece metadatos para identificar la acción, asignando el tipo "NameChangeAction" // y marcando isGeometryEdit como false para indicar que no se trata de una edición de geometría. // Estos metadatos pueden ser utilizados por WME y otros plugins para gestionar y mostrar la acción. //************************************************************************** function NameChangeAction(venue, oldName, newName) { // Referencia al Place y los nombres this.venue = venue; this.oldName = oldName; this.newName = newName; // ID único del Place this.venueId = venue.attributes.id; // Metadatos que WME/Plugins pueden usar this.type = "NameChangeAction"; this.isGeometryEdit = false; // no es una edición de geometría } /** * 1) getActionName: nombre de la acción en el historial. */ NameChangeAction.prototype.getActionName = function() { return "Update place name"; }; /** 2) getActionText: texto corto que WME a veces muestra. */ NameChangeAction.prototype.getActionText = function() { return "Update place name"; }; /** 3) getName: algunas versiones llaman a getName(). */ NameChangeAction.prototype.getName = function() { return "Update place name"; }; /** 4) getDescription: descripción detallada de la acción. */ NameChangeAction.prototype.getDescription = function() { return `Place name changed from "${this.oldName}" to "${this.newName}".`; }; /** 5) getT: título (a veces requerido por plugins). */ NameChangeAction.prototype.getT = function() { return "Update place name"; }; /** 6) getID: si un plugin llama a e.getID(). */ NameChangeAction.prototype.getID = function() { return `NameChangeAction-${this.venueId}`; }; /** 7) doAction: asigna el nuevo nombre (WME llama a esto al crear la acción). */ NameChangeAction.prototype.doAction = function() { this.venue.attributes.name = this.newName; this.venue.isDirty = true; if (typeof W.model.venues.markObjectEdited === "function") { W.model.venues.markObjectEdited(this.venue); } }; /** 8) undoAction: revertir al nombre anterior (Ctrl+Z). */ NameChangeAction.prototype.undoAction = function() { this.venue.attributes.name = this.oldName; this.venue.isDirty = true; if (typeof W.model.venues.markObjectEdited === "function") { W.model.venues.markObjectEdited(this.venue); } }; /** 9) redoAction: rehacer (Ctrl+Shift+Z), vuelve a doAction. */ NameChangeAction.prototype.redoAction = function() { return this.doAction(); }; /** 10) undoSupported / redoSupported: indica si se puede des/rehacer. */ NameChangeAction.prototype.undoSupported = function() { return true; }; NameChangeAction.prototype.redoSupported = function() { return true; }; /** 11) accept / supersede: evita fusionar con otras acciones. */ NameChangeAction.prototype.accept = function() { return false; }; NameChangeAction.prototype.supersede = function() { return false; }; /** 12) isEditAction: true => habilita "Guardar". */ NameChangeAction.prototype.isEditAction = function() { return true; }; /** 13) getAffectedUniqueIds: objetos que se alteran. */ NameChangeAction.prototype.getAffectedUniqueIds = function() { return [this.venueId]; }; /** 14) isSerializable: si no implementas serialize(), pon false. */ NameChangeAction.prototype.isSerializable = function() { return false; }; /** 15) isActionStackable: false => no combina con otras ediciones. */ NameChangeAction.prototype.isActionStackable = function() { return false; }; /** 16) getFocusFeatures: WME/Plugins pueden usarlo para "enfocar" el objeto. */ NameChangeAction.prototype.getFocusFeatures = function() { // Devolvemos el venue para indicar que ese es el foco (o un array vacío si prefieres). return [this.venue]; }; /** 17) Métodos vacíos para evitar futuros "no es una función" si WME pide estos. */ NameChangeAction.prototype.getFocusSegments = function() { return []; }; NameChangeAction.prototype.getFocusNodes = function() { return []; }; NameChangeAction.prototype.getFocusClosures = function() { return []; }; /** 18) getTimestamp: método nuevo que WME/Plugins están llamando. */ NameChangeAction.prototype.getTimestamp = function() { // Devolvemos un timestamp numérico (ms desde época UNIX). return Date.now(); }; //************************************************************************** //Nombre: openFloatingPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: placesToNormalize (array de lugares con nombre original y sugerencias) //Salidas: Panel flotante con opciones de normalización y eliminación //Prerrequisitos si existen: Debe haberse definido normalizePlaceName y cargado excludeWords correctamente //Descripción: Crea un panel interactivo donde se presentan los lugares que requieren cambios, // permitiendo su corrección o eliminación. Solo muestra lugares que requieren cambio // y errores ortográficos verdaderos. //************************************************************************** function openFloatingPanel2(placesToNormalize) { const panel = document.createElement("div"); panel.id = "normalizer-floating-panel"; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; max-height: 80vh; overflow-y: auto; min-width: 800px; `; let panelContent = ` <style> #normalizer-table { width: 100%; border-collapse: collapse; } #normalizer-table th, #normalizer-table td { padding: 8px; border: 1px solid #ddd; } #normalizer-table tr:nth-child(even) { background-color: #f9f9f9; } .warning-row { background-color: #fff3cd !important; } .close-btn { position: absolute; top: 10px; right: 10px; background: #ccc; color: black; border: none; border-radius: 4px; width: 25px; height: 25px; font-size: 16px; font-weight: bold; cursor: pointer; text-align: center; line-height: 25px; transition: background-color 0.3s ease; } .close-btn:hover { background: #bbb; } .footer-buttons { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } </style> <button class="close-btn" id="close-panel-btn">X</button> <h3>Places to Normalize</h3> <table id="normalizer-table"> <thead> <tr> <th>Normalizar</th> <th>Eliminar</th> <th>Estado</th> <th>Nombre Original</th> <th>Nombre Sugerido</th> <th>Corrección</th> <th>Acción</th> </tr> </thead> <tbody> `; // Procesar cada lugar y sus advertencias placesToNormalize.forEach((place, index) => { const { originalName, newName, spellingWarnings = [] } = place; // Crear una fila por cada advertencia ortográfica spellingWarnings.forEach((warning, warningIndex) => { const suggestionId = `suggestion-${index}-${warningIndex}`; panelContent += ` <tr class="warning-row"> <td style="text-align: center;"> <input type="checkbox" class="normalize-checkbox" data-index="${index}" data-warning-index="${warningIndex}" data-suggestion-id="${suggestionId}"> </td> <td style="text-align: center;"> <input type="checkbox" class="delete-checkbox" data-index="${index}" data-warning-index="${warningIndex}"> </td> <td style="text-align: center;">⚠️</td> <td id="name-cell-${index}-${warningIndex}">${originalName}</td> <td> <input type="text" class="new-name-input" data-index="${index}" data-warning-index="${warningIndex}" data-place-id="${place.id}" data-suggestion-id="${suggestionId}" value="${newName}"> </td> <td>${warning.original} → ${warning.sugerida} (${warning.tipo})</td> <td style="text-align: center;"> <button class="apply-suggestion-btn" data-index="${index}" data-warning-index="${warningIndex}" title="Corregir ortografía de la palabra" style="background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Fix </button> <button class="add-exclude-btn" data-word="${warning.original}" title="Adicionar palabra excluida nueva" style="background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Add Excld </button> </td> </tr>`; }); }); panelContent += ` </tbody> </table> <div class="footer-buttons"> <button class="apply-changes-btn" id="apply-changes-btn">✔️ Apply Changes</button> <button class="cancel-btn" id="cancel-btn">❌ Cancel</button> </div> `; panel.innerHTML = panelContent; document.body.appendChild(panel); // Eventos para los botones document.getElementById('close-panel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); document.getElementById('apply-changes-btn').addEventListener('click', () => { window.applyNormalization(); // Llamar a la función global para aplicar cambios }); document.getElementById('cancel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); // Asignar eventos a los botones "Fix" document.querySelectorAll('.apply-suggestion-btn').forEach(button => { button.addEventListener('click', function () { const index = this.dataset.index; const warningIndex = this.dataset.warningIndex; const input = document.querySelector( `.new-name-input[data-index="${index}"][data-warning-index="${warningIndex}"]` ); const checkbox = document.querySelector( `.normalize-checkbox[data-index="${index}"][data-warning-index="${warningIndex}"]` ); if (input && checkbox) { const warning = placesToNormalize[index].spellingWarnings[warningIndex]; input.value = input.value.replace(warning.original, warning.sugerida || warning.original); checkbox.checked = true; // Marcar el checkbox } }); }); // Asignar eventos a los botones "Add Excld Word" document.querySelectorAll('.add-exclude-btn').forEach(button => { button.addEventListener('click', function () { const word = this.dataset.word; if (!excludeWords.includes(word)) { excludeWords.push(word); excludeWords.sort((a, b) => a.localeCompare(b)); // Ordenar alfabéticamente localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Actualizar el panel lateral // Mostrar popup en el centro del panel flotante const panel = document.getElementById("normalizer-floating-panel"); const popup = document.createElement('div'); popup.textContent = `✅ The Word "${word}" has been added to the exclusion list.`; popup.style.position = 'absolute'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.backgroundColor = '#4CAF50'; popup.style.color = 'white'; popup.style.padding = '10px 20px'; popup.style.borderRadius = '5px'; popup.style.zIndex = '10000'; popup.style.opacity = '1'; popup.style.transition = 'opacity 1s ease-in-out'; panel.appendChild(popup); setTimeout(() => { popup.style.opacity = '0'; setTimeout(() => panel.removeChild(popup), 1000); }, 2000); // Bloquear el botón y cambiar su texto this.textContent = 'Excluded Word Added'; this.disabled = true; this.style.backgroundColor = '#6c757d'; // Cambiar color a gris this.style.cursor = 'not-allowed'; } else { alert(`⚠️ The word "${word}" is already on the exclusion list.`); } }); }); } //************************************************************************** //Nombre: waitForWME //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab y renderExcludedWordsPanel. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. // Una vez disponible, inicializa la lista de palabras excluidas desde localStorage, // crea el tab lateral personalizado y renderiza la lista visual de palabras excluidas. // Si WME aún no está listo, vuelve a intentar cada 1000 ms. //************************************************************************** function waitForWME2() { if (W && W.userscripts && W.model && W.model.venues) { console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); // ⚠️ Usa solo localStorage createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[waitForWME] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); // Muestra las palabras setupDragAndDropImport(); // Activa drag & drop //Evita que se abra el archivo si cae fuera del área // window.addEventListener("dragover", e => e.preventDefault(), false); // window.addEventListener("drop", e => e.preventDefault(), false); }); } else { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(waitForWME, 1000); } } //************************************************************************** //Nombre: init //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab, waitForDOM, renderExcludedWordsPanel y setupDragAndDropImport. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. Si WME aún no está listo, // reintenta la inicialización cada 1000 ms. Una vez disponible, inicializa la lista de palabras // excluidas, crea el tab lateral personalizado, espera a que el DOM del tab esté listo para // renderizar el contenido (palabras excluidas y funcionalidad drag & drop), y expone globalmente // las funciones applyNormalization y normalizePlaceName para uso externo. //************************************************************************** function init() { if (!W || !W.userscripts || !W.model || !W.model.venues) { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(init, 1000); return; } console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[init] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); setupDragAndDropImport(); }); window.applyNormalization = applyNormalization; window.normalizePlaceName = normalizePlaceName; } init(); } catch (e) { console.error('[PlacesNameNormalizer] Fatal initialization error:', e); } })(); //************************************************************************** //Nombre: applyNormalization //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Debe existir un elemento en el DOM con las clases .normalize-checkbox, .delete-checkbox y .new-name-input. // - El objeto global W debe estar disponible, incluyendo W.model.venues, W.model.actionManager y W.controller. // - Deben estar definidos los módulos "Waze/Action/UpdateObject" y "Waze/Action/DeleteObject" (accesibles mediante require()). // - Debe existir la variable placesToNormalize, que contiene datos de los lugares a normalizar, incluyendo sugerencias ortográficas. //Descripción: // Esta función aplica la normalización y/o eliminación de nombres de lugares en el Waze Map Editor // según las selecciones realizadas en el panel flotante. Primero, obtiene los checkboxes seleccionados // para normalización y eliminación. Si no hay ningún elemento seleccionado, se informa y se cancela la operación. // Si se han seleccionado TODOS los checkboxes de eliminación, se solicita una confirmación adicional. // Para cada checkbox de normalización seleccionado, se verifica si se debe aplicar la sugerencia ortográfica // (cuando se ha hecho clic en el botón correspondiente) o el nombre completo modificado, y se actualiza el lugar // mediante la acción de actualización. Posteriormente, se procesan los checkboxes de eliminación aplicando la // acción de eliminación a los lugares correspondientes. Si se realizaron cambios, se marca el modelo como modificado. // Finalmente, se cierra el panel flotante. //************************************************************************** function applyNormalization() { const normalizeCheckboxes = document.querySelectorAll(".normalize-checkbox:checked"); const deleteCheckboxes = document.querySelectorAll(".delete-checkbox:checked"); let changesMade = false; if (normalizeCheckboxes.length === 0 && deleteCheckboxes.length === 0) { alert("No hay lugares seleccionados para normalizar o eliminar."); return; } // Procesar normalización normalizeCheckboxes.forEach(cb => { const index = cb.dataset.index; const input = document.querySelector(`.new-name-input[data-index="${index}"]`); const newName = input?.value?.trim(); const placeId = input?.getAttribute("data-place-id"); const place = W.model.venues.getObjectById(placeId); if (!place || !place.attributes?.name) { console.warn(`No se encontró el lugar con ID: ${placeId}`); return; } const currentName = place.attributes.name.trim(); if (currentName !== newName) { try { const UpdateObject = require("Waze/Action/UpdateObject"); const action = new UpdateObject(place, { name: newName }); W.model.actionManager.add(action); console.log(`Nombre actualizado: "${currentName}" → "${newName}"`); changesMade = true; } catch (error) { console.error("Error aplicando la acción de actualización:", error); } } }); // Procesar eliminación deleteCheckboxes.forEach(cb => { const index = cb.dataset.index; const placeId = document.querySelector(`.new-name-input[data-index="${index}"]`)?.getAttribute("data-place-id"); const place = W.model.venues.getObjectById(placeId); if (!place) { console.warn(`No se encontró el lugar con ID para eliminar: ${placeId}`); return; } try { const DeleteObject = require("Waze/Action/DeleteObject"); const deleteAction = new DeleteObject(place); W.model.actionManager.add(deleteAction); console.log(`Lugar eliminado: ${placeId}`); changesMade = true; } catch (error) { console.error("Error eliminando el lugar:", error); } }); if (changesMade) { alert("Cambios aplicados correctamente."); } else { alert("No se realizaron cambios."); } // Cerrar el panel flotante const panel = document.getElementById("normalizer-floating-panel"); if (panel) panel.remove(); } //************************************************************************** //Nombre: isSimilar //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - a (string): Primera palabra a comparar. // - b (string): Segunda palabra a comparar. //Salidas: // - boolean: Retorna true si las palabras son consideradas similares de forma leve; de lo contrario, retorna false. //Prerrequisitos si existen: Ninguno. //Descripción: // Esta función evalúa la similitud leve entre dos palabras. Primero, verifica si ambas palabras son // idénticas, en cuyo caso retorna true. Luego, comprueba si la diferencia en la cantidad de caracteres // entre ambas es mayor a 2; si es así, retorna false. Posteriormente, compara carácter por carácter // hasta el largo mínimo de las palabras, contando las diferencias. Si el número de discrepancias // excede 2, se considera que las palabras no son similares y retorna false; en caso contrario, retorna true. //************************************************************************** function isSimilar(a, b) { if (a === b) return true; if (Math.abs(a.length - b.length) > 2) return false; let mismatches = 0; for (let i = 0; i < Math.min(a.length, b.length); i++) { if (a[i] !== b[i]) mismatches++; if (mismatches > 2) return false; } return true; } //************************************************************************** //Nombre: normalizePlaceName //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: name (string) - Nombre del lugar //Salidas: texto normalizado (string) //Descripción: Normaliza un nombre aplicando capitalización, manejo de artículos, números y paréntesis. //************************************************************************** function normalizePlaceName(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y"]; const words = name.trim().split(/\s+/); const isRoman = word => /^(i{1,3}|iv|v|vi{0,3}|ix|x|xi{0,3}|xiv|xv|xvi{0,3}|xix|xx|xxi{0,3}|xxiv|xxv)$/i.test(word); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Si la palabra es un número, no la analizamos if (/^\d+$/.test(word)) return word; // Si la palabra está en la lista de exclusión, no la modificamos if (excludeWords.some(w => w.toLowerCase() === lowerWord)) return word; // Si es un número romano, lo dejamos en mayúsculas if (isRoman(word)) return word.toUpperCase(); // Si es una sigla con estructura T&T o a&A, convertirla a mayúsculas if (/^[A-Za-z]&[A-Za-z]$/.test(word)) return word.toUpperCase(); // Si es una sigla con apóstrofe como "E's", también la dejamos igual if (/^[A-Z]'[A-Z][a-z]+$/.test(word)) return word; // Si no se deben normalizar artículos y es un artículo, lo dejamos en minúsculas (excepto la primera palabra) if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) return lowerWord; // Si es un número seguido de letras, lo dejamos igual if (/^\d+[A-Z][a-zA-Z]*$/.test(word)) return word; // Si está entre paréntesis y es todo mayúsculas o minúsculas, lo dejamos igual if (/^\(.*\)$/.test(word)) { const inner = word.slice(1, -1); if (inner === inner.toUpperCase() || inner === inner.toLowerCase()) return word; } // Capitalizamos la palabra (primera letra en mayúscula, el resto en minúscula) return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); let newName = normalizedWords.join(" ") .replace(/\s*\|\s*/g, " - ") .replace(/([(["'])\s*([\p{L}])/gu, (match, p1, p2) => p1 + p2.toUpperCase()) .replace(/\s*-\s*/g, " - ") .replace(/\b(\d+)([A-Z])\b/g, (match, num, letter) => num + letter.toUpperCase()) .replace(/\.$/, "") .replace(/&(\s*)([A-Z])/g, (match, space, letter) => "&" + space + letter.toUpperCase()); return newName.replace(/\s{2,}/g, " ").trim(); } // Para exponer al contexto global real desde Tampermonkey unsafeWindow.normalizePlaceName = normalizePlaceName; //************************************************************************** //Nombre: openFloatingPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: placesToNormalize (array de lugares con nombre original y sugerencias) //Salidas: Panel flotante con opciones de normalización y eliminación //Prerrequisitos si existen: Debe haberse definido normalizePlaceName y cargado excludeWords correctamente //Descripción: Crea un panel interactivo donde se presentan los lugares que requieren cambios, // permitiendo su corrección o eliminación. Solo muestra lugares que requieren cambio // y errores ortográficos verdaderos. //************************************************************************** function openFloatingPanel(placesToNormalize) { const panel = document.createElement("div"); panel.id = "normalizer-floating-panel"; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; max-height: 80vh; overflow-y: auto; min-width: 800px; `; let panelContent = ` <style> #normalizer-table { width: 100%; border-collapse: collapse; } #normalizer-table th, #normalizer-table td { padding: 8px; border: 1px solid #ddd; } #normalizer-table tr:nth-child(even) { background-color: #f9f9f9; } .warning-row { background-color: #fff3cd !important; } .close-btn { position: absolute; top: 10px; right: 10px; background: #ccc; color: black; border: none; border-radius: 4px; width: 25px; height: 25px; font-size: 16px; font-weight: bold; cursor: pointer; text-align: center; line-height: 25px; transition: background-color 0.3s ease; } .close-btn:hover { background: #bbb; } .footer-buttons { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } </style> <button class="close-btn" id="close-panel-btn">X</button> <h3>Places to Normalize</h3> <table id="normalizer-table"> <thead> <tr> <th>Normalizar</th> <th>Eliminar</th> <th>Estado</th> <th>Nombre Original</th> <th>Nombre Sugerido</th> <th>Corrección</th> <th>Acción</th> </tr> </thead> <tbody> `; // Procesar cada lugar y sus advertencias placesToNormalize.forEach((place, index) => { const { originalName, newName, spellingWarnings = [] } = place; // Crear una fila por cada advertencia ortográfica spellingWarnings.forEach((warning, warningIndex) => { const suggestionId = `suggestion-${index}-${warningIndex}`; panelContent += ` <tr class="warning-row"> <td style="text-align: center;"> <input type="checkbox" class="normalize-checkbox" data-index="${index}" data-warning-index="${warningIndex}" data-suggestion-id="${suggestionId}"> </td> <td style="text-align: center;"> <input type="checkbox" class="delete-checkbox" data-index="${index}" data-warning-index="${warningIndex}"> </td> <td style="text-align: center;">⚠️</td> <td id="name-cell-${index}-${warningIndex}">${originalName}</td> <td> <input type="text" class="new-name-input" data-index="${index}" data-warning-index="${warningIndex}" data-place-id="${place.id}" data-suggestion-id="${suggestionId}" value="${newName}"> </td> <td>${warning.original} → ${warning.sugerida} (${warning.tipo})</td> <td style="text-align: center;"> <button class="apply-suggestion-btn" data-index="${index}" data-warning-index="${warningIndex}" title="Corregir ortografía de la palabra" style="background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Fix </button> <button class="add-exclude-btn" data-word="${warning.original}" title="Adicionar palabra excluida nueva" style="background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Add </button> </td> </tr>`; }); }); panelContent += ` </tbody> </table> <div class="footer-buttons"> <button class="apply-changes-btn" id="apply-changes-btn">✔️ Apply Changes</button> <button class="cancel-btn" id="cancel-btn">❌ Cancel</button> </div> `; panel.innerHTML = panelContent; document.body.appendChild(panel); // Eventos para los botones document.getElementById('close-panel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); document.getElementById('apply-changes-btn').addEventListener('click', () => { window.applyNormalization(); // Llamar a la función global para aplicar cambios }); document.getElementById('cancel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); // Asignar eventos a los botones "Fix" document.querySelectorAll('.apply-suggestion-btn').forEach(button => { button.addEventListener('click', function () { const index = this.dataset.index; const warningIndex = this.dataset.warningIndex; const input = document.querySelector( `.new-name-input[data-index="${index}"][data-warning-index="${warningIndex}"]` ); const checkbox = document.querySelector( `.normalize-checkbox[data-index="${index}"][data-warning-index="${warningIndex}"]` ); if (input && checkbox) { // Aplicar la corrección ortográfica al campo de entrada const warning = placesToNormalize[index].spellingWarnings[warningIndex]; input.value = input.value.replace(warning.original, warning.sugerida); checkbox.checked = true; // Marcar el checkbox } }); }); // Asignar eventos a los botones "Add Excld Word" document.querySelectorAll('.add-exclude-btn').forEach(button => { button.addEventListener('click', function () { const word = this.dataset.word; // Verificar si es una sigla con formato X&X if (/^[A-Za-z]&[A-Za-z]$/.test(word)) { alert("⚠️ No es necesario adicionar palabras excluidas que tengan '&'."); return; } if (!excludeWords.includes(word)) { excludeWords.push(word); excludeWords.sort((a, b) => a.localeCompare(b)); // Ordenar alfabéticamente localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Actualizar el panel lateral // Mostrar popup en el centro del panel flotante const panel = document.getElementById("normalizer-floating-panel"); const popup = document.createElement('div'); popup.textContent = `✅ The Word "${word}" has been added to the exclusion list.`; popup.style.position = 'absolute'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.backgroundColor = '#4CAF50'; popup.style.color = 'white'; popup.style.padding = '10px 20px'; popup.style.borderRadius = '5px'; popup.style.zIndex = '10000'; popup.style.opacity = '1'; popup.style.transition = 'opacity 1s ease-in-out'; panel.appendChild(popup); setTimeout(() => { popup.style.opacity = '0'; setTimeout(() => panel.removeChild(popup), 1000); }, 2000); // Bloquear el botón y cambiar su texto this.textContent = 'Added'; this.disabled = true; this.style.backgroundColor = '#6c757d'; // Cambiar color a gris this.style.cursor = 'not-allowed'; } else { alert(`⚠️ The word "${word}" is already on the exclusion list.`); } }); }); } //************************************************************************** //Nombre: loadExcludeWordsFromXML //Fecha modificación: //Autor: mincho77 //Entradas: // - callback: función opcional que se ejecuta una vez se cargan y procesan las palabras excluidas. //Salidas: ninguna directa. Actualiza la variable global `excludeWords`. //Prerrequisitos: // - Debe existir un archivo llamado 'excludeWords.xml' accesible por fetch. // - Debe estar definida la variable global `excludeWords`. //Descripción: // Carga un archivo XML que contiene una lista de palabras excluidas. // Combina las palabras nuevas con las que ya están guardadas en localStorage // y actualiza la lista global `excludeWords`. Si el XML no puede ser cargado, // se usa únicamente el contenido almacenado localmente como respaldo. //************************************************************************** function loadExcludeWordsFromXML(callback) { fetch("excludeWords.xml") .then(response => response.text()) // Corrected the syntax here .then(xmlText => { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const wordNodes = xmlDoc.getElementsByTagName("word"); const wordsFromXML = Array.from(wordNodes).map(node => node.textContent.trim()); const existing = JSON.parse(localStorage.getItem("excludeWords")) || []; excludeWords = [...new Set([...existing, ...wordsFromXML])].sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); if (callback) callback(); }) .catch(() => { console.warn("⚠️ No se pudo cargar excludeWords.xml. Solo se usará localStorage."); excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || ["EDS", "IPS", "McDonald's", "EPS"]; localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); if (callback) callback(); }); } function exportExcludeWordsToXML() { const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${excludeWords.map(word => ` <word>${word}</word>`).join("\n")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); // ✅ ESTA LÍNEA ESTABA FALTANDO const link = document.createElement("a"); link.href = url; link.download = "excludeWords.xml"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function showFloatingMessage(message) { const msg = document.createElement("div"); msg.textContent = message; msg.style.position = "fixed"; msg.style.bottom = "30px"; msg.style.left = "50%"; msg.style.transform = "translateX(-50%)"; msg.style.backgroundColor = "#333"; msg.style.color = "#fff"; msg.style.padding = "10px 20px"; msg.style.borderRadius = "5px"; msg.style.zIndex = 9999; msg.style.opacity = "0.95"; msg.style.transition = "opacity 1s ease-in-out"; document.body.appendChild(msg); setTimeout(() => { msg.style.opacity = "0"; setTimeout(() => document.body.removeChild(msg), 1000); }, 3000); } //************************************************************************** //Nombre: waitForWME //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab y renderExcludedWordsPanel. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. // Una vez disponible, inicializa la lista de palabras excluidas desde localStorage, // crea el tab lateral personalizado y renderiza la lista visual de palabras excluidas. // Si WME aún no está listo, vuelve a intentar cada 1000 ms. //************************************************************************** function waitForWME() { if (W && W.userscripts && W.model && W.model.venues) { console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); // ⚠️ Usa solo localStorage createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[waitForWME] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); // Muestra las palabras setupDragAndDropImport(); // Activa drag & drop //Evita que se abra el archivo si cae fuera del área // window.addEventListener("dragover", e => e.preventDefault(), false); // window.addEventListener("drop", e => e.preventDefault(), false); }); } else { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(waitForWME, 1000); } } //************************************************************************** //Nombre: cleanupEventListeners //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Debe existir un elemento en el DOM con el id "normalizer-floating-panel". //Descripción: // Esta función limpia los event listeners asociados al panel flotante de normalización. // Lo hace clonando el nodo del panel y reemplazándolo en el DOM, lo cual elimina todos // los listeners previamente asignados a ese nodo, evitando posibles fugas de memoria // o comportamientos inesperados. //************************************************************************** function cleanupEventListeners() { const panel = document.getElementById("normalizer-floating-panel"); if (panel) { const clone = panel.cloneNode(true); panel.parentNode.replaceChild(clone, panel); } } //************************************************************************** //Nombre: normalizePlaceName (unsafeWindow) //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - name (string): el nombre original del lugar. //Salidas: // - string: nombre normalizado, respetando exclusiones y opciones del usuario. //Prerrequisitos si existen: // - Debe estar cargada la lista global excludeWords. // - Debe existir un checkbox con id “normalizeArticles” para definir si se normalizan artículos. //Descripción: // Esta versión expuesta globalmente permite acceder a la normalización básica del nombre de un lugar // desde otros contextos como el navegador o Tampermonkey. Capitaliza cada palabra, respeta las excluidas // y no aplica normalización a artículos si el checkbox lo indica. // Realiza limpieza básica: reemplazo de pipes, eliminación de espacios dobles y trim final. //************************************************************************** unsafeWindow.normalizePlaceName = function(name) { if (!name) return ""; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; const articles = ["el", "la", "los", "las", "de", "del", "al", "y", "o"]; const words = name.trim().split(/\s+/); const normalizedWords = words.map((word, index) => { const lowerWord = word.toLowerCase(); // Saltar palabras excluidas if (excludeWords.includes(word)) return word; // Saltar artículos si el checkbox está activo y no es la primera palabra if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) { return lowerWord; } //Mayúsculas return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }); name = normalizedWords.join(" "); name = name.replace(/\s*\|\s*/g, " - "); name = name.replace(/\s{2,}/g, " ").trim(); return name; }; //************************************************************************** //Nombre: waitForDOM //Fecha modificación: 2025-03-31 //Autor: mincho77 //Entradas: // selector (string): Selector CSS del nodo a esperar. // callback (function): Función a ejecutar cuando el nodo esté disponible. // interval (int, opcional): Tiempo entre intentos en milisegundos. Default: 500ms. // maxAttempts (int, opcional): Máximo de intentos antes de abortar. Default: 10. //Salidas: Ejecuta el callback si encuentra el selector dentro del tiempo. //Descripción: // Esta función monitorea el DOM en intervalos constantes hasta que // encuentra un nodo que coincida con el selector. Si lo encuentra, // ejecuta el callback con ese nodo. Si no, muestra un error por consola. //************************************************************************** function waitForDOM(selector, callback, interval = 500, maxAttempts = 10) { let attempts = 0; const checkExist = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(checkExist); callback(element); } else if (attempts >= maxAttempts) { clearInterval(checkExist); console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM después de ${maxAttempts} intentos.`); } attempts++; }, interval); } //************************************************************************** //Nombre: getSidebarHTML //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna. //Salidas: Retorna un string con HTML que define el contenido del panel lateral del script. //Prerrequisitos si existen: Debe estar disponible el valor de las variables globales: // - normalizeArticles (boolean): Define si los artículos deben ser normalizados o no. // - maxPlaces (number): Número máximo de lugares a escanear. //Descripción: // Esta función construye el HTML que se inyecta en el panel lateral (sidebar) de WME. // Incluye controles para: // - Activar o desactivar normalización de artículos ("el", "la", etc). // - Definir la cantidad máxima de lugares a procesar. // - Agregar palabras excluidas manualmente. // - Exportar palabras excluidas a un archivo XML. // - Importar una lista de palabras excluidas desde archivo XML. // - Disparar el escaneo de lugares para normalización. // La lista de palabras excluidas **no se renderiza aquí directamente**, sino en el div // con id "normalizer-sidebar" para permitir que sea manejada dinámicamente. //************************************************************************** function getSidebarHTML() { return ` <div id="normalizer-tab"> <h4>Places Name Normalizer <span style="font-size:11px;">v${VERSION}</span></h4> <div> <input type="checkbox" id="normalizeArticles" ${normalizeArticles ? "checked" : ""}> <label for="normalizeArticles">No Normalizar artículos (el, la, los, ...)</label> </div> <div> <label>Máximo de Places a buscar: </label> <input type='number' id='maxPlacesInput' value='${maxPlaces}' min='1' max='800' style='width: 60px;'> </div> <div> <label>Palabras Excluidas:</label> <input type='text' id='excludeWord' style='width: 120px;'> <button id='addExcludeWord'>Add Word</button> <div id="normalizer-sidebar" style="margin-top: 20px;"></div> <button id="exportExcludeWords" style="margin-top: 5px;">Export Words</button> <br> <button id="importExcludeWordsUnifiedBtn" style="margin-top: 5px;">Import List...</button> <input type="file" id="hiddenImportInput" accept=".xml" style="display: none;"> </div> <div> <input type="checkbox" id="checkOnlyTildes" checked> <label for="checkOnlyTildes">Revisar solo tildes</label> </div> <div id="drop-zone" style="border: 2px dashed #ccc; border-radius: 6px; padding: 15px; margin: 15px 0; text-align: center; font-style: italic; color: #555; background-color: #f8f9fa; transition: all 0.3s ease;"> 📂 Arrastra aquí tu archivo .txt o .xml para importar palabras excluidas </div> <hr> <button id="scanPlaces">Scan...</button> </div> `; } //************************************************************************** //Nombre: attachEvents //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - Deben existir en el DOM los elementos con los siguientes IDs: // "normalizeArticles", "maxPlacesInput", "addExcludeWord", "scanPlaces", // "hiddenImportInput", "importExcludeWordsUnifiedBtn" y "exportExcludeWords". // - Debe existir la función handleImportList y la función scanPlaces. // - Debe estar definida la variable global excludeWords y la función renderExcludedWordsPanel. //Descripción: // Esta función adjunta los event listeners necesarios para gestionar la interacción del usuario // con el panel del normalizador de nombres. Se encargan de: // - Actualizar la opción de normalizar artículos al cambiar el estado del checkbox. // - Modificar el número máximo de lugares a procesar a través de un input. // - Exportar la lista de palabras excluidas a un archivo XML. // - Añadir nuevas palabras a la lista de palabras excluidas, evitando duplicados, y actualizar el panel. // - Activar el botón unificado para la importación de palabras excluidas mediante un input oculto. // - Ejecutar la función de escaneo de lugares al hacer clic en el botón correspondiente. //************************************************************************** function attachEvents2() { console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`); const normalizeArticlesCheckbox = document.getElementById("normalizeArticles"); const maxPlacesInput = document.getElementById("maxPlacesInput"); const addExcludeWordButton = document.getElementById("addExcludeWord"); const scanPlacesButton = document.getElementById("scanPlaces"); const hiddenInput = document.getElementById("hiddenImportInput"); const importButtonUnified = document.getElementById("importExcludeWordsUnifiedBtn"); // Validación de elementos necesarios if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) { console.error(`[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`); return; } // ✅ Evento: cambiar estado de "no normalizar artículos" normalizeArticlesCheckbox.addEventListener("change", (e) => { normalizeArticles = e.target.checked; }); // ✅ Evento: cambiar número máximo de places maxPlacesInput.addEventListener("input", (e) => { maxPlaces = parseInt(e.target.value, 10); }); // ✅ Evento: exportar palabras excluidas a XML document.getElementById("exportExcludeWords").addEventListener("click", () => { const savedWords = JSON.parse(localStorage.getItem("excludeWords")) || []; if (savedWords.length === 0) { alert("No hay palabras excluidas para exportar."); return; } const sortedWords = [...savedWords].sort((a, b) => a.localeCompare(b)); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${sortedWords.map(word => ` <word>${word}</word>`).join("\n ")} </ExcludedWords>`; const blob = new Blob([xmlContent], { type: "application/xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "excluded_words.xml"; document.body.appendChild(link); // Correctly appends the link link.click(); document.body.removeChild(link); // Correctly removes the link }); // ✅ Evento: añadir palabra excluida sin duplicados addExcludeWordButton.addEventListener("click", () => { const wordInput = document.getElementById("excludeWord") || document.getElementById("excludedWord"); const word = wordInput?.value.trim(); if (!word) return; const lowerWord = word.toLowerCase(); const alreadyExists = excludeWords.some(w => w.toLowerCase() === lowerWord); if (!alreadyExists) { excludeWords.push(word); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Usar renderExcludedWordsPanel en lugar de updateExcludeList } wordInput.value = ""; }); // ✅ Evento: nuevo botón unificado de importación importButtonUnified.addEventListener("click", () => { hiddenInput.click(); // abre el file input oculto }); hiddenInput.addEventListener("change", () => { handleImportList(); // ✅ Llama a la función handleImportList al importar }); // ✅ Evento: escanear lugares scanPlacesButton.addEventListener("click", scanPlaces); } //************************************************************************** //Nombre: NameChangeAction //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: // - venue (object): Objeto Place que contiene la información del lugar a modificar. // - oldName (string): Nombre actual del lugar. // - newName (string): Nuevo nombre sugerido para el lugar. //Salidas: Ninguna (función constructora que crea un objeto de acción). //Prerrequisitos si existen: // - El objeto venue debe contar con la propiedad attributes, y dentro de ésta, con el campo id. //Descripción: // Esta función constructora crea un objeto que representa la acción de cambio de nombre // de un Place en el Waze Map Editor (WME). Asigna las propiedades correspondientes: // - Guarda el objeto venue, el nombre original (oldName) y el nuevo nombre (newName). // - Extrae y guarda el ID único del lugar desde venue.attributes.id. // - Establece metadatos para identificar la acción, asignando el tipo "NameChangeAction" // y marcando isGeometryEdit como false para indicar que no se trata de una edición de geometría. // Estos metadatos pueden ser utilizados por WME y otros plugins para gestionar y mostrar la acción. //************************************************************************** function NameChangeAction(venue, oldName, newName) { // Referencia al Place y los nombres this.venue = venue; this.oldName = oldName; this.newName = newName; // ID único del Place this.venueId = venue.attributes.id; // Metadatos que WME/Plugins pueden usar this.type = "NameChangeAction"; this.isGeometryEdit = false; // no es una edición de geometría } /** * 1) getActionName: nombre de la acción en el historial. */ NameChangeAction.prototype.getActionName = function() { return "Update place name"; }; /** 2) getActionText: texto corto que WME a veces muestra. */ NameChangeAction.prototype.getActionText = function() { return "Update place name"; }; /** 3) getName: algunas versiones llaman a getName(). */ NameChangeAction.prototype.getName = function() { return "Update place name"; }; /** 4) getDescription: descripción detallada de la acción. */ NameChangeAction.prototype.getDescription = function() { return `Place name changed from "${this.oldName}" to "${this.newName}".`; }; /** 5) getT: título (a veces requerido por plugins). */ NameChangeAction.prototype.getT = function() { return "Update place name"; }; /** 6) getID: si un plugin llama a e.getID(). */ NameChangeAction.prototype.getID = function() { return `NameChangeAction-${this.venueId}`; }; /** 7) doAction: asigna el nuevo nombre (WME llama a esto al crear la acción). */ NameChangeAction.prototype.doAction = function() { this.venue.attributes.name = this.newName; this.venue.isDirty = true; if (typeof W.model.venues.markObjectEdited === "function") { W.model.venues.markObjectEdited(this.venue); } }; /** 8) undoAction: revertir al nombre anterior (Ctrl+Z). */ NameChangeAction.prototype.undoAction = function() { this.venue.attributes.name = this.oldName; this.venue.isDirty = true; if (typeof W.model.venues.markObjectEdited === "function") { W.model.venues.markObjectEdited(this.venue); } }; /** 9) redoAction: rehacer (Ctrl+Shift+Z), vuelve a doAction. */ NameChangeAction.prototype.redoAction = function() { return this.doAction(); }; /** 10) undoSupported / redoSupported: indica si se puede des/rehacer. */ NameChangeAction.prototype.undoSupported = function() { return true; }; NameChangeAction.prototype.redoSupported = function() { return true; }; /** 11) accept / supersede: evita fusionar con otras acciones. */ NameChangeAction.prototype.accept = function() { return false; }; NameChangeAction.prototype.supersede = function() { return false; }; /** 12) isEditAction: true => habilita "Guardar". */ NameChangeAction.prototype.isEditAction = function() { return true; }; /** 13) getAffectedUniqueIds: objetos que se alteran. */ NameChangeAction.prototype.getAffectedUniqueIds = function() { return [this.venueId]; }; /** 14) isSerializable: si no implementas serialize(), pon false. */ NameChangeAction.prototype.isSerializable = function() { return false; }; /** 15) isActionStackable: false => no combina con otras ediciones. */ NameChangeAction.prototype.isActionStackable = function() { return false; }; /** 16) getFocusFeatures: WME/Plugins pueden usarlo para "enfocar" el objeto. */ NameChangeAction.prototype.getFocusFeatures = function() { // Devolvemos el venue para indicar que ese es el foco (o un array vacío si prefieres). return [this.venue]; }; /** 17) Métodos vacíos para evitar futuros "no es una función" si WME pide estos. */ NameChangeAction.prototype.getFocusSegments = function() { return []; }; NameChangeAction.prototype.getFocusNodes = function() { return []; }; NameChangeAction.prototype.getFocusClosures = function() { return []; }; /** 18) getTimestamp: método nuevo que WME/Plugins están llamando. */ NameChangeAction.prototype.getTimestamp = function() { // Devolvemos un timestamp numérico (ms desde época UNIX). return Date.now(); }; //************************************************************************** //Nombre: openFloatingPanel //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: placesToNormalize (array de lugares con nombre original y sugerencias) //Salidas: Panel flotante con opciones de normalización y eliminación //Prerrequisitos si existen: Debe haberse definido normalizePlaceName y cargado excludeWords correctamente //Descripción: Crea un panel interactivo donde se presentan los lugares que requieren cambios, // permitiendo su corrección o eliminación. Solo muestra lugares que requieren cambio // y errores ortográficos verdaderos. //************************************************************************** function openFloatingPanel2(placesToNormalize) { const panel = document.createElement("div"); panel.id = "normalizer-floating-panel"; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; max-height: 80vh; overflow-y: auto; min-width: 800px; `; let panelContent = ` <style> #normalizer-table { width: 100%; border-collapse: collapse; } #normalizer-table th, #normalizer-table td { padding: 8px; border: 1px solid #ddd; } #normalizer-table tr:nth-child(even) { background-color: #f9f9f9; } .warning-row { background-color: #fff3cd !important; } .close-btn { position: absolute; top: 10px; right: 10px; background: #ccc; color: black; border: none; border-radius: 4px; width: 25px; height: 25px; font-size: 16px; font-weight: bold; cursor: pointer; text-align: center; line-height: 25px; transition: background-color 0.3s ease; } .close-btn:hover { background: #bbb; } .footer-buttons { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } </style> <button class="close-btn" id="close-panel-btn">X</button> <h3>Places to Normalize</h3> <table id="normalizer-table"> <thead> <tr> <th>Normalizar</th> <th>Eliminar</th> <th>Estado</th> <th>Nombre Original</th> <th>Nombre Sugerido</th> <th>Corrección</th> <th>Acción</th> </tr> </thead> <tbody> `; // Procesar cada lugar y sus advertencias placesToNormalize.forEach((place, index) => { const { originalName, newName, spellingWarnings = [] } = place; // Crear una fila por cada advertencia ortográfica spellingWarnings.forEach((warning, warningIndex) => { const suggestionId = `suggestion-${index}-${warningIndex}`; panelContent += ` <tr class="warning-row"> <td style="text-align: center;"> <input type="checkbox" class="normalize-checkbox" data-index="${index}" data-warning-index="${warningIndex}" data-suggestion-id="${suggestionId}"> </td> <td style="text-align: center;"> <input type="checkbox" class="delete-checkbox" data-index="${index}" data-warning-index="${warningIndex}"> </td> <td style="text-align: center;">⚠️</td> <td id="name-cell-${index}-${warningIndex}">${originalName}</td> <td> <input type="text" class="new-name-input" data-index="${index}" data-warning-index="${warningIndex}" data-place-id="${place.id}" data-suggestion-id="${suggestionId}" value="${newName}"> </td> <td>${warning.original} → ${warning.sugerida} (${warning.tipo})</td> <td style="text-align: center;"> <button class="apply-suggestion-btn" data-index="${index}" data-warning-index="${warningIndex}" title="Corregir ortografía de la palabra" style="background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Fix </button> <button class="add-exclude-btn" data-word="${warning.original}" title="Adicionar palabra excluida nueva" style="background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"> Add Excld </button> </td> </tr>`; }); }); panelContent += ` </tbody> </table> <div class="footer-buttons"> <button class="apply-changes-btn" id="apply-changes-btn">✔️ Apply Changes</button> <button class="cancel-btn" id="cancel-btn">❌ Cancel</button> </div> `; panel.innerHTML = panelContent; document.body.appendChild(panel); // Eventos para los botones document.getElementById('close-panel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); document.getElementById('apply-changes-btn').addEventListener('click', () => { window.applyNormalization(); // Llamar a la función global para aplicar cambios }); document.getElementById('cancel-btn').addEventListener('click', () => { panel.remove(); // Cerrar el panel flotante }); // Asignar eventos a los botones "Fix" document.querySelectorAll('.apply-suggestion-btn').forEach(button => { button.addEventListener('click', function () { const index = this.dataset.index; const warningIndex = this.dataset.warningIndex; const input = document.querySelector( `.new-name-input[data-index="${index}"][data-warning-index="${warningIndex}"]` ); const checkbox = document.querySelector( `.normalize-checkbox[data-index="${index}"][data-warning-index="${warningIndex}"]` ); if (input && checkbox) { const warning = placesToNormalize[index].spellingWarnings[warningIndex]; input.value = input.value.replace(warning.original, warning.sugerida || warning.original); checkbox.checked = true; // Marcar el checkbox } }); }); // Asignar eventos a los botones "Add Excld Word" document.querySelectorAll('.add-exclude-btn').forEach(button => { button.addEventListener('click', function () { const word = this.dataset.word; if (!excludeWords.includes(word)) { excludeWords.push(word); excludeWords.sort((a, b) => a.localeCompare(b)); // Ordenar alfabéticamente localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Actualizar el panel lateral // Mostrar popup en el centro del panel flotante const panel = document.getElementById("normalizer-floating-panel"); const popup = document.createElement('div'); popup.textContent = `✅ The Word "${word}" has been added to the exclusion list.`; popup.style.position = 'absolute'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.backgroundColor = '#4CAF50'; popup.style.color = 'white'; popup.style.padding = '10px 20px'; popup.style.borderRadius = '5px'; popup.style.zIndex = '10000'; popup.style.opacity = '1'; popup.style.transition = 'opacity 1s ease-in-out'; panel.appendChild(popup); setTimeout(() => { popup.style.opacity = '0'; setTimeout(() => panel.removeChild(popup), 1000); }, 2000); // Bloquear el botón y cambiar su texto this.textContent = 'Excluded Word Added'; this.disabled = true; this.style.backgroundColor = '#6c757d'; // Cambiar color a gris this.style.cursor = 'not-allowed'; } else { alert(`⚠️ The word "${word}" is already on the exclusion list.`); } }); }); } //************************************************************************** //Nombre: waitForWME //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab y renderExcludedWordsPanel. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. // Una vez disponible, inicializa la lista de palabras excluidas desde localStorage, // crea el tab lateral personalizado y renderiza la lista visual de palabras excluidas. // Si WME aún no está listo, vuelve a intentar cada 1000 ms. //************************************************************************** function waitForWME2() { if (W && W.userscripts && W.model && W.model.venues) { console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); // ⚠️ Usa solo localStorage createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[waitForWME] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); // Muestra las palabras setupDragAndDropImport(); // Activa drag & drop //Evita que se abra el archivo si cae fuera del área // window.addEventListener("dragover", e => e.preventDefault(), false); // window.addEventListener("drop", e => e.preventDefault(), false); }); } else { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(waitForWME, 1000); } } //************************************************************************** //Nombre: init //Fecha modificación: 2025-03-30 //Autor: mincho77 //Entradas: Ninguna //Salidas: Ninguna //Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, createSidebarTab, waitForDOM, renderExcludedWordsPanel y setupDragAndDropImport. //Descripción: // Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, // verificando que existan los objetos necesarios para iniciar el script. Si WME aún no está listo, // reintenta la inicialización cada 1000 ms. Una vez disponible, inicializa la lista de palabras // excluidas, crea el tab lateral personalizado, espera a que el DOM del tab esté listo para // renderizar el contenido (palabras excluidas y funcionalidad drag & drop), y expone globalmente // las funciones applyNormalization y normalizePlaceName para uso externo. //************************************************************************** function init() { if (!W || !W.userscripts || !W.model || !W.model.venues) { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(init, 1000); return; } console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[init] 🧩 Sidebar listo, renderizando palabras excluidas"); renderExcludedWordsPanel(); setupDragAndDropImport(); }); window.applyNormalization = applyNormalization; window.normalizePlaceName = normalizePlaceName; } init(); } catch (e) { console.error('[PlacesNameNormalizer] Fatal initialization error:', e); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址