您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Normaliza nombres de lugares en Waze Map Editor (WME)
// ==UserScript== // @name WME Places Name Normalizer // @namespace https://gf.qytechs.cn/en/users/mincho77 // @version 6.2.2 // @author Mincho77 // @description Normaliza nombres de lugares en Waze Map Editor (WME) // @license MIT // @include https://beta.waze.com/* // @include https://www.waze.com/editor* // @include https://www.waze.com/*/editor* // @exclude https://www.waze.com/user/editor* // @grant none // @run-at document-end // ==/UserScript== (function() { // Variables globales básicas const SCRIPT_NAME = GM_info.script.name; const VERSION = GM_info.script.version.toString(); // Variables globales para el panel flotante let floatingPanelElement = null; const processingPanelDimensions = { width: '400px', height: '200px' }; // Panel pequeño para procesamiento const resultsPanelDimensions = { width: '1300px', height: '700px' }; // Panel grande para resultados const commonWords = [ 'de', 'del', 'el', 'la', 'los', 'las', 'y', 'e', 'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en', 'con', 'sin', 'sobre', 'tras', 'por', 'al', 'lo' ]; // --- Definir nombres de pestañas cortos antes de la generación de botones --- const tabNames = [ { label: "Gene", icon: "⚙️" }, { label: "Espe", icon: "🏷️" }, { label: "Dicc", icon: "📘" }, { label: "Reemp", icon: "🔂" } ]; let wmeSDK = null; // Almacena la instancia del SDK de WME. //**************************************************************************************************** // Nombre: tryInitializeSDK // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Intenta inicializar el SDK de WME de forma asíncrona. // Parámetros: // finalCallback (Function): Función a llamar después del intento de inicialización (exitoso o no). //**************************************************************************************************** function tryInitializeSDK(finalCallback) { let attempts = 0; const maxAttempts = 60; // Intentos máximos (60 * 500ms = 30 segundos) const intervalTime = 500; let sdkAttemptInterval = null; function attempt() { if (typeof getWmeSdk === 'function') { if (sdkAttemptInterval) clearInterval(sdkAttemptInterval); try { wmeSDK = getWmeSdk({ scriptId : 'WMEPlacesNameInspector', scriptName : SCRIPT_NAME, }); if (wmeSDK) { // console.log("[SDK INIT SUCCESS] SDK obtenido exitosamente:", wmeSDK); } else { // console.warn("[SDK INIT WARNING] getWmeSdk() fue llamada pero devolvió null/undefined."); } } catch (e) { // console.error("[SDK INIT ERROR] Error al llamar a getWmeSdk():", e); wmeSDK = null; } finalCallback(); return; } attempts++; if (attempts >= maxAttempts) { if (sdkAttemptInterval) clearInterval(sdkAttemptInterval); // console.error(`[SDK INIT FAILURE] No se pudo encontrar getWmeSdk() después de ${maxAttempts} intentos.`); wmeSDK = null; finalCallback(); } } sdkAttemptInterval = setInterval(attempt, intervalTime); attempt(); } //**************************************************************************************************** function waitForWazeAPI(callbackPrincipalDelScript) { let wAttempts = 0; const wMaxAttempts = 40; const wInterval = setInterval(() => { wAttempts++; if (typeof W !== 'undefined' && W.map && W.loginManager && W.model && W.model.venues && W.userscripts && typeof W.userscripts.registerSidebarTab === 'function') { clearInterval(wInterval); // console.log("✅ Waze API (objeto W) cargado correctamente."); tryInitializeSDK(callbackPrincipalDelScript); } else if (wAttempts >= wMaxAttempts) { clearInterval(wInterval); // console.error("Error: No se pudo cargar la API principal de Waze (objeto W) después de varios intentos."); callbackPrincipalDelScript(); } }, 500); } function updateScanProgressBar(currentIndex, totalPlaces) { if (totalPlaces === 0) { return; } let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100); progressPercent = Math.min(progressPercent, 100); const progressBarInnerTab = document.getElementById("progressBarInnerTab"); const progressBarTextTab = document.getElementById("progressBarTextTab"); if (progressBarInnerTab && progressBarTextTab) { progressBarInnerTab.style.width = `${progressPercent}%`; const currentDisplay = Math.min(currentIndex + 1, totalPlaces); progressBarTextTab.textContent = `Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`; } } // Función para escapar caracteres especiales en expresiones regulares function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Función para eliminar tildes/diacríticos de una cadena function removeDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } // Función para calcular la similitud entre dos cadenas function isValidExcludedWord(newWord) { if (!newWord) return { valid : false, msg : "La palabra no puede estar vacía." }; const lowerNewWord = newWord.toLowerCase(); // No permitir palabras de un solo caracter if (newWord.length === 1) { return { valid : false, msg : "No se permite agregar palabras de un solo caracter." }; } // No permitir caracteres especiales solos if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord)) { return { valid : false, msg : "No se permite agregar solo caracteres especiales." }; } // No permitir si ya está en el diccionario (ignorando mayúsculas) if (window.dictionaryWords && Array.from(window.dictionaryWords) .some(w => w.toLowerCase() === lowerNewWord)) { return { valid : false, msg : "La palabra ya existe en el diccionario. No se puede agregar a especiales." }; } // No permitir palabras comunes if (commonWords.includes(lowerNewWord)) { return { valid : false, msg : "Esa palabra es muy común y no debe agregarse a la lista." }; } // No permitir duplicados en excluidas if (excludedWords && Array.from(excludedWords).some(w => w.toLowerCase() === lowerNewWord)) { return { valid : false, msg : "La palabra ya está en la lista (ignorando mayúsculas)." }; } return { valid : true }; } // Función para calcular la similitud entre dos cadenas function aplicarReemplazosGenerales(name) { if (typeof window.skipGeneralReplacements === "boolean" && window.skipGeneralReplacements) { return name; } const reglas = [ // Nueva regla: reemplazar | por espacio, guion y espacio { buscar: /\|/g, reemplazar: " - " }, // Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor { buscar : /\s*\/\s*/g, reemplazar : " / " }, { buscar : /\[P\]|\[p\]/g, reemplazar : "" }, { buscar : /\s*-\s*/g, reemplazar : " - " }, ]; reglas.forEach( regla => { name = name.replace(regla.buscar, regla.reemplazar); }); name = name.replace(/\s{2,}/g, ' '); return name; } function aplicarReglasEspecialesNombre(newName) { newName = newName.replace(/([A-Za-z])'([A-Za-z])/g, (match, before, after) => `${before}'${after.toLowerCase()}`); newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); newName = newName.replace(/\.\s+([a-z])/g, (match, letter) => `. ${letter.toUpperCase()}`); // --- INICIO: NUEVA REGLA PARA CAPITALIZAR DESPUÉS DE PARÉNTESIS DE APERTURA --- newName = newName.replace(/(\(\s*)([a-z])/g, (match, P1, P2) => { // P1 es el paréntesis de apertura y cualquier espacio después (ej. "( " o "(") // P2 es la primera letra minúscula después de eso. return P1 + P2.toUpperCase(); } ); // --- FIN: NUEVA REGLA --- const frasesAlFinal = [ "Conjunto Residencial", "Urbanización", "Conjunto Cerrado", "Unidad Residencial", "Parcelación", "Condominio Campestre", "Condominio", "Edificio", "Conjunto Habitacional", "Apartamentos", "Club Campestre", "Club Residencial", "Motel", "Restaurante", "Eco Hotel", "Finca Hotel", "Hotel" ]; for (const frase of frasesAlFinal) { const regex = new RegExp(`\\s+${escapeRegExp(frase)}$`, 'i'); if (regex.test(newName)) { const match = newName.match(regex); const fraseEncontrada = match[0].trim(); const restoDelNombre = newName.replace(regex, '').trim(); newName = `${fraseEncontrada} ${restoDelNombre}`; break; } } newName = newName.replace(/\s([a-zA-Z])$/, (match, letter) => ` ${letter.toUpperCase()}`); return newName; } function aplicarReemplazosDefinidos(text, replacementRules) { let newText = text; // Verificar si replacementRules es un objeto y tiene claves if (typeof replacementRules !== 'object' || replacementRules === null || Object.keys(replacementRules).length === 0) { return newText; // No hay reglas o no es un objeto, devolver el texto original } // Ordenar las claves de reemplazo por longitud descendente para manejar correctamente // los casos donde una clave puede ser subcadena de otra (ej. "D1 Super" y "D1"). const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length); for (const fromKey of sortedFromKeys) { const toValue = replacementRules[fromKey]; const escapedFromKey = escapeRegExp(fromKey); // Regex mejorada para definir "palabra completa" usando propiedades Unicode. // Grupo 1: Captura el delimitador previo (un no-palabra o inicio de cadena). // Grupo 2: Captura la clave de reemplazo que coincidió. // Grupo 3: Captura el delimitador posterior o fin de cadena. const regex = new RegExp(`(^|[^\\p{L}\\p{N}_])(${escapedFromKey})($|(?=[^\\p{L}\\p{N}_]))`, 'giu'); newText = newText.replace(regex, ( match, // La subcadena coincidente completa delimitadorPrevio, // Contenido del grupo de captura 1 matchedKey, // Contenido del grupo de captura 2 _delimitadorPosteriorOculto, // Contenido del grupo de captura 3 (no usado directamente en la lógica de abajo, pero necesario para alinear parámetros) offsetOfMatchInCurrentText, // El desplazamiento de la subcadena coincidente stringBeingProcessed // La cadena completa que se está examinando ) => { // Asegurarse de que stringBeingProcessed es una cadena if (typeof stringBeingProcessed !== 'string') { console.error( "[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'stringBeingProcessed' (la cadena original en el reemplazo) no es una cadena.", "Tipo:", typeof stringBeingProcessed, "Valor:", stringBeingProcessed, "Regla actual (fromKey):", fromKey, "Texto parcial procesado:", newText ); return match; } // Asegurarse de que offsetOfMatchInCurrentText es un número if (typeof offsetOfMatchInCurrentText !== 'number') { console.error( "[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'offsetOfMatchInCurrentText' no es un número.", "Tipo:", typeof offsetOfMatchInCurrentText, "Valor:", offsetOfMatchInCurrentText ); return match; } // Lógica existente para evitar el reemplazo si toValue ya está presente contextualizando fromKey. const fromKeyLower = fromKey.toLowerCase(); const toValueLower = toValue.toLowerCase(); const indexOfFromInTo = toValueLower.indexOf(fromKeyLower); if (indexOfFromInTo !== -1) { // El offset real de matchedKey dentro de stringBeingProcessed const actualMatchedKeyOffset = offsetOfMatchInCurrentText + (delimitadorPrevio ? delimitadorPrevio.length : 0); const potentialExistingToStart = actualMatchedKeyOffset - indexOfFromInTo; if (potentialExistingToStart >= 0 && (potentialExistingToStart + toValue.length) <= stringBeingProcessed.length) { const substringInOriginal = stringBeingProcessed.substring(potentialExistingToStart, potentialExistingToStart + toValue.length); if (substringInOriginal.toLowerCase() === toValueLower) { return match; } } } // --- INICIO: NUEVA LÓGICA PARA EVITAR DUPLICACIÓN DE PALABRAS EN LOS BORDES --- const palabrasDelToValue = toValue.trim().split(/\s+/).filter(p => p.length >0); // Filtrar palabras vacías if (palabrasDelToValue.length > 0) { const primeraPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[0].toLowerCase()); const ultimaPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[palabrasDelToValue.length - 1].toLowerCase()); // Palabra ANTES del inicio del 'match' (delimitadorPrevio + matchedKey) // El offsetOfMatchInCurrentText es el inicio de 'match', no de 'delimitadorPrevio' si son diferentes. // Necesitamos el texto ANTES de delimitadorPrevio. El offset real del inicio del match completo es offsetOfMatchInCurrentText. const textoAntesDelMatch = stringBeingProcessed.substring(0, offsetOfMatchInCurrentText); const palabrasEnTextoAntes = textoAntesDelMatch.trim().split(/\s+/).filter(p => p.length > 0); const palabraAnteriorLimpia = palabrasEnTextoAntes.length > 0 ? removeDiacritics(palabrasEnTextoAntes[palabrasEnTextoAntes.length - 1].toLowerCase()) : ""; // Palabra DESPUÉS del final del 'match' (delimitadorPrevio + matchedKey) const textoDespuesDelMatch = stringBeingProcessed.substring(offsetOfMatchInCurrentText + match.length); const palabrasEnTextoDespues = textoDespuesDelMatch.trim().split(/\s+/).filter(p => p.length > 0); const palabraSiguienteLimpia = palabrasEnTextoDespues.length > 0 ? removeDiacritics(palabrasEnTextoDespues[0].toLowerCase()) : ""; if (palabraAnteriorLimpia && primeraPalabraToValueLimpia && palabraAnteriorLimpia === primeraPalabraToValueLimpia) { // Solo prevenir si el delimitador previo es solo espacio o vacío, // indicando adyacencia real de palabras. if (delimitadorPrevio.trim() === "" || delimitadorPrevio.match(/^\s+$/)) { return match; } } if (palabraSiguienteLimpia && ultimaPalabraToValueLimpia && ultimaPalabraToValueLimpia === palabraSiguienteLimpia) { // El delimitadorPosteriorOculto nos dice qué sigue inmediatamente al matchedKey. // Si este delimitador es solo espacio o vacío, y luego viene la palabra duplicada. if (_delimitadorPosteriorOculto.trim() === "" || _delimitadorPosteriorOculto.match(/^\s+$/)) { return match; } } } // --- FIN: NUEVA LÓGICA PARA EVITAR DUPLICACIÓN --- return delimitadorPrevio + toValue; }); } return newText; } function getVisiblePlaces() { if (typeof W === 'undefined' || !W.map || !W.model || !W.model.venues) { console.warn('Waze Map Editor no está completamente cargado.'); return []; } const venues = W.model.venues.objects; const visiblePlaces = Object.values(venues).filter(venue => { const olGeometry = venue.getOLGeometry?.(); const bounds = olGeometry?.getBounds?.(); return bounds && W.map.getExtent().intersectsBounds(bounds); }); return visiblePlaces; } //************************************************************************************************************************************** // Nombre: renderPlacesInFloatingPanel // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Procesa y muestra los lugares con posibles inconsistencias en un panel flotante. // Incluye la lógica para obtener detalles de cada lugar, aplicar normalizaciones, // comparar con el nombre original y generar sugerencias. // Parámetros: // places (Array): Un array de objetos 'venue' de WME para analizar. //************************************************************************************************************************************** function renderPlacesInFloatingPanel(places) { createFloatingPanel("processing"); // Mostrar panel en modo "procesando" const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10); if (places.length > maxPlacesToScan) { places = places.slice(0, maxPlacesToScan); // Limitar el número de places a escanear } // console.log("[DEBUG] Preparando panel flotante. Total places a analizar:",places.length); // --- Funciones auxiliares para tipo y categoría --- function getPlaceCategoryName(venueFromOldModel, venueSDKObject) { // Acepta ambos tipos de venue let categoryId = null; let categoryName = null; // let source = ""; // Para saber de dónde vino la info (comentario de depuración eliminado) // Intento 1: Usar el venueSDKObject si está disponible y tiene la info if (venueSDKObject) { if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id) { categoryId = venueSDKObject.mainCategory.id; if (venueSDKObject.mainCategory.name) { categoryName = venueSDKObject.mainCategory.name; // source = "SDK (mainCategory.name)"; } } else if (Array.isArray(venueSDKObject.categories) && venueSDKObject.categories.length > 0) { const firstCategorySDK = venueSDKObject.categories[0]; if (typeof firstCategorySDK === 'object' && firstCategorySDK.id) { categoryId = firstCategorySDK.id; if (firstCategorySDK.name) { categoryName = firstCategorySDK.name; // source = "SDK (categories[0].name)"; } } else if (typeof firstCategorySDK === 'string') { categoryName = firstCategorySDK; // source = "SDK (categories[0] as string)"; } } else if (venueSDKObject.primaryCategoryID) { categoryId = venueSDKObject.primaryCategoryID; // source = "SDK (primaryCategoryID)"; } } if (categoryName) { // console.log(`[CATEGORÍA] Usando nombre de categoría de ${source}: ${categoryName} ${categoryId ? `(ID: ${categoryId})` : ''}`); // Comentario de depuración eliminado return categoryName; } // Intento 2: Usar W.model si no se obtuvo del SDK if (!categoryId && venueFromOldModel && venueFromOldModel.attributes && Array.isArray(venueFromOldModel.attributes.categories) && venueFromOldModel.attributes.categories.length > 0) { categoryId = venueFromOldModel.attributes.categories[0]; // source = "W.model (attributes.categories[0])"; } if (!categoryId) { return "Sin categoría"; } let categoryObjWModel = null; if (typeof W !== 'undefined' && W.model) { if (W.model.venueCategories && typeof W.model.venueCategories.getObjectById === "function") { categoryObjWModel = W.model.venueCategories.getObjectById(categoryId); } if (!categoryObjWModel && W.model.categories && typeof W.model.categories.getObjectById === "function") { categoryObjWModel = W.model.categories.getObjectById(categoryId); } } if (categoryObjWModel && categoryObjWModel.attributes && categoryObjWModel.attributes.name) { // console.log(`[CATEGORÍA] Usando nombre de categoría de W.model.categories (para ID ${categoryId} de ${source}): ${categoryObjWModel.attributes.name}`); // Comentario de depuración eliminado return categoryObjWModel.attributes.name; } if (typeof categoryId === 'number' || (typeof categoryId === 'string' && categoryId.trim() !== '')) { // console.log(`[CATEGORÍA] No se pudo resolver el nombre para ID de categoría ${categoryId} (obtenido de ${source}). Devolviendo ID.`); // Comentario de depuración eliminado return `${categoryId}`; // Devuelve el ID si no se encuentra el nombre. } return "Sin categoría"; } //**************************************************************************************************** // Nombre: getPlaceTypeInfo // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Determina si un lugar es un área o un punto y devuelve información iconográfica relacionada. // Parámetros: // venue (Object): El objeto 'venue' de WME. // Retorna: // Object: Un objeto con las propiedades 'isArea' (boolean), 'icon' (String) y 'title' (String). //**************************************************************************************************** function getPlaceTypeInfo(venue) { const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null; const isArea = geometry?.CLASS_NAME?.endsWith("Polygon"); return { isArea, icon : isArea ? "⭔" : "⊙", // Icono para área o punto title : isArea ? "Área" : "Punto" }; } //**************************************************************************************************** //**************************************************************************************************** // Nombre: shouldForceSuggestionForReview // Autor: Gemini AI / mincho77 // Fecha: 2025-05-27 // Ajusta la fecha según sea necesario // Descripción: Determina si una palabra del diccionario debe mostrarse siempre como sugerencia // para revisión manual debido a tildes y letras/combinaciones específicas. // Parámetros: // word (String): La palabra del diccionario a evaluar. // Retorna: // Boolean: true si la palabra cumple los criterios, false en caso contrario. //**************************************************************************************************** function shouldForceSuggestionForReview(word) { if (typeof word !== 'string') { return false; } const lowerWord = word.toLowerCase(); // Verificar si la palabra tiene alguna tilde (incluyendo mayúsculas acentuadas) const hasTilde = /[áéíóúÁÉÍÓÚ]/.test(word); if (!hasTilde) { return false; // Si no hay tilde, no forzar sugerencia por esta regla } // Lista de patrones de letras/combinaciones que, junto con una tilde, fuerzan la sugerencia // (insensible a mayúsculas debido a lowerWord) const problematicSubstrings = [ 'c', 's', 'x', 'cc', 'sc', 'cs', 'g', 'j', 'b', 'v','z','ll', 'y' // Podrías añadir más si es necesario, ej. 'z', 'b', 'v' si son problemáticas en tu contexto ]; for (const sub of problematicSubstrings) { if (lowerWord.includes(sub)) { return true; // Tiene tilde y una de las letras/combinaciones problemáticas } } return false; // Tiene tilde, pero no una de las letras/combinaciones problemáticas } //**************************************************************************************************** // --- Renderizar barra de progreso en el TAB PRINCIPAL justo después del // slice --- const tabOutput = document.querySelector("#wme-normalization-tab-output"); if (tabOutput) { // Reiniciar el estilo del mensaje en el tab al valor predeterminado tabOutput.style.color = "#000"; tabOutput.style.fontWeight = "normal"; // Crear barra de progreso visual const progressBarWrapperTab = document.createElement("div"); progressBarWrapperTab.style.margin = "10px 0"; progressBarWrapperTab.style.marginTop = "10px"; progressBarWrapperTab.style.height = "18px"; progressBarWrapperTab.style.backgroundColor = "transparent"; const progressBarTab = document.createElement("div"); progressBarTab.style.height = "100%"; progressBarTab.style.width = "0%"; progressBarTab.style.backgroundColor = "#007bff"; progressBarTab.style.transition = "width 0.2s"; progressBarTab.id = "progressBarInnerTab"; progressBarWrapperTab.appendChild(progressBarTab); const progressTextTab = document.createElement("div"); progressTextTab.style.fontSize = "12px"; progressTextTab.style.marginTop = "5px"; progressTextTab.id = "progressBarTextTab"; tabOutput.appendChild(progressBarWrapperTab); tabOutput.appendChild(progressTextTab); } // Asegurar que la barra de progreso en el tab se actualice desde el // principio const progressBarInnerTab = document.getElementById("progressBarInnerTab"); const progressBarTextTab = document.getElementById("progressBarTextTab"); if (progressBarInnerTab && progressBarTextTab) { progressBarInnerTab.style.width = "0%"; progressBarTextTab.textContent = `Progreso: 0% (0/${places.length})`; } // Mostrar el panel flotante desde el inicio // createFloatingPanel(0); // Mostrar el panel flotante desde el inicio // --- PANEL FLOTANTE: limpiar y preparar salida --- const output = document.querySelector("#wme-place-inspector-output"); if (!output) { console.error("❌ Panel flotante no está disponible"); return; } output.innerHTML = ""; // Limpia completamente el contenido del panel flotante output.innerHTML = "<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:11px; color:#555;'>Inicializando escaneo...</div></div></div>"; // Animación de puntos suspensivos const dotsSpan = output.querySelector(".dots"); if (dotsSpan) { const dotStates = ["", ".", "..", "..."]; let dotIndex = 0; window.processingDotsInterval = setInterval(() => { dotIndex = (dotIndex + 1) % dotStates.length; dotsSpan.textContent = dotStates[dotIndex]; }, 500); } output.style.height = "calc(55vh - 40px)"; // Si no hay places, mostrar mensaje y salir if (!places.length) { output.appendChild( document.createTextNode("No hay places visibles para analizar.")); const existingOverlay = document.getElementById("scanSpinnerOverlay"); if (existingOverlay) existingOverlay.remove(); return; } // --- Procesamiento incremental para evitar congelamiento --- let inconsistents = []; let index = 0; // Remover ícono de ✔ previo si existe const scanBtn = document.querySelector("button[type='button']"); if (scanBtn) { const existingCheck = scanBtn.querySelector("span"); if (existingCheck) { existingCheck.remove(); } } // --- Sugerencias por palabra global para toda la ejecución --- let sugerenciasPorPalabra = {}; // Convertir excludedWords a array solo una vez al inicio del análisis, // seguro ante undefined const excludedArray = (typeof excludedWords !== "undefined" && Array.isArray(excludedWords)) ? excludedWords : (typeof excludedWords !== "undefined" ? Array.from(excludedWords) : []); async function processNextPlace() { // 1. Leer estados de checkboxes y configuraciones iniciales const chkHideMyEditsChecked = document.getElementById("chk-hide-my-edits")?.checked ?? false; const useFullPipeline = true; // Como definimos, siempre ejecutar el flujo completo const applyGeneralReplacements = useFullPipeline || (document.getElementById("chk-general-replacements")?.checked ?? true); const checkExcludedWords = useFullPipeline || (document.getElementById("chk-check-excluded")?.checked ?? false); const checkDictionaryWords = true; const restoreCommas = document.getElementById("chk-restore-commas")?.checked ?? false; const similarityThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "85") / 100; // 2. Condición de salida principal (todos los lugares procesados) if (index >= places.length) { //console.log("[DEBUG] Todos los lugares procesados. Finalizando render..."); finalizeRender(inconsistents, places); return; } const venueFromOldModel = places[index]; // Usaremos este para datos base y fallback const currentVenueNameObj = venueFromOldModel?.attributes?.name; const nameValue = typeof currentVenueNameObj === 'object' && currentVenueNameObj !== null && typeof currentVenueNameObj.value === 'string' ? currentVenueNameObj.value.trim() !== '' ? currentVenueNameObj.value : undefined : typeof currentVenueNameObj === 'string' && currentVenueNameObj.trim() !== '' ? currentVenueNameObj : undefined; // Protección extra si el place en el índice es inválido if (!places[index] || typeof places[index] !== 'object') { // console.warn(`[DEBUG] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]); updateScanProgressBar(index, places.length); index++; setTimeout(() => processNextPlace(), 0); return; } // 3. Salto temprano si el venue es inválido o no tiene nombre if (!venueFromOldModel || typeof venueFromOldModel !== 'object' || !venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '') { //console.warn(`[DEBUG] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel); updateScanProgressBar(index, places.length); index++; setTimeout(() => processNextPlace(), 0); return; } const originalName = venueFromOldModel?.attributes?.name?.value || venueFromOldModel?.attributes?.name || ''; const currentVenueId = venueFromOldModel.getID(); // 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (PRIORIZANDO // SDK) --- let lastEditorInfoForLog = "Editor: Desconocido"; let lastEditorIdForComparison = null; let wasEditedByMe = false; let currentLoggedInUserId = null; let currentLoggedInUserName = null; // Para comparar si el SDK devuelve nombre if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user) { if (typeof W.loginManager.user.id === 'number') { currentLoggedInUserId = W.loginManager.user.id; } if (typeof W.loginManager.user.userName === 'string') { currentLoggedInUserName = W.loginManager.user.userName; } } if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById) { // console.log(`[SDK] Intentando obtener venue ID: ${currentVenueId} con sdk.DataModel.Venues.getById`); try { const venueSDK = await wmeSDK.DataModel.Venues.getById( { venueId : currentVenueId }); if (venueSDK && venueSDK.modificationData) { const updatedByDataFromSDK = venueSDK.modificationData.updatedBy; /* console.log( `[SDK] Para ID ${ currentVenueId}, venue.modificationData.updatedBy: `, updatedByDataFromSDK, `(Tipo: ${typeof updatedByDataFromSDK})`);*/ if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '') { lastEditorInfoForLog = `Editor (SDK): ${updatedByDataFromSDK}`; resolvedEditorName = updatedByDataFromSDK; // SDK provided username // directly if (currentLoggedInUserName && currentLoggedInUserName === updatedByDataFromSDK) { wasEditedByMe = true; } } else if (typeof updatedByDataFromSDK === 'number') { lastEditorInfoForLog = `Editor (SDK): ID ${updatedByDataFromSDK}`; resolvedEditorName = `ID ${updatedByDataFromSDK}`; // Default to ID lastEditorIdForComparison = updatedByDataFromSDK; if (typeof W !== 'undefined' && W.model && W.model.users) { const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK); if (userObjectW && userObjectW.userName) { lastEditorInfoForLog = `Editor (SDK ID ${ updatedByDataFromSDK} -> W.model): ${ userObjectW.userName}`; resolvedEditorName = userObjectW .userName; // Username found via W.model } else if (userObjectW) { lastEditorInfoForLog = `Editor (SDK ID ${ updatedByDataFromSDK} -> W.model): ID ${ updatedByDataFromSDK} (sin userName en W.model)`; // resolvedEditorName remains `ID // ${updatedByDataFromSDK}` } } } else if (updatedByDataFromSDK === null) { lastEditorInfoForLog = "Editor (SDK): N/D (updatedBy es null)"; resolvedEditorName = "N/D"; } else { lastEditorInfoForLog = `Editor (SDK): Valor inesperado para updatedBy ('${ updatedByDataFromSDK}')`; resolvedEditorName = "Inesperado (SDK)"; } } else { // venueSDK o venueSDK.modificationData no encontrados lastEditorInfoForLog = "Editor (SDK): No venue/modificationData. Usando fallback W.model."; if (venueSDK) console.log( "[SDK] venueSDK.modificationData no encontrado. Venue SDK:", venueSDK); else console.log("[SDK] venueSDK no fue obtenido para ID:", currentVenueId); const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy; if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined) { lastEditorIdForComparison = oldModelUpdatedBy; resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Default to ID let usernameFromOldModel = `ID ${oldModelUpdatedBy}`; if (typeof W !== 'undefined' && W.model && W.model.users) { const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy); if (userObjectW && userObjectW.userName) { usernameFromOldModel = userObjectW.userName; resolvedEditorName = userObjectW.userName; // Username found } else if (userObjectW) { usernameFromOldModel = `ID ${oldModelUpdatedBy} (sin userName)`; } } lastEditorInfoForLog = `Editor (W.model Fallback): ${usernameFromOldModel}`; } else { lastEditorInfoForLog = "Editor (W.model Fallback): N/D"; resolvedEditorName = "N/D"; } } } catch (e) { // console.error(`[SDK] Error al obtener venue ${currentVenueId} con el SDK:`, e); lastEditorInfoForLog = "Editor: Error con SDK (usando fallback W.model)"; resolvedEditorName = "Error SDK"; // Initial state for error const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy; if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined) { lastEditorIdForComparison = oldModelUpdatedBy; resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Default to ID on fallback let usernameFromOldModel = `ID ${oldModelUpdatedBy}`; if (typeof W !== 'undefined' && W.model && W.model.users) { const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy); if (userObjectW && userObjectW.userName) { usernameFromOldModel = userObjectW.userName; resolvedEditorName = userObjectW.userName; // Username found } else if (userObjectW) { usernameFromOldModel = `ID ${oldModelUpdatedBy} (sin userName)`; } } lastEditorInfoForLog = `Editor (W.model Fallback): ${usernameFromOldModel}`; } else { lastEditorInfoForLog = "Editor (W.model Fallback): N/D"; resolvedEditorName = "N/D"; } } } else { // Fallback completo a W.model si el SDK no está inicializado const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy; if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined) { lastEditorIdForComparison = oldModelUpdatedBy; resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Default to ID let usernameFromOldModel = `ID ${oldModelUpdatedBy}`; if (typeof W !== 'undefined' && W.model && W.model.users) { const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy); if (userObjectW && userObjectW.userName) { usernameFromOldModel = userObjectW.userName; resolvedEditorName = userObjectW.userName; // Username found } else if (userObjectW) { usernameFromOldModel = `ID ${oldModelUpdatedBy} (sin userName)`; } } lastEditorInfoForLog = `Editor (W.model): ${usernameFromOldModel}`; } else { lastEditorInfoForLog = "Editor (W.model): N/D"; resolvedEditorName = "N/D"; } } // Actualizar wasEditedByMe basado en ID numérico si lo tenemos if (currentLoggedInUserId !== null && typeof lastEditorIdForComparison === 'number' && currentLoggedInUserId === lastEditorIdForComparison) { wasEditedByMe = true; } //console.log(`[DEBUG] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`); const editedByMeText = wasEditedByMe ? ' (Editado por mí)' : ''; /* console.log(`[INFO EDITOR LUGAR] Nombre: "${originalName}" (ID: ${ currentVenueId}) | ${lastEditorInfoForLog}${editedByMeText}`);*/ // ---- FIN INFO DEL EDITOR ---- // 5. --- PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA --- let nombreSugeridoParcial = []; let sugerenciasLugar = {}; const originalWords = originalName.split(/\s+/); const processingStepLabel = document.getElementById("processingStep"); if (index === 0) { sugerenciasPorPalabra = {}; } // Definir expresiones regulares fuera del bucle para eficiencia const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/; // Sensible a mayúsculas const romanRegexStrictInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i; // Insensible originalWords.forEach((P, idx) => { const endsWithComma = restoreCommas && P.endsWith(","); const baseWord = endsWithComma ? P.slice(0, -1) : P; const cleaned = baseWord.trim(); if (cleaned === "") { nombreSugeridoParcial.push(cleaned); return; } let isExcluded = false; let matchingExcludedWord = null; if (checkExcludedWords) { matchingExcludedWord = excludedArray.find( w_excluded => removeDiacritics(w_excluded.toLowerCase()) === removeDiacritics(cleaned.toLowerCase())); isExcluded = !!matchingExcludedWord; } let tempReplaced; const isCommon = commonWords.includes(cleaned.toLowerCase()); // Usar la versión insensible para la detección inicial flexible de si ES un romano const isPotentiallyRomanNumeral = romanRegexStrictInsensitive.test(cleaned); if (isExcluded) { tempReplaced = matchingExcludedWord; // Si la palabra excluida es un número romano, asegurarse de que esté en mayúsculas if (romanRegexStrictInsensitive.test(tempReplaced)) { tempReplaced = tempReplaced.toUpperCase(); } } else { let dictionaryFormToUse = null; // Paso 1: Consultar el diccionario if (checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") { const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase()); for (const diccWord of window.dictionaryWords) { if (removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics) { // Si la palabra original es potencialmente romana, solo aceptamos del diccionario si también está en MAYÚSCULAS. if (isPotentiallyRomanNumeral) { if (diccWord === diccWord.toUpperCase() && romanRegexStrict.test(diccWord)) { // Verificar que la forma del dicc también sea un romano válido en mayus dictionaryFormToUse = diccWord; } } else { dictionaryFormToUse = diccWord; } break; } } } // Paso 2: Decidir la forma base if (dictionaryFormToUse !== null) { tempReplaced = dictionaryFormToUse; } else { // normalizePlaceName ya maneja la capitalización de romanos a MAYUS si no viene del diccionario tempReplaced = normalizePlaceName(cleaned); } // Paso 3: Ajustes finales de capitalización y forzar MAYUS para romanos if (isPotentiallyRomanNumeral) { // Si se detectó como romano (incluso si el diccionario dio algo mixto o normalizePlaceName falló), // forzar a mayúsculas. Usamos la expresión regular estricta (sensible a mayúsculas) // para validar que la forma final sea un romano correcto. const upperVersion = tempReplaced.toUpperCase(); if (romanRegexStrict.test(upperVersion)) { // Validar que la versión en mayúsculas sea un romano tempReplaced = upperVersion; } else { // Si al pasar a mayúsculas deja de ser un romano válido (raro, pero posible si cleaned era "mixToRoman"), // entonces aplicar capitalización normal. tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase(); } } else if (idx === 0) { // Primera palabra y no es romano tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1); } else { // No es la primera palabra y no es romano if (isCommon) { tempReplaced = tempReplaced.toLowerCase(); } else { // No es común, no es primera, no es romano: Capitalizar. tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1); } } } // <<< DEBUG LOG INICIAL PARA "Panaderia" / "Cafe" >>> if (cleaned.toLowerCase() === "panaderia" || cleaned.toLowerCase() === "cafe" || cleaned.toLowerCase() === "ii") { console.log(`[DEBUG WORD FINAL] Original: "${cleaned}", isExcluded: ${isExcluded}, matchingExcluded: "${matchingExcludedWord}", tempReplaced: "${tempReplaced}"`); } // Ahora, la lógica para generar las SUGERENCIAS CLICKEABLES del diccionario. if (!isExcluded && checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") { const processedWordForCompare = removeDiacritics(tempReplaced.toLowerCase()); window.dictionaryWords.forEach(diccWord => { const diccWordForCompare = removeDiacritics(diccWord.toLowerCase()); if (processedWordForCompare === diccWordForCompare) { if (diccWord !== tempReplaced) { if (!sugerenciasLugar[baseWord]) { sugerenciasLugar[baseWord] = []; } if (!sugerenciasLugar[baseWord].some(s => s.word === diccWord && s.fuente === 'dictionary')) { sugerenciasLugar[baseWord].push({ word: diccWord, similarity: 1, fuente: 'dictionary' }); } } } }); } // Sugerencias de palabras excluidas (se mantiene igual) if (checkExcludedWords) { const similarExcluded = findSimilarWords(cleaned, excludedArray, similarityThreshold) .filter(s => s.similarity < 1); if (similarExcluded.length > 0) { sugerenciasLugar[baseWord] = (sugerenciasLugar[baseWord] || []) .concat(similarExcluded.map( s => ({...s, fuente: 'excluded' }))); } } if (endsWithComma && !tempReplaced.endsWith(",")) { tempReplaced += ","; } nombreSugeridoParcial.push(tempReplaced); }); // ---- FIN PROCESAMIENTO PALABRA POR PALABRA ---- // 6. --- COMPILACIÓN DE suggestedName --- const joinedSuggested = nombreSugeridoParcial.join(' '); let processedName = joinedSuggested; if (applyGeneralReplacements) { processedName = aplicarReemplazosGenerales(processedName); } processedName = aplicarReglasEspecialesNombre(processedName); processedName = postProcessQuotesAndParentheses(processedName); if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0) { processedName = aplicarReemplazosDefinidos(processedName, replacementWords); } let suggestedName = processedName.replace(/\s{2,}/g, ' ').trim(); // Nombre base sugerido // ***** NUEVO: Eliminar punto solo si está al FINAL del nombre completo sugerido ***** if (suggestedName.endsWith('.')) { suggestedName = suggestedName.slice(0, -1); } // ***** FIN NUEVO ***** // ---- FIN COMPILACIÓN DE suggestedName ---- console.log(`[WME PLN DEBUG] Original: "${originalName}", Procesado palabra a palabra (joined): "${joinedSuggested}", Sugerido Final (antes de skip): "${suggestedName}"`); // 7. --- LÓGICA DE SALTO (SKIP) CONSOLIDADA --- const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0; let shouldSkipThisPlace = false; let skipReasonLog = ""; if (originalName.trim() === suggestedName.trim()) { shouldSkipThisPlace = true; skipReasonLog = `[SKIP EXACT MATCH] Descartado "${ originalName}" porque es idéntico al nombre sugerido "${ suggestedName}".`; } // --- INICIO: LÓGICA DE SALTO POR "OMITIR MIS EDICIONES" --- else if (chkHideMyEditsChecked && wasEditedByMe) { shouldSkipThisPlace = true; skipReasonLog = `[SKIP MY EDIT] Descartado "${ originalName}" (ID: ${ currentVenueId}) porque fue editado por mí y la opción "Omitir mis ediciones" está activa.`; } // --- FIN: LÓGICA DE SALTO POR "OMITIR MIS EDICIONES" --- else { let tempOriginalNormalized = aplicarReemplazosGenerales(originalName.trim()); tempOriginalNormalized = aplicarReglasEspecialesNombre(tempOriginalNormalized); tempOriginalNormalized = postProcessQuotesAndParentheses(tempOriginalNormalized); if (tempOriginalNormalized.endsWith('.')) { tempOriginalNormalized = tempOriginalNormalized.slice(0, -1); } tempOriginalNormalized = tempOriginalNormalized.replace(/\s{2,}/g, ' ').trim(); if (tempOriginalNormalized.toLowerCase() === suggestedName.toLowerCase() && !tieneSugerencias) { shouldSkipThisPlace = true; skipReasonLog = `[SKIP NORMALIZED] Se descartó "${ originalName}" (normalizado a "${tempOriginalNormalized}") porque ya coincide con el sugerido "${ suggestedName}" y no hay otras sugerencias.`; } } // ---- FIN LÓGICA DE SALTO --- // 8. Registrar o no en la lista de inconsistentes if (shouldSkipThisPlace) { if (skipReasonLog) console.log(skipReasonLog); } else { if (processingStepLabel) { processingStepLabel.textContent = "Registrando lugar(es) con inconsistencias..."; } let categoryNameToStore = "Sin categoría (Error)"; if (venueFromOldModel) { let venueSDKObjectForCategory = null; // Necesitaríamos obtener el venueSDKObject de nuevo // aquí si no lo guardamos o pasarlo si lo guardamos // antes. Para esta iteración, si el SDK dio el venue // antes, podríamos reusarlo, pero por simplicidad, // getPlaceCategoryName puede intentar obtenerlo si le // pasamos el ID, o solo usar W.model por ahora. // simplificar y solo pasar venueFromOldModel, // getPlaceCategoryName usará W.model. // use el SDK para categoría, getPlaceCategoryName debe // modificarse para aceptar ID y hacer la llamada async // al SDK. try { // Pasamos el venue del W.model; getPlaceCategoryName // intentará usar SDK si wmeSDK está disponible y se le pasa // el venueSDK Para esto, necesitaríamos haber guardado el // venueSDK que obtuvimos arriba. Por ahora, // getPlaceCategoryName ya intenta usar un // venueSDKObject si se le pasa. Lo obtendremos de nuevo // aquí para la categoría para mantenerlo encapsulado en // esta prueba. let venueSDKForCategory = null; if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById) { try { venueSDKForCategory = await wmeSDK.DataModel.Venues.getById( { venueId : currentVenueId }); } catch (catSDKError) { console.error("[SDK CATEGORY] Error obteniendo venueSDK para categoría:", catSDKError); } } categoryNameToStore = getPlaceCategoryName( venueFromOldModel, venueSDKForCategory); } catch (e) { console.error("Error llamando a getPlaceCategoryName desde processNextPlace:", e); } } inconsistents.push({ id : currentVenueId, original : originalName, // Mantener el original real normalized : suggestedName, // El sugerido ya procesado y sin punto final category : categoryNameToStore, editor : resolvedEditorName }); sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar; } // 9. Finalizar procesamiento del 'place' actual y pasar al siguiente updateScanProgressBar(index, places.length); index++; setTimeout(() => processNextPlace(), 0); } // ---- FIN DE LA FUNCIÓN processNextPlace ---- /* console.log("[DEBUG] ¿excludedArray es array?:", Array.isArray(excludedArray)); console.log("[DEBUG] ¿processNextPlace está definido?:", typeof processNextPlace === "function"); console.log("[DEBUG] Justo antes de llamar a processNextPlace");*/ // Inicializar procesamiento incremental try { // console.log("[DEBUG] Justo antes de llamar a processNextPlace"); setTimeout(() => { processNextPlace(); }, 10); } catch (error) { console.error("[ERROR] Fallo en processNextPlace:", error); } // Fallback automático si finalizeRender no se ejecuta scanFallbackTimeoutId = setTimeout(() => { if (!document.querySelector("#wme-place-inspector-output")?.innerHTML) { /* console.warn( "[WME PLN] Fallback activado: finalizeRender no se llamó en 30s. Mostrando panel flotante vacío."); console.error( "[WME PLN] Error: finalizeRender no se completó. Se invocó fallback tras 30 segundos de espera.");*/ createFloatingPanel("results", 0); // <--- CAMBIO AQUÍ: Llamar con "results" y 0 inconsistents si hay fallback const outputFallback = document.querySelector("#wme-place-inspector-output"); if (outputFallback) // Re-seleccionar por si acaso { outputFallback.innerHTML = "El proceso tomó demasiado tiempo o se interrumpió. No se completó el análisis."; } } }, 30000); function reapplyExcludedWordsLogic(text, excludedWordsSet) { if (typeof text !== 'string' || !excludedWordsSet || excludedWordsSet.size === 0) { return text; } const wordsInText = text.split(/\s+/); const processedWordsArray = wordsInText.map(word => { if (word === "") return ""; const wordWithoutDiacriticsLower = removeDiacritics(word.toLowerCase()); // Encontrar la palabra excluida que coincida (insensible a may/min y diacríticos) const matchingExcludedWord = Array.from(excludedWordsSet).find( w_excluded => removeDiacritics(w_excluded.toLowerCase()) === wordWithoutDiacriticsLower ); if (matchingExcludedWord) { // Si coincide, DEVOLVER LA FORMA EXACTA DE LA LISTA DE EXCLUIDAS return matchingExcludedWord; } // Si no, devolver la palabra como estaba (ya normalizada por pasos previos) return word; }); return processedWordsArray.join(' '); } // Mostrar el panel flotante solo al terminar el procesamiento // (mover esta llamada al final del procesamiento) // --- Función para finalizar renderizado una vez completado el análisis --- function finalizeRender(inconsistents, placesArr) { // alert("🟢 Entrando a finalizeRender"); // ← punto 6 // Log de depuración al entrar a finalizeRender /* console.log("[WME PLN] Finalizando render. Inconsistentes encontrados:", inconsistents.length);*/ // Limpiar el mensaje de procesamiento y spinner al finalizar el // análisis // Detener animación de puntos suspensivos si existe if (window.processingDotsInterval) { clearInterval(window.processingDotsInterval); window.processingDotsInterval = null; } // Refuerza el restablecimiento del botón de escaneo al entrar const scanBtn = document.querySelector("button[type='button']"); if (scanBtn) { scanBtn.textContent = "Start Scan..."; scanBtn.disabled = false; scanBtn.style.opacity = "1"; scanBtn.style.cursor = "pointer"; } const output = document.querySelector("#wme-place-inspector-output"); if (!output) { console.error("❌ No se pudo montar el panel flotante. Revisar estructura del DOM."); alert("Hubo un problema al mostrar los resultados. Intenta recargar la página."); return; } // Esta llamada se hace ANTES de limpiar el output. // El primer argumento es el estado, el segundo es el número de inconsistencias. createFloatingPanel("results", inconsistents.length); // <--- CAMBIO AQUÍ: Llamar con "results" if (output) // Ya no necesitamos output.innerHTML = ""; porque createFloatingPanel lo hace { // output.innerHTML = ""; // Limpiar el mensaje de procesamiento y spinner // ESTA LÍNEA SE ELIMINA O COMENTA // Mostrar el panel flotante al terminar el procesamiento // createFloatingPanel(inconsistents.length); // ESTA LÍNEA SE ELIMINA O COMENTA, YA SE HIZO ARRIBA } // Limitar a 30 resultados y mostrar advertencia si excede const maxRenderLimit = 30; const totalInconsistents = inconsistents.length; if (totalInconsistents > maxRenderLimit) { inconsistents = inconsistents.slice(0, maxRenderLimit); if (!sessionStorage.getItem("popupShown")) { const modalLimit = document.createElement("div"); // Renombrado a modalLimit para claridad modalLimit.style.position = "fixed"; modalLimit.style.top = "50%"; modalLimit.style.left = "50%"; modalLimit.style.transform = "translate(-50%, -50%)"; modalLimit.style.background = "#fff"; modalLimit.style.border = "1px solid #ccc"; modalLimit.style.padding = "20px"; modalLimit.style.zIndex = "10007"; // <<<<<<< Z-INDEX AUMENTADO modalLimit.style.width = "400px"; modalLimit.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)"; modalLimit.style.borderRadius = "8px"; modalLimit.style.fontFamily = "sans-serif"; // Fondo suave azul y mejor presentación modalLimit.style.backgroundColor = "#f0f8ff"; modalLimit.style.border = "1px solid #aad"; modalLimit.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)"; // --- Insertar ícono visual de información arriba del mensaje --- const iconInfo = document.createElement("div"); // Renombrado iconInfo.innerHTML = "ℹ️"; iconInfo.style.fontSize = "24px"; iconInfo.style.marginBottom = "10px"; modalLimit.appendChild(iconInfo); const message = document.createElement("p"); message.innerHTML = `Se encontraron <strong>${ totalInconsistents}</strong> lugares con nombres no normalizados.<br><br>Solo se mostrarán los primeros <strong>${ maxRenderLimit}</strong>.<br><br>Una vez corrijas estos, presiona nuevamente <strong>'Start Scan...'</strong> para continuar con el análisis del resto.`; message.style.marginBottom = "20px"; modalLimit.appendChild(message); const acceptBtn = document.createElement("button"); acceptBtn.textContent = "Aceptar"; acceptBtn.style.padding = "6px 12px"; acceptBtn.style.cursor = "pointer"; acceptBtn.style.backgroundColor = "#007bff"; acceptBtn.style.color = "#fff"; acceptBtn.style.border = "none"; acceptBtn.style.borderRadius = "4px"; acceptBtn.addEventListener("click", () => { sessionStorage.setItem("popupShown", "true"); modalLimit.remove(); }); modalLimit.appendChild(acceptBtn); document.body.appendChild(modalLimit); // Se añade al body, así que el z-index debería funcionar globalmente } } // Si no hay inconsistencias, mostrar mensaje y salir (progreso visible) if (inconsistents.length === 0) { output.appendChild(document.createTextNode( "Todos los nombres de lugares visibles están correctamente normalizados.")); // Mensaje visual de análisis finalizado sin inconsistencias const checkIcon = document.createElement("div"); checkIcon.innerHTML = "✔ Análisis finalizado sin inconsistencias."; checkIcon.style.marginTop = "10px"; checkIcon.style.fontSize = "14px"; checkIcon.style.color = "green"; output.appendChild(checkIcon); // Mensaje visual adicional solicitado const successMsg = document.createElement("div"); successMsg.textContent = "Todos los nombres están correctamente normalizados."; successMsg.style.marginTop = "10px"; successMsg.style.fontSize = "14px"; successMsg.style.color = "green"; successMsg.style.fontWeight = "bold"; output.appendChild(successMsg); const existingOverlay = document.getElementById("scanSpinnerOverlay"); if (existingOverlay) existingOverlay.remove(); // Actualizar barra de progreso 100% const progressBarInnerTab = document.getElementById("progressBarInnerTab"); const progressBarTextTab = document.getElementById("progressBarTextTab"); if (progressBarInnerTab && progressBarTextTab) { progressBarInnerTab.style.width = "100%"; progressBarTextTab.textContent = `Progreso: 100% (${placesArr.length}/${placesArr.length})`; } // Mensaje adicional en el tab principal (pestaña) const outputTab = document.getElementById("wme-normalization-tab-output"); if (outputTab) { outputTab.innerHTML = `✔ Todos los nombres están normalizados. Se analizaron ${ placesArr.length} lugares.`; outputTab.style.color = "green"; outputTab.style.fontWeight = "bold"; } // Restaurar el texto y estado del botón de escaneo const scanBtn = document.querySelector("button[type='button']"); if (scanBtn) { scanBtn.textContent = "Start Scan..."; scanBtn.disabled = false; scanBtn.style.opacity = "1"; scanBtn.style.cursor = "pointer"; // Agregar check verde al lado del botón al finalizar sin // errores const iconCheck = document.createElement("span"); iconCheck.textContent = " ✔"; iconCheck.style.marginLeft = "8px"; iconCheck.style.color = "green"; scanBtn.appendChild(iconCheck); } return; } // Mostrar spinner solo si hay inconsistencias a procesar // showLoadingSpinner(); const table = document.createElement("table"); table.style.width = "100%"; table.style.borderCollapse = "collapse"; table.style.fontSize = "12px"; const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); [ "Permalink", "Tipo", "Categoría", "Editor", "Nombre Actual", "Nombre Sugerido", "Sugerencias de reemplazo", "Acción" ].forEach(header => { const th = document.createElement("th"); th.textContent = header; th.style.borderBottom = "1px solid #ccc"; th.style.padding = "4px"; th.style.textAlign = "left"; headerRow.appendChild(th); }); thead.style.position = "sticky"; thead.style.top = "0"; thead.style.background = "#f1f1f1"; thead.style.zIndex = "10"; // z-index de la cabecera de la tabla headerRow.style.backgroundColor = "#003366"; headerRow.style.color = "#ffffff"; thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement("tbody"); inconsistents.forEach(({ id, original, normalized, category, editor }, index) => { // Actualizar barra de progreso visual EN EL TAB PRINCIPAL const progressPercent = Math.floor(((index + 1) / inconsistents.length) * 100); // Actualiza barra de progreso en el tab principal const progressBarInnerTab = document.getElementById("progressBarInnerTab"); const progressBarTextTab = document.getElementById("progressBarTextTab"); if (progressBarInnerTab && progressBarTextTab) { progressBarInnerTab.style.width = `${progressPercent}%`; progressBarTextTab.textContent = `Progreso: ${ progressPercent}% (${index + 1}/${inconsistents.length})`; } const row = document.createElement("tr"); const permalinkCell = document.createElement("td"); const link = document.createElement("a"); link.href = "#"; // Reemplazado onclick por addEventListener para mejor // compatibilidad y centrado de mapa link.addEventListener("click", (e) => { e.preventDefault(); const venue = W.model.venues.getObjectById(id); if (!venue) return; // Centrar mapa y seleccionar el lugar const geometry = venue.getGeometry(); if (geometry && geometry.getCentroid) { const center = geometry.getCentroid(); W.map.setCenter(center, null, false, 0); } if (W.selectionManager && typeof W.selectionManager.select === "function") { W.selectionManager.select(venue); } else if (W.selectionManager && typeof W.selectionManager.setSelectedModels === "function") { W.selectionManager.setSelectedModels([ venue ]); } }); link.title = "Abrir en panel lateral"; link.textContent = "🔗"; permalinkCell.appendChild(link); permalinkCell.style.padding = "4px"; permalinkCell.style.width = "65px"; row.appendChild(permalinkCell); // Columna Tipo de place const venue = W.model.venues.getObjectById(id); const { icon : typeIcon, title : typeTitle } = getPlaceTypeInfo(venue); const typeCell = document.createElement("td"); typeCell.textContent = typeIcon; typeCell.title = `Lugar tipo ${typeTitle}`; typeCell.style.padding = "4px"; typeCell.style.width = "65px"; row.appendChild(typeCell); // Columna Categoría del place const categoryCell = document.createElement("td"); const categoryName = getPlaceCategoryName(venue); categoryCell.textContent = categoryName; categoryCell.title = `Categoría: ${categoryName}`; categoryCell.style.padding = "4px"; categoryCell.style.width = "130px"; row.appendChild(categoryCell); // Columna Editor (username) const editorCell = document.createElement("td"); editorCell.textContent = editor || "Desconocido"; // Use the stored editor name editorCell.title = "Último editor"; editorCell.style.padding = "4px"; editorCell.style.width = "140px"; row.appendChild(editorCell); const originalCell = document.createElement("td"); const inputOriginal = document.createElement("input"); inputOriginal.type = "text"; const venueLive = W.model.venues.getObjectById(id); const currentLiveName = venueLive?.attributes?.name?.value || venueLive?.attributes?.name || ""; inputOriginal.value = currentLiveName; // --- Resaltar en rojo si hay diferencia con el sugerido --- if (currentLiveName.trim().toLowerCase() !== normalized.trim().toLowerCase()) { inputOriginal.style.border = "1px solid red"; inputOriginal.title = "Este nombre difiere del original mostrado en el panel"; } inputOriginal.disabled = true; inputOriginal.style.width = "270px"; inputOriginal.style.backgroundColor = "#eee"; originalCell.appendChild(inputOriginal); originalCell.style.padding = "4px"; originalCell.style.width = "270px"; row.appendChild(originalCell); const suggestionCell = document.createElement("td"); // Nueva columna: sugerencia de reemplazo seleccionada const suggestionListCell = document.createElement("td"); suggestionListCell.style.padding = "4px"; suggestionListCell.style.fontSize = "11px"; suggestionListCell.style.color = "#333"; // Permitir múltiples líneas en la celda de sugerencias suggestionListCell.style.whiteSpace = "pre-wrap"; suggestionListCell.style.wordBreak = "break-word"; suggestionListCell.style.width = "270px"; // Calcular sugerencias similares de especiales aquí // --- Nueva lógica para separar sugerencias por fuente --- const allSuggestions = sugerenciasPorPalabra?.[id] || {}; const similarList = {}; const similarDictList = {}; Object.entries(allSuggestions) .forEach(([ originalWord, suggestions ]) => { suggestions.forEach(s => { if (s.fuente === 'excluded') { if (!similarList[originalWord]) similarList[originalWord] = []; similarList[originalWord].push(s); } else if (s.fuente === 'dictionary') { if (!similarDictList[originalWord]) similarDictList[originalWord] = []; similarDictList[originalWord].push(s); } }); }); // --- NUEVA LÓGICA: aplicar reemplazos automáticos y solo mostrar // sugerencias < 1 --- let autoApplied = false; let localNormalized = normalized; // 1. Procesar sugerencias de especiales (similarList) let hasExcludedSuggestionsToShow = false; if (similarList && Object.keys(similarList).length > 0) { Object.entries(similarList) .forEach(([ originalWord, suggestions ]) => { suggestions.forEach(s => { if (s.similarity === 1) { // Si encontramos una sugerencia 100%, ya fue // aplicada en el pipeline principal. autoApplied = true; // NO mostrar sugerencia clickable ni texto en // columna de sugerencias. } else if (s.similarity < 1) { hasExcludedSuggestionsToShow = true; } }); }); } // 2. Procesar sugerencias de diccionario (similarDictList) let hasDictSuggestionsToShow = false; if (similarDictList && Object.keys(similarDictList).length > 0) { Object.entries(similarDictList) .forEach(([ originalWord, suggestions ]) => { suggestions.forEach(s => { if (s.similarity < 1) { hasDictSuggestionsToShow = true; } }); }); } // --- EVITAR DUPLICADOS DE SUGERENCIAS ENTRE "ESPECIALES" Y // "DICCIONARIO" --- Crear set de palabras ya procesadas en // sugerencias especiales const palabrasYaProcesadas = new Set(); if (similarList && Object.keys(similarList).length > 0) { Object.keys(similarList) .forEach(palabra => palabrasYaProcesadas.add(palabra.toLowerCase())); } // Render input de sugerencia const inputReplacement = document.createElement("input"); inputReplacement.type = "text"; let mainSuggestion = localNormalized; inputReplacement.value = mainSuggestion; inputReplacement.style.width = "270px"; // Visual cue if change was due to excluded word if (localNormalized !== normalized) { inputReplacement.style.backgroundColor = "#fff3cd"; // color amarillo claro inputReplacement.title = "Contiene palabra excluida reemplazada"; } else if (autoApplied) { inputReplacement.style.backgroundColor = "#c8e6c9"; // verde claro inputReplacement.title = "Reemplazo automático aplicado (100% similitud)"; } else if (normalized !== inputReplacement.value) { inputReplacement.style.backgroundColor = "#e6f7ff"; // Azul claro para cambios automáticos del // diccionario (≥ 90%) inputReplacement.title = "Cambio automático basado en diccionario (≥ 90%)"; } else { inputReplacement.title = "Nombre normalizado"; } // NUEVA LÓGICA: marcar si contiene palabra sugerida por el // diccionario const palabrasDelDiccionario = new Set(); Object.values(similarDictList).forEach(arr => { arr.forEach(s => { if (mainSuggestion.toLowerCase().includes( s.word.toLowerCase())) { palabrasDelDiccionario.add(s.word.toLowerCase()); } }); }); if (palabrasDelDiccionario.size > 0) { inputReplacement.style.backgroundColor = "#cce5ff"; // Azul claro inputReplacement.title = "Contiene sugerencias del diccionario aplicadas manualmente"; } suggestionCell.appendChild(inputReplacement); suggestionCell.style.padding = "4px"; suggestionCell.style.width = "270px"; // --- Función debounce --- function debounce(func, delay) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } // --- Activar/desactivar el botón Aplicar según si hay cambios --- inputReplacement.addEventListener('input', debounce(() => { if (inputReplacement.value.trim() !== original) { applyButton.disabled = false; applyButton.style.color = ""; } else { applyButton.disabled = true; applyButton.style.color = "#bbb"; } }, 300)); // --- Listener para inputOriginal con debounce (puede personalizarse la lógica) --- inputOriginal.addEventListener('input', debounce(() => { // Opcional: alguna lógica si se desea manejar cambios en inputOriginal }, 300)); // Renderizar solo sugerencias < 1 en sugerencias de reemplazo // especiales primero if (similarList && Object.keys(similarList).length > 0) { Object.entries(similarList).forEach(([originalWord, suggestions]) => { suggestions.forEach(s => { // 's' aquí es { word: candidate, similarity: sim, fuente: 'excluded' } // Mostrar todas las sugerencias < 100% de similitud, // o incluso las de 100% si la forma en la lista de excluidas es diferente (ej. mayúsculas/minúsculas) // a la originalWord. // La condición original s.similarity < 1 es buena para evitar sugerir lo mismo que ya está. if (s.similarity < 1 || (s.similarity === 1 && originalWord !== s.word) ) { const suggestionDiv = document.createElement("div"); const icono = "🏷️"; // Icono para palabras especiales/excluidas suggestionDiv.textContent = `${icono} ¿"${originalWord}" por "${s.word}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`; suggestionDiv.style.cursor = "pointer"; suggestionDiv.style.padding = "2px 4px"; suggestionDiv.style.margin = "2px 0"; suggestionDiv.style.border = "1px solid #ddd"; suggestionDiv.style.backgroundColor = "#f3f9ff"; // Color distintivo si quieres suggestionDiv.addEventListener("click", () => { const currentSuggestedValue = inputReplacement.value; // Valor actual del campo "Nombre Sugerido" // console.log("--- DEBUG: Clic en Sugerencia ESPECIAL ---"); // console.log("Palabra Original (base de la sugerencia):", originalWord); // console.log("Sugerencia (palabra a usar de lista Especiales):", s.word); // 1. Normalizamos la 'originalWord' para saber qué forma buscar const normalizedOriginalWord = normalizePlaceName(originalWord); //console.log("Forma Normalizada de Palabra Original (para buscar):", normalizedOriginalWord); // 2. Normalizamos la palabra sugerida 's.word' de la lista de Especiales const wordToInsert = normalizePlaceName(s.word); //console.log("Palabra Normalizada para Insertar:", wordToInsert); //console.log("Valor ACTUAL del campo 'Nombre Sugerido':", currentSuggestedValue); // 3. Creamos la regex para buscar la 'normalizedOriginalWord' const searchRegex = new RegExp("\\b" + escapeRegExp(normalizedOriginalWord) + "\\b", "gi"); //console.log("Regex para buscar en 'Nombre Sugerido':", searchRegex.toString()); if (!searchRegex.test(currentSuggestedValue)) { console.warn("¡ADVERTENCIA ESPECIAL! La forma normalizada de la palabra original ('" + normalizedOriginalWord + "') no se encontró en el 'Nombre Sugerido' actual ('" + currentSuggestedValue + "'). No se hará reemplazo."); } const newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert); //console.log("Valor DESPUÉS de .replace() (Especial):", newSuggestedValue); if (currentSuggestedValue !== newSuggestedValue) { inputReplacement.value = newSuggestedValue; // console.log("Campo 'Nombre Sugerido' ACTUALIZADO (Especial) a:", newSuggestedValue); } else { console.log("No hubo cambios en 'Nombre Sugerido' (Especial) (el nuevo valor es idéntico al anterior o la palabra a reemplazar no se encontró/ya era igual)."); } inputReplacement.dispatchEvent(new Event("input")); // console.log("--- FIN DEBUG (Especial) ---"); }); suggestionListCell.appendChild(suggestionDiv); } }); }); } // Diccionario después, evitando duplicados de palabras ya sugeridas // en especiales if (similarDictList && Object.keys(similarDictList).length > 0) { Object.entries(similarDictList) .forEach(([ originalWord, suggestions ]) => { // originalWord es la palabra del nombre del lugar ANTES de normalizePlaceName (la que fue clave en sugerenciasLugar) if (palabrasYaProcesadas.has(originalWord.toLowerCase())) return; suggestions.forEach(s => { // s es { word: diccWordDelDiccionario, similarity: sim, fuente: 'dictionary' } const normalizedOriginalWordForDisplay = normalizePlaceName(originalWord); const normalizedDictionaryWordToApply = normalizePlaceName(s.word); // Condición A: Si la aplicación de la sugerencia del diccionario // resulta en el nombre COMPLETO que ya está sugerido (localNormalized), Y la palabra original // (normalizada) también era igual a ese nombre completo, entonces la palabra es única y ya está perfecta. // Principalmente para nombres de una sola palabra. if (normalizedDictionaryWordToApply === localNormalized && normalizedOriginalWordForDisplay === localNormalized) { return; } // Condición B: Si la palabra original (normalizada para mostrar) es idéntica // a la palabra del diccionario (normalizada para aplicar), entonces la sugerencia // sería del tipo "¿X por X?", lo cual es inútil y no ofrece ningún cambio para esa palabra específica. if (normalizedOriginalWordForDisplay === normalizedDictionaryWordToApply) { return; } // Si hemos pasado ambas condiciones, significa que la sugerencia ofrece un cambio útil. const suggestionItem = document.createElement("div"); const icono = "📘"; suggestionItem.textContent = `${icono} ¿"${normalizedOriginalWordForDisplay}" por "${normalizedDictionaryWordToApply}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`; suggestionItem.style.cursor = "pointer"; suggestionItem.style.padding = "2px 4px"; suggestionItem.style.margin = "2px 0"; suggestionItem.style.border = "1px solid #ddd"; suggestionItem.style.backgroundColor = "#f9f9f9"; suggestionItem.addEventListener("click", () => { const currentSuggestedValue = inputReplacement.value; const wordToInsert = normalizedDictionaryWordToApply; const wordToSearchAndReplace = normalizedOriginalWordForDisplay; // Usar la forma normalizada de la palabra original para la búsqueda const searchRegex = new RegExp("\\b" + escapeRegExp(wordToSearchAndReplace) + "\\b", "gi"); let newSuggestedValue = currentSuggestedValue; if (searchRegex.test(currentSuggestedValue)) { newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert); } else { console.warn(`¡ADVERTENCIA! La palabra a reemplazar ('${wordToSearchAndReplace}') no se encontró en el 'Nombre Sugerido' actual ('${currentSuggestedValue}').`); } if (inputReplacement.value !== newSuggestedValue) { inputReplacement.value = newSuggestedValue; } else { console.log("No hubo cambios efectivos en 'Nombre Sugerido' o la palabra a reemplazar no se encontró."); } inputReplacement.dispatchEvent(new Event("input")); }); suggestionListCell.appendChild(suggestionItem); }); }); } row.appendChild(suggestionCell); row.appendChild(suggestionListCell); const actionCell = document.createElement("td"); actionCell.style.padding = "4px"; actionCell.style.width = "120px"; const buttonGroup = document.createElement("div"); buttonGroup.style.display = "flex"; buttonGroup.style.gap = "4px"; const applyButton = document.createElement("button"); applyButton.textContent = "✔"; applyButton.title = "Aplicar sugerencia"; applyButton.style.padding = "4px 8px"; applyButton.style.cursor = "pointer"; const deleteButton = document.createElement("button"); deleteButton.textContent = "💣"; deleteButton.title = "Eliminar lugar"; deleteButton.style.padding = "4px 8px"; deleteButton.style.cursor = "pointer"; applyButton.relatedDelete = deleteButton; deleteButton.relatedApply = applyButton; applyButton.addEventListener("click", () => { const venue = W.model.venues.getObjectById(id); if (!venue) { alert( "Error: El lugar no está disponible o ya fue eliminado."); return; } const newName = inputReplacement.value.trim(); try { const UpdateObject = require("Waze/Action/UpdateObject"); const action = new UpdateObject(venue, { name : newName }); W.model.actionManager.add(action); applyButton.disabled = true; applyButton.style.color = "#bbb"; applyButton.style.opacity = "0.5"; if (applyButton.relatedDelete) { applyButton.relatedDelete.disabled = true; applyButton.relatedDelete.style.color = "#bbb"; applyButton.relatedDelete.style.opacity = "0.5"; } const successIcon = document.createElement("span"); successIcon.textContent = " ✅"; successIcon.style.marginLeft = "5px"; applyButton.parentElement.appendChild(successIcon); } catch (e) { alert("Error al actualizar: " + e.message); } }); // Listener para el botón de eliminar deleteButton.addEventListener("click", () => { // Modal bonito de confirmación const confirmModal = document.createElement("div"); confirmModal.style.position = "fixed"; confirmModal.style.top = "50%"; confirmModal.style.left = "50%"; confirmModal.style.transform = "translate(-50%, -50%)"; confirmModal.style.background = "#fff"; confirmModal.style.border = "1px solid #aad"; confirmModal.style.padding = "28px 32px 20px 32px"; confirmModal.style.zIndex = "20000"; // Z-INDEX AUMENTADO confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)"; confirmModal.style.fontFamily = "sans-serif"; confirmModal.style.borderRadius = "10px"; confirmModal.style.textAlign = "center"; confirmModal.style.minWidth = "340px"; // Ícono visual const iconElement = document.createElement("div"); iconElement.innerHTML = "⚠️"; iconElement.style.fontSize = "38px"; iconElement.style.marginBottom = "10px"; confirmModal.appendChild(iconElement); // Mensaje principal const message = document.createElement("div"); const venue = W.model.venues.getObjectById(id); const placeName = venue?.attributes?.name?.value || venue?.attributes?.name || "este lugar"; message.innerHTML = `<b>¿Eliminar "${placeName}"?</b>`; // CORREGIDO para mostrar el nombre del lugar message.style.fontSize = "18px"; message.style.marginBottom = "8px"; confirmModal.appendChild(message); // Nombre del lugar (puede ser redundante si ya está en el mensaje, pero se mantiene por si acaso) const nameDiv = document.createElement("div"); nameDiv.textContent = `"${placeName}"`; nameDiv.style.fontSize = "15px"; nameDiv.style.color = "#007bff"; nameDiv.style.marginBottom = "18px"; confirmModal.appendChild(nameDiv); // Botones const buttonWrapper = document.createElement("div"); buttonWrapper.style.display = "flex"; buttonWrapper.style.justifyContent = "center"; buttonWrapper.style.gap = "18px"; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancelar"; cancelBtn.style.padding = "7px 18px"; cancelBtn.style.background = "#eee"; cancelBtn.style.border = "none"; cancelBtn.style.borderRadius = "4px"; cancelBtn.style.cursor = "pointer"; cancelBtn.addEventListener("click", () => confirmModal.remove()); const confirmBtn = document.createElement("button"); confirmBtn.textContent = "Eliminar"; confirmBtn.style.padding = "7px 18px"; confirmBtn.style.background = "#d9534f"; confirmBtn.style.color = "#fff"; confirmBtn.style.border = "none"; confirmBtn.style.borderRadius = "4px"; confirmBtn.style.cursor = "pointer"; confirmBtn.style.fontWeight = "bold"; confirmBtn.addEventListener("click", () => { const venue = W.model.venues.getObjectById(id); if (!venue) { alert("El lugar no está disponible o ya fue eliminado."); confirmModal.remove(); return; } try { const DeleteObject = require("Waze/Action/DeleteObject"); const action = new DeleteObject(venue); W.model.actionManager.add(action); deleteButton.disabled = true; deleteButton.style.color = "#bbb"; deleteButton.style.opacity = "0.5"; if (deleteButton.relatedApply) { deleteButton.relatedApply.disabled = true; deleteButton.relatedApply.style.color = "#bbb"; deleteButton.relatedApply.style.opacity = "0.5"; } const successIcon = document.createElement("span"); successIcon.textContent = " 🗑️"; successIcon.style.marginLeft = "5px"; deleteButton.parentElement.appendChild(successIcon); } catch (e) { alert("Error al eliminar: " + e.message); } confirmModal.remove(); }); buttonWrapper.appendChild(cancelBtn); buttonWrapper.appendChild(confirmBtn); confirmModal.appendChild(buttonWrapper); document.body.appendChild(confirmModal); }); buttonGroup.appendChild(applyButton); buttonGroup.appendChild(deleteButton); const addToExclusionBtn = document.createElement("button"); addToExclusionBtn.textContent = "🏷️"; addToExclusionBtn.title = "Marcar palabra como especial (no se modifica)"; addToExclusionBtn.style.padding = "4px 6px"; addToExclusionBtn.addEventListener("click", () => { const words = original.split(/\s+/); const modal = document.createElement("div"); modal.style.position = "fixed"; modal.style.top = "50%"; modal.style.left = "50%"; modal.style.transform = "translate(-50%, -50%)"; modal.style.background = "#fff"; modal.style.border = "1px solid #ccc"; modal.style.padding = "10px"; modal.style.zIndex = "20000"; // Z-INDEX AUMENTADO modal.style.maxWidth = "300px"; const title = document.createElement("h4"); title.textContent = "Agregar palabra a especiales"; modal.appendChild(title); const list = document.createElement("ul"); list.style.listStyle = "none"; list.style.padding = "0"; words.forEach(w => { // --- Filtro: palabras vacías, comunes, o ya existentes // (ignorar mayúsculas) --- if (w.trim() === '') return; const lowerW = w.trim().toLowerCase(); /* const commonWords = [ 'de', 'del', 'el', 'la', 'los', 'las', 'y', 'e', 'o', 'u', 'un', 'una', 'y', 'unos', 'unas', 'a', 'en', 'con', 'sin', 'sobre', 'tras', 'por' ];*/ const alreadyExists = Array.from(excludedWords) .some(existing => existing.toLowerCase() === lowerW); if (commonWords.includes(lowerW) || alreadyExists) return; const li = document.createElement("li"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = w; checkbox.id = `cb-exc-${w.replace(/[^a-zA-Z0-9]/g, "")}`; li.appendChild(checkbox); const label = document.createElement("label"); label.htmlFor = checkbox.id; label.appendChild(document.createTextNode(" " + w)); li.appendChild(label); list.appendChild(li); }); modal.appendChild(list); const confirmBtn = document.createElement("button"); confirmBtn.textContent = "Añadir Seleccionadas"; confirmBtn.addEventListener("click", () => { const checked = modal.querySelectorAll("input[type=checkbox]:checked"); let wordsActuallyAdded = false; // Para saber si se añadió algo nuevo checked.forEach(c => { // Antes de añadir, podrías verificar si ya existe para evitar // trabajo innecesario, aunque un Set maneja duplicados. // También, considera si quieres aplicar isValidExcludedWord aquí, // aunque usualmente las palabras del nombre del lugar son candidatas directas. if (!excludedWords.has(c.value)) { excludedWords.add(c.value); wordsActuallyAdded = true; } }); if (wordsActuallyAdded) { // Llama a renderExcludedWordsList para actualizar la UI en la pestaña "Especiales" // y para guardar en localStorage if (typeof renderExcludedWordsList === 'function') { // Es mejor pasar el elemento si se puede, o dejar que la función lo encuentre const excludedListElement = document.getElementById("excludedWordsList"); if (excludedListElement) { renderExcludedWordsList(excludedListElement); } else { renderExcludedWordsList(); // Fallback } } } modal.remove(); }); modal.appendChild(confirmBtn); const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancelar"; cancelBtn.style.marginLeft = "8px"; cancelBtn.addEventListener("click", () => modal.remove()); modal.appendChild(cancelBtn); document.body.appendChild(modal); }); buttonGroup.appendChild(addToExclusionBtn); // buttonGroup.appendChild(addToDictionaryBtn); actionCell.appendChild(buttonGroup); row.appendChild(actionCell); // Añadir borde inferior visible entre cada lugar row.style.borderBottom = "1px solid #ddd"; row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff"; tbody.appendChild(row); // Actualizar progreso al final del ciclo usando setTimeout para // liberar el hilo visual setTimeout(() => { const progress = Math.floor(((index + 1) / inconsistents.length) * 100); const progressElem = document.getElementById("scanProgressText"); if (progressElem) { progressElem.textContent = `Analizando lugares: ${ progress}% (${index + 1}/${inconsistents.length})`; } }, 0); }); table.appendChild(tbody); output.appendChild(table); // Log de cierre // console.log("✔ Panel finalizado y tabla renderizada."); // Quitar overlay spinner justo antes de mostrar la tabla const existingOverlay = document.getElementById("scanSpinnerOverlay"); if (existingOverlay) { existingOverlay.remove(); } // Al finalizar, actualizar el texto final en el tab principal (progreso // 100%) const progressBarInnerTab = document.getElementById("progressBarInnerTab"); const progressBarTextTab = document.getElementById("progressBarTextTab"); if (progressBarInnerTab && progressBarTextTab) { progressBarInnerTab.style.width = "100%"; progressBarTextTab.textContent = `Progreso: 100% (${inconsistents.length}/${placesArr.length})`; } // Función para reactivar todos los botones de acción en el panel // flotante function reactivateAllActionButtons() { document.querySelectorAll("#wme-place-inspector-output button") .forEach(btn => { btn.disabled = false; btn.style.color = ""; btn.style.opacity = ""; }); } W.model.actionManager.events.register("afterundoaction", null, () => { waitForWazeAPI(() => { const places = getVisiblePlaces(); renderPlacesInFloatingPanel(places); setTimeout(reactivateAllActionButtons, 250); // Esperar a que el panel se reconstruya }); }); W.model.actionManager.events.register("afterredoaction", null, () => { waitForWazeAPI(() => { const places = getVisiblePlaces(); renderPlacesInFloatingPanel(places); setTimeout(reactivateAllActionButtons, 250); }); }); // Mostrar el panel flotante al terminar el procesamiento // createFloatingPanel(inconsistents.length); // Ahora se invoca arriba // si output existe } } function getLevenshteinDistance(a, b) { const matrix = Array.from( { length : b.length + 1 }, (_, i) => Array.from({ length : a.length + 1 }, (_, j) => (i === 0 ? j : (j === 0 ? i : 0)))); for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j] + 1, // deletion matrix[i][j - 1] + 1, // insertion matrix[i - 1][j - 1] + 1 // substitution ); } } } return matrix[b.length][a.length]; } function calculateSimilarity(word1, word2) { const distance = getLevenshteinDistance(word1.toLowerCase(), word2.toLowerCase()); const maxLen = Math.max(word1.length, word2.length); return 1 - distance / maxLen; } function findSimilarWords(word, excludedWords, threshold) { const userThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "85") / 100; const lowerWord = word.toLowerCase(); // excludedWords is now always an array const firstChar = lowerWord.charAt(0); let candidates = excludedWords; if (typeof excludedWords === 'object' && !Array.isArray(excludedWords)) { // Estamos usando el índice candidates = excludedWords[firstChar] || []; } return candidates .map(candidate => { const similarity = calculateSimilarity(lowerWord, candidate.toLowerCase()); return { word : candidate, similarity }; }) .filter(item => item.similarity >= threshold) .sort((a, b) => b.similarity - a.similarity); } function suggestExcludedReplacements(currentName, excludedWords) { const words = currentName.split(/\s+/); const suggestions = {}; const threshold = parseFloat(document.getElementById("similarityThreshold")?.value || "85") / 100; words.forEach(word => { const similar = findSimilarWords(word, Array.from(excludedWords), threshold); if (similar.length > 0) { suggestions[word] = similar; } }); return suggestions; } // Reset del inspector: progreso y texto de tab function resetInspectorState() { const inner = document.getElementById("progressBarInnerTab"); const text = document.getElementById("progressBarTextTab"); const outputTab = document.getElementById("wme-normalization-tab-output"); if (inner) inner.style.width = "0%"; if (text) text.textContent = `Progreso: 0% (0/0)`; if (outputTab) outputTab.textContent = "Presiona 'Start Scan...' para analizar los lugares visibles."; } ///*************************************************************************** // Nombre: createFloatingPanel // Autor: mincho77 // Fecha: 2025-05-26 // Actualiza la fecha si es necesario // Descripción: Crea el panel flotante con dimensiones y títulos correctos, y ajusta la posición del panel de resultados. //*************************************************************************** function createFloatingPanel(status = "processing", numInconsistents = 0) { if (!floatingPanelElement) { floatingPanelElement = document.createElement("div"); floatingPanelElement.id = "wme-place-inspector-panel"; floatingPanelElement.style.position = "fixed"; floatingPanelElement.style.zIndex = "10005"; // Z-INDEX DEL PANEL DE RESULTADOS floatingPanelElement.style.background = "#fff"; floatingPanelElement.style.border = "1px solid #ccc"; floatingPanelElement.style.borderRadius = "8px"; floatingPanelElement.style.boxShadow = "0 5px 15px rgba(0,0,0,0.2)"; floatingPanelElement.style.padding = "10px"; floatingPanelElement.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif"; floatingPanelElement.style.display = 'none'; floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s"; // Agregado left y top a la transición floatingPanelElement.style.overflow = "hidden"; // ESTA LÍNEA const closeBtn = document.createElement("span"); closeBtn.textContent = "×"; closeBtn.style.position = "absolute"; closeBtn.style.top = "8px"; closeBtn.style.right = "12px"; closeBtn.style.cursor = "pointer"; closeBtn.style.fontSize = "22px"; closeBtn.style.color = "#555"; closeBtn.title = "Cerrar panel"; closeBtn.addEventListener("click", () => { if (floatingPanelElement) floatingPanelElement.style.display = 'none'; resetInspectorState(); }); floatingPanelElement.appendChild(closeBtn); const titleElement = document.createElement("h4"); titleElement.id = "wme-pln-panel-title"; titleElement.style.marginTop = "0"; titleElement.style.marginBottom = "10px"; titleElement.style.fontSize = "20px"; titleElement.style.color = "#333"; titleElement.style.textAlign = "center"; titleElement.style.fontWeight = "bold"; floatingPanelElement.appendChild(titleElement); const outputDivLocal = document.createElement("div"); outputDivLocal.id = "wme-place-inspector-output"; outputDivLocal.style.fontSize = "14px"; outputDivLocal.style.backgroundColor = "#fdfdfd"; outputDivLocal.style.overflowY = "auto"; // Y ESTA floatingPanelElement.appendChild(outputDivLocal); document.body.appendChild(floatingPanelElement); } const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title"); const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output"); if(outputDiv) outputDiv.innerHTML = ""; if (status === "processing") { floatingPanelElement.style.width = processingPanelDimensions.width; floatingPanelElement.style.height = processingPanelDimensions.height; if(outputDiv) outputDiv.style.height = "150px"; if(titleElement) titleElement.textContent = "Buscando..."; if(outputDiv) { outputDiv.innerHTML = "<div style='display:flex; align-items:center; justify-content:center; height:100%;'><span class='loader-spinner' style='width:32px; height:32px; border:4px solid #ccc; border-top:4px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span></div>"; } // Centrar el panel de procesamiento floatingPanelElement.style.top = "50%"; floatingPanelElement.style.left = "50%"; floatingPanelElement.style.transform = "translate(-50%, -50%)"; } else { // status === "results" floatingPanelElement.style.width = resultsPanelDimensions.width; floatingPanelElement.style.height = resultsPanelDimensions.height; if(outputDiv) outputDiv.style.height = "660px"; if(titleElement) titleElement.textContent = "Resultado de la búsqueda"; // Mover el panel de resultados más a la derecha floatingPanelElement.style.top = "50%"; floatingPanelElement.style.left = "60%"; floatingPanelElement.style.transform = "translate(-50%, -50%)"; } floatingPanelElement.style.display = 'flex'; floatingPanelElement.style.flexDirection = 'column'; } //*************************************************************************** // Escuchar el botón Guardar de WME para resetear el inspector const wmeSaveBtn = document.querySelector( "button.action.save, button[title='Guardar'], button[aria-label='Guardar']"); if (wmeSaveBtn) { wmeSaveBtn.addEventListener("click", () => resetInspectorState()); } function createSidebarTab() { try { // 1. Verificar si WME y la función para registrar pestañas están listos if (!W || !W.userscripts || typeof W.userscripts.registerSidebarTab !== 'function') { console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral."); return; } // 2. Registrar la pestaña principal del script en WME y obtener tabPane let registration; try { registration = W.userscripts.registerSidebarTab( "NrmliZer"); // Nombre del Tab que aparece en WME } catch (e) { if (e.message.includes("already been registered")) { console.warn( "[WME PLN] Tab 'NrmliZer' ya registrado. El script puede no funcionar como se espera si hay múltiples instancias."); // Podrías intentar obtener el tabPane existente o simplemente // retornar. Para evitar mayor complejidad, si ya está // registrado, no continuaremos con la creación de la UI de la // pestaña. return; } //console.error("[WME PLN] Error registrando el sidebar tab:", e); throw e; // Relanzar otros errores para que se vean en consola } const { tabLabel, tabPane } = registration; if (!tabLabel || !tabPane) { //console.error("[WME PLN] Falló el registro del Tab: 'tabLabel' o 'tabPane' no fueron retornados."); return; } // Configurar el ícono y nombre de la pestaña principal del script tabLabel.innerHTML = ` <img src="data:image/jpeg;base64,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" style="height: 16px; vertical-align: middle; margin-right: 5px;"> NrmliZer `; // 3. Inicializar las pestañas internas (General, Especiales, // Diccionario, Reemplazos) const tabsContainer = document.createElement("div"); tabsContainer.style.display = "flex"; tabsContainer.style.marginBottom = "8px"; tabsContainer.style.gap = "8px"; const tabButtons = {}; const tabContents = {}; // Objeto para guardar los divs de contenido // Crear botones para cada pestaña tabNames.forEach(({ label, icon }) => { const btn = document.createElement("button"); btn.innerHTML = icon ? `<span style="display: inline-flex; align-items: center; font-size: 11px;"> <span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label} </span>` : `<span style="font-size: 11px;">${label}</span>`; btn.style.fontSize = "11px"; btn.style.padding = "4px 8px"; btn.style.marginRight = "4px"; btn.style.minHeight = "28px"; btn.style.border = "1px solid #ccc"; btn.style.borderRadius = "4px 4px 0 0"; btn.style.cursor = "pointer"; btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada btn.className = "custom-tab-style"; // Agrega aquí el tooltip personalizado para cada tab if (label === "Gene") btn.title = "Configuración general"; else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)"; else if (label === "Dicc") btn.title = "Diccionario de palabras válidas"; else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos"; // Estilo inicial: la primera pestaña es la activa if (label === tabNames[0].label) { btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco) btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa btn.style.fontWeight = "bold"; } else { btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro) btn.style.fontWeight = "normal"; } btn.addEventListener("click", () => { tabNames.forEach(({label : tabLabel_inner}) => { const isActive = (tabLabel_inner === label); const currentButton = tabButtons[tabLabel_inner]; if (tabContents[tabLabel_inner]) { tabContents[tabLabel_inner].style.display = isActive ? "block" : "none"; } if (currentButton) { // Aplicar/Quitar estilos de pestaña activa directamente if (isActive) { currentButton.style.backgroundColor = "#ffffff"; // Activo currentButton.style.borderBottom = "2px solid #007bff"; currentButton.style.fontWeight = "bold"; } else { currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo currentButton.style.borderBottom = "none"; currentButton.style.fontWeight = "normal"; } } // Llamar a la función de renderizado correspondiente if (isActive) { if (tabLabel_inner === "Espe") { const ul = document.getElementById("excludedWordsList"); if (ul && typeof renderExcludedWordsList === 'function') renderExcludedWordsList(ul); } else if (tabLabel_inner === "Dicc") { const ulDict = document.getElementById("dictionaryWordsList"); if (ulDict && typeof renderDictionaryList === 'function') renderDictionaryList(ulDict); } else if (tabLabel_inner === "Reemp") { const ulReemplazos = document.getElementById("replacementsListElementID"); if (ulReemplazos && typeof renderReplacementsList === 'function') renderReplacementsList(ulReemplazos); } } }); }); tabButtons[label] = btn; tabsContainer.appendChild(btn); }); tabPane.appendChild(tabsContainer); // Crear los divs contenedores para el contenido de cada pestaña tabNames.forEach(({ label }) => { const contentDiv = document.createElement("div"); contentDiv.style.display = label === tabNames[0].label ? "block" : "none"; // Mostrar solo la primera contentDiv.style.padding = "10px"; tabContents[label] = contentDiv; // Guardar referencia tabPane.appendChild(contentDiv); }); // --- POBLAR EL CONTENIDO DE CADA PESTAÑA --- // 4. Poblar el contenido de la pestaña "General" const containerGeneral = tabContents["Gene"]; if (containerGeneral) { let initialUsernameAttempt = "Pendiente"; // Para la etiqueta simplificada // No es necesario el polling complejo si solo es para la lógica // interna del checkbox if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user && W.loginManager.user.userName) { initialUsernameAttempt = W.loginManager.user .userName; // Se usará internamente en processNextPlace } const mainTitle = document.createElement("h3"); mainTitle.textContent = "NormliZer"; mainTitle.style.textAlign = "center"; mainTitle.style.fontSize = "18px"; mainTitle.style.marginBottom = "2px"; containerGeneral.appendChild(mainTitle); const versionInfo = document.createElement("div"); versionInfo.textContent = "V. " + VERSION; // VERSION global versionInfo.style.textAlign = "right"; versionInfo.style.fontSize = "10px"; versionInfo.style.color = "#777"; versionInfo.style.marginBottom = "15px"; containerGeneral.appendChild(versionInfo); const normSectionTitle = document.createElement("h4"); normSectionTitle.textContent = "Análisis de Nombres de Places"; normSectionTitle.style.fontSize = "15px"; normSectionTitle.style.marginTop = "10px"; normSectionTitle.style.marginBottom = "5px"; normSectionTitle.style.borderBottom = "1px solid #eee"; normSectionTitle.style.paddingBottom = "3px"; containerGeneral.appendChild(normSectionTitle); const scanButton = document.createElement("button"); scanButton.textContent = "Start Scan..."; scanButton.setAttribute("type", "button"); scanButton.style.marginBottom = "10px"; scanButton.style.width = "100%"; scanButton.style.padding = "8px"; scanButton.style.border = "none"; scanButton.style.borderRadius = "4px"; scanButton.style.backgroundColor = "#007bff"; scanButton.style.color = "#fff"; scanButton.style.cursor = "pointer"; scanButton.addEventListener("click", () => { const places = getVisiblePlaces(); const outputDiv = document.getElementById("wme-normalization-tab-output"); if (!outputDiv) { // Mover esta verificación antes // console.error("Div de salida (wme-normalization-tab-output) no encontrado en el tab."); return; } if (places.length === 0) { outputDiv.textContent = "No se encontraron lugares visibles para analizar."; return; } const maxPlacesInput = document.getElementById("maxPlacesInput"); const maxPlacesToScan = parseInt(maxPlacesInput?.value || "100", 10); const scannedCount = Math.min(places.length, maxPlacesToScan); outputDiv.textContent = `Escaneando ${scannedCount} lugares...`; setTimeout(() => {renderPlacesInFloatingPanel(places.slice(0, maxPlacesToScan)); }, 10); }); containerGeneral.appendChild(scanButton); const maxWrapper = document.createElement("div"); maxWrapper.style.display = "flex"; maxWrapper.style.alignItems = "center"; maxWrapper.style.gap = "8px"; maxWrapper.style.marginBottom = "8px"; const maxLabel = document.createElement("label"); maxLabel.textContent = "Máximo de places a revisar:"; maxLabel.style.fontSize = "13px"; maxWrapper.appendChild(maxLabel); const maxInput = document.createElement("input"); maxInput.type = "number"; maxInput.id = "maxPlacesInput"; maxInput.min = "1"; maxInput.value = "100"; maxInput.style.width = "80px"; maxWrapper.appendChild(maxInput); containerGeneral.appendChild(maxWrapper); const presets = [ 25, 50, 100, 250, 500 ]; const presetContainer = document.createElement("div"); presetContainer.style.textAlign = "center"; presetContainer.style.marginBottom = "8px"; presets.forEach(preset => { const btn = document.createElement("button"); btn.textContent = preset.toString(); btn.style.margin = "2px"; btn.style.padding = "4px 6px"; btn.addEventListener("click", () => { if (maxInput) maxInput.value = preset.toString(); }); presetContainer.appendChild(btn); }); containerGeneral.appendChild(presetContainer); const similarityLabel = document.createElement("label"); similarityLabel.textContent = "Similitud mínima para sugerencia de palabras:"; similarityLabel.title = "Ajusta el umbral mínimo de similitud (entre 80% y 95%) que se usará para sugerencias de reemplazo en nombres. El 100% es un reemplazo directo."; similarityLabel.style.fontSize = "12px"; similarityLabel.style.display = "block"; similarityLabel.style.marginTop = "8px"; similarityLabel.style.marginBottom = "4px"; containerGeneral.appendChild(similarityLabel); const similaritySlider = document.createElement("input"); similaritySlider.type = "range"; similaritySlider.min = "80"; similaritySlider.max = "95"; similaritySlider.value = "85"; similaritySlider.id = "similarityThreshold"; similaritySlider.style.width = "100%"; similaritySlider.title = "Desliza para ajustar la similitud mínima"; const similarityValueDisplay = document.createElement("span"); similarityValueDisplay.textContent = similaritySlider.value + "%"; similarityValueDisplay.style.marginLeft = "8px"; similaritySlider.addEventListener("input", () => { similarityValueDisplay.textContent = similaritySlider.value + "%"; }); containerGeneral.appendChild(similaritySlider); containerGeneral.appendChild(similarityValueDisplay); // ELIMINAR ESTO (SI SE APLICÓ EL CAMBIO ANTERIOR): // const editorFilterWrapper = document.getElementById("editorFilterInput")?.closest("div"); // if (editorFilterWrapper) editorFilterWrapper.remove(); // --- NUEVO: Checkbox para omitir mis ediciones --- const hideMyEditsWrapper = document.createElement("div"); hideMyEditsWrapper.style.marginTop = "10px"; hideMyEditsWrapper.style.marginBottom = "5px"; hideMyEditsWrapper.style.display = "flex"; hideMyEditsWrapper.style.alignItems = "center"; const hideMyEditsCheckbox = document.createElement("input"); hideMyEditsCheckbox.type = "checkbox"; hideMyEditsCheckbox.id = "chk-hide-my-edits"; hideMyEditsCheckbox.style.marginRight = "5px"; // Opcional: Cargar preferencia desde localStorage // const savedHideMyEdits = localStorage.getItem('WMEPlnHOM'); // if (savedHideMyEdits !== null) { // hideMyEditsCheckbox.checked = savedHideMyEdits === 'true'; // } // hideMyEditsCheckbox.addEventListener('change', (event) => { // localStorage.setItem('WMEPlnHOM', event.target.checked); // }); const hideMyEditsLabel = document.createElement("label"); // --- MODIFICACIÓN AQUÍ --- let labelText = "Omitir lugares editados por mí"; let currentUserName = null; if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user && W.loginManager.user.userName) { currentUserName = W.loginManager.user.userName; labelText += ` (${currentUserName})`; } else { labelText += " (Usuario no detectado)"; } hideMyEditsLabel.textContent = labelText; // --- FIN MODIFICACIÓN --- hideMyEditsLabel.htmlFor = "chk-hide-my-edits"; hideMyEditsLabel.style.fontSize = "13px"; hideMyEditsLabel.style.cursor = "pointer"; hideMyEditsWrapper.appendChild(hideMyEditsCheckbox); hideMyEditsWrapper.appendChild(hideMyEditsLabel); // containerGeneral.appendChild(hideMyEditsWrapper); // --- FIN: Checkbox para omitir mis ediciones --- const tabProgressWrapper = document.createElement("div"); tabProgressWrapper.style.margin = "10px 0"; tabProgressWrapper.style.height = "18px"; tabProgressWrapper.style.backgroundColor = "transparent"; const tabProgressBar = document.createElement("div"); tabProgressBar.style.height = "100%"; tabProgressBar.style.width = "0%"; tabProgressBar.style.backgroundColor = "#007bff"; tabProgressBar.style.transition = "width 0.2s"; tabProgressBar.id = "progressBarInnerTab"; tabProgressWrapper.appendChild(tabProgressBar); containerGeneral.appendChild(tabProgressWrapper); const tabProgressText = document.createElement("div"); tabProgressText.style.fontSize = "12px"; tabProgressText.style.marginTop = "5px"; tabProgressText.id = "progressBarTextTab"; tabProgressText.textContent = "Progreso: 0% (0/0)"; containerGeneral.appendChild(tabProgressText); const outputNormalizationInTab = document.createElement("div"); outputNormalizationInTab.id = "wme-normalization-tab-output"; outputNormalizationInTab.style.fontSize = "12px"; outputNormalizationInTab.style.minHeight = "20px"; outputNormalizationInTab.style.padding = "5px"; outputNormalizationInTab.style.marginBottom = "15px"; outputNormalizationInTab.textContent = "Presiona 'Start Scan...' para analizar los places visibles."; containerGeneral.appendChild(outputNormalizationInTab); } else { console.error("[WME PLN] No se pudo poblar la pestaña 'General' porque su contenedor no existe."); } // 5. Poblar las otras pestañas if (tabContents["Espe"]) { createExcludedWordsManager(tabContents["Espe"]) ; } else { console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'."); } if (tabContents["Dicc"]) { createDictionaryManager(tabContents["Dicc"]); } else { console.error( "[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Diccionario'."); } // --- LLAMADA A LA FUNCIÓN PARA POBLAR LA NUEVA PESTAÑA "Reemplazos" // --- if (tabContents["Reemp"]) { createReplacementsManager( tabContents["Reemp"]); // Esta es la llamada clave } else { console.error( "[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'."); } } catch (error) { console.error("[WME PLN] Error catastrófico creando la pestaña lateral:", error, error.stack); } } // Fin de createSidebarTab function waitForSidebarAPI() { if (W && W.userscripts && W.userscripts.registerSidebarTab) { const savedExcluded = localStorage.getItem("excludedWordsList"); if (savedExcluded) { try { const parsed = JSON.parse(savedExcluded); excludedWords = new Set(parsed); /* console.log( "[WME PLN] Palabras especiales restauradas desde localStorage:", Array.from(excludedWords));*/ } catch (e) { /*console.error( "[WME PLN] Error al cargar excludedWordsList del localStorage:", e);*/ excludedWords = new Set(); } } else { excludedWords = new Set(); /* console.log( "[WME PLN] No se encontraron palabras especiales en localStorage.");*/ } // --- Cargar diccionario desde localStorage --- const savedDictionary = localStorage.getItem("dictionaryWordsList"); if (savedDictionary) { try { const parsed = JSON.parse(savedDictionary); window.dictionaryWords = new Set(parsed); window.dictionaryIndex = {}; parsed.forEach(word => { const letter = word.charAt(0).toLowerCase(); if (!window.dictionaryIndex[letter]) { window.dictionaryIndex[letter] = []; } window.dictionaryIndex[letter].push(word); }); /* console.log( "[WME PLN] Diccionario restaurado desde localStorage:", parsed);*/ } catch (e) { /* console.error( "[WME PLN] Error al cargar dictionaryWordsList del localStorage:", e);*/ window.dictionaryWords = new Set(); } } else { window.dictionaryWords = new Set(); // console.log("[WME PLN] No se encontró diccionario en // localStorage."); } loadReplacementWordsFromStorage(); waitForWazeAPI(() => { createSidebarTab(); }); } else { // console.log("[WME PLN] Esperando W.userscripts API..."); setTimeout(waitForSidebarAPI, 1000); } } // 1. MODIFICAR normalizePlaceName function normalizePlaceName(word) { if (!word || typeof word !== "string") { return ""; } // Manejar palabras con "/" recursivamente if (word.includes("/")) { if (word === "/") return "/"; return word.split("/").map(part => normalizePlaceName(part.trim())).join("/"); } // Casos especiales como "MI" y "DI" if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI") { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } // Números romanos: todo en mayúsculas. No elimina puntos aquí. const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i; if (romanRegexStrict.test(word)) { return word.toUpperCase(); } // Si hay un número seguido de letra (sin espacio), convertir la letra en mayúscula word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`); let normalizedWord; if (/^[0-9]+$/.test(word)) { // Solo números normalizedWord = word; } else if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && word.includes('.')) { // Si es todo mayúsculas (o números) Y CONTIENE UN PUNTO (ej. "St."), mantener como está. // Podrías añadir más heurísticas aquí si es necesario para acrónimos con puntos. normalizedWord = word; } else { // Capitalización estándar normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } // NO eliminamos el punto aquí. Eso se hará al final del nombre completo del place si es necesario. // Ejemplo: "St." se mantendrá como "St." después de esta función. return normalizedWord; } //*************************************************************************** // Nombre: normalizeWordInternal // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Usada por postProcessQuotesAndParentheses. Similar a normalizePlaceName // pero con contexto de si es la primera palabra o dentro de comillas/paréntesis. //*************************************************************************** function normalizeWordInternal(word, isFirstWordInSequence = false, isInsideQuotesOrParentheses = false) { if (!word || typeof word !== "string") return ""; // Replicar lógica esencial de normalizePlaceName para consistencia if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI") { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i; if (romanRegexStrict.test(word)) { return word.toUpperCase(); } word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`); let resultWord; if (isInsideQuotesOrParentheses && !isFirstWordInSequence && commonWords.includes(word.toLowerCase())) { resultWord = word.toLowerCase(); } else if (/^[0-9]+$/.test(word)) { resultWord = word; } else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && word.includes('.')) { resultWord = word; // Mantener "St." dentro de comillas/paréntesis } else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1) { resultWord = word; // Mantener acrónimos sin puntos } else { resultWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } // NO eliminamos el punto aquí tampoco. return resultWord; } // 3. La función postProcessQuotesAndParentheses (CORREGIDA de la respuesta anterior) function postProcessQuotesAndParentheses(text) { if (typeof text !== 'string') return text; // Normalizar contenido dentro de comillas dobles text = text.replace(/"([^"]*)"/g, (match, content) => { const trimmedContent = content.trim(); if (trimmedContent === "") return '""'; const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0); const normalizedWordsInside = wordsInside.map((singleWord, index) => { return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses }).join(' '); return `"${normalizedWordsInside}"`; // Sin espacios extra }); // Normalizar contenido dentro de paréntesis text = text.replace(/\(([^)]*)\)/g, (match, content) => { const trimmedContent = content.trim(); if (trimmedContent === "") return '()'; const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0); const normalizedWordsInside = wordsInside.map((singleWord, index) => { return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses }).join(' '); return `(${normalizedWordsInside})`; // Sin espacios extra }); return text.replace(/\s+/g, ' ').trim(); // Limpieza final general } // === Palabras especiales === let excludedWords = new Set(); // Inicializada en waitForSidebarAPI let replacementWords = {}; // { "Urb.": "Urbanización", ... } let dictionaryWords = new Set(); // O window.dictionaryWords = new Set(); function createExcludedWordsManager(parentContainer) { const section = document.createElement("div"); section.id = "excludedWordsManagerSection"; // ID para la sección section.style.marginTop = "20px"; section.style.borderTop = "1px solid #ccc"; section.style.paddingTop = "10px"; const title = document.createElement("h4"); // Cambiado a h4 para jerarquía title.textContent = "Gestión de Palabras Especiales"; title.style.fontSize = "15px"; // Consistente con el otro título de sección title.style.marginBottom = "10px"; // Más espacio abajo section.appendChild(title); const addControlsContainer = document.createElement("div"); addControlsContainer.style.display = "flex"; addControlsContainer.style.gap = "8px"; addControlsContainer.style.marginBottom = "8px"; addControlsContainer.style.alignItems = "center"; // Alinear verticalmente const input = document.createElement("input"); input.type = "text"; input.placeholder = "Nueva palabra o frase"; input.style.flexGrow = "1"; input.style.padding = "6px"; // Mejor padding input.style.border = "1px solid #ccc"; input.style.borderRadius = "3px"; addControlsContainer.appendChild(input); const addBtn = document.createElement("button"); addBtn.textContent = "Añadir"; addBtn.style.padding = "6px 10px"; // Mejor padding addBtn.style.cursor = "pointer"; addBtn.addEventListener("click", function() { const newWord = input.value.trim(); const validation = isValidExcludedWord(newWord); if (!validation.valid) { alert(validation.msg); return; } // isValidExcludedWord ya comprueba duplicados en excludedWords y // commonWords y ahora también si existe en dictionaryWords. excludedWords.add(newWord); input.value = ""; renderExcludedWordsList( document.getElementById("excludedWordsList")); saveExcludedWordsToLocalStorage(); // Asumiendo que tienes esta // función }); addControlsContainer.appendChild(addBtn); section.appendChild(addControlsContainer); const actionButtonsContainer = document.createElement("div"); actionButtonsContainer.style.display = "flex"; actionButtonsContainer.style.gap = "8px"; actionButtonsContainer.style.marginBottom = "10px"; // Más espacio const exportBtn = document.createElement("button"); exportBtn.textContent = "Exportar"; // Más corto exportBtn.title = "Exportar Lista a XML"; exportBtn.style.padding = "6px 10px"; exportBtn.style.cursor = "pointer"; exportBtn.addEventListener("click", exportSharedDataToXml); actionButtonsContainer.appendChild(exportBtn); const clearBtn = document.createElement("button"); clearBtn.textContent = "Limpiar"; // Más corto clearBtn.title = "Limpiar toda la lista"; clearBtn.style.padding = "6px 10px"; clearBtn.style.cursor = "pointer"; clearBtn.addEventListener("click", function() { if ( confirm( "¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?")) { excludedWords.clear(); renderExcludedWordsList(document.getElementById( "excludedWordsList")); // Pasar el elemento UL } }); actionButtonsContainer.appendChild(clearBtn); section.appendChild(actionButtonsContainer); const search = document.createElement("input"); search.type = "text"; search.placeholder = "Buscar en especiales..."; search.style.display = "block"; search.style.width = "calc(100% - 14px)"; // Considerar padding y borde search.style.padding = "6px"; search.style.border = "1px solid #ccc"; search.style.borderRadius = "3px"; search.style.marginBottom = "5px"; search.addEventListener("input", () => { // Pasar el ulElement directamente renderExcludedWordsList( document.getElementById("excludedWordsList"), search.value.trim()); }); section.appendChild(search); const listContainerElement = document.createElement("ul"); listContainerElement.id = "excludedWordsList"; // Este es el UL listContainerElement.style.maxHeight = "150px"; listContainerElement.style.overflowY = "auto"; listContainerElement.style.border = "1px solid #ddd"; listContainerElement.style.padding = "5px"; // Padding interno listContainerElement.style.margin = "0"; // Resetear margen listContainerElement.style.background = "#fff"; listContainerElement.style.listStyle = "none"; section.appendChild(listContainerElement); const dropArea = document.createElement("div"); dropArea.textContent = "Arrastra aquí el archivo XML de palabras especiales"; dropArea.style.border = "2px dashed #ccc"; // Borde más visible dropArea.style.borderRadius = "4px"; dropArea.style.padding = "15px"; // Más padding dropArea.style.marginTop = "10px"; dropArea.style.textAlign = "center"; dropArea.style.background = "#f9f9f9"; dropArea.style.color = "#555"; dropArea.addEventListener("dragover", (e) => { e.preventDefault(); dropArea.style.background = "#e9e9e9"; dropArea.style.borderColor = "#aaa"; }); dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; dropArea.style.borderColor = "#ccc"; }); dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.style.background = "#f9f9f9"; handleXmlFileDrop(e.dataTransfer.files[0]); if (file && (file.type === "text/xml" || file.name.endsWith(".xml"))) { const reader = new FileReader(); reader.onload = function(evt) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString( evt.target.result, "application/xml"); const parserError = xmlDoc.querySelector("parsererror"); if (parserError) { alert("Error al parsear el archivo XML."); return; } // Detectar raíz const rootTag = xmlDoc.documentElement.tagName.toLowerCase(); if (rootTag !== "excludedwords" && rootTag !== "diccionario") { alert( "El archivo XML no es válido. Debe tener <ExcludedWords> o <diccionario> como raíz."); return; } // Importar palabras const words = xmlDoc.getElementsByTagName("word"); let newWordsAddedCount = 0; for (let i = 0; i < words.length; i++) { const val = words[i].textContent.trim(); if (val && !excludedWords.has(val)) { excludedWords.add(val); newWordsAddedCount++; } } // Importar reemplazos si existen const replacements = xmlDoc.getElementsByTagName("replacement"); for (let i = 0; i < replacements.length; i++) { const from = replacements[i].getAttribute("from"); const to = replacements[i].textContent.trim(); if (from && to) { replacementWords[from] = to; } } renderExcludedWordsList( document.getElementById("excludedWordsList")); alert(`Importación completada. Palabras nuevas: ${ newWordsAddedCount}`); } catch (err) { alert("Error procesando el archivo XML."); } }; reader.readAsText(file); } else { alert("Por favor, arrastra un archivo XML válido."); } }); section.appendChild(dropArea); parentContainer.appendChild(section); } // === Diccionario === function createDictionaryManager(parentContainer) { const section = document.createElement("div"); section.id = "dictionaryManagerSection"; section.style.marginTop = "20px"; section.style.borderTop = "1px solid #ccc"; section.style.paddingTop = "10px"; const title = document.createElement("h4"); title.textContent = "Gestión del Diccionario"; title.style.fontSize = "15px"; title.style.marginBottom = "10px"; section.appendChild(title); const addControlsContainer = document.createElement("div"); addControlsContainer.style.display = "flex"; addControlsContainer.style.gap = "8px"; addControlsContainer.style.marginBottom = "8px"; addControlsContainer.style.alignItems = "center"; // Alinear verticalmente const input = document.createElement("input"); input.type = "text"; input.placeholder = "Nueva palabra"; input.style.flexGrow = "1"; input.style.padding = "6px"; // Mejor padding input.style.border = "1px solid #ccc"; input.style.borderRadius = "3px"; addControlsContainer.appendChild(input); const addBtn = document.createElement("button"); addBtn.textContent = "Añadir"; addBtn.style.padding = "6px 10px"; // Mejor padding addBtn.style.cursor = "pointer"; addBtn.addEventListener("click", function() { const newWord = input.value.trim(); if (newWord) { const lowerNewWord = newWord.toLowerCase(); const alreadyExists = Array.from(window.dictionaryWords) .some(w => w.toLowerCase() === lowerNewWord); if (commonWords.includes(lowerNewWord)) { alert( "La palabra es muy común y no debe agregarse a la lista."); return; } if (alreadyExists) { alert("La palabra ya está en la lista (ignorando mayúsculas)."); return; } window.dictionaryWords.add(newWord); input.value = ""; renderDictionaryList( document.getElementById("dictionaryWordsList")); } }); addControlsContainer.appendChild(addBtn); section.appendChild(addControlsContainer); const actionButtonsContainer = document.createElement("div"); actionButtonsContainer.style.display = "flex"; actionButtonsContainer.style.gap = "8px"; actionButtonsContainer.style.marginBottom = "10px"; // Más espacio const exportBtn = document.createElement("button"); exportBtn.textContent = "Exportar"; // Más corto exportBtn.title = "Exportar Diccionario a XML"; exportBtn.style.padding = "6px 10px"; exportBtn.style.cursor = "pointer"; exportBtn.addEventListener("click", exportDictionaryWordsList); actionButtonsContainer.appendChild(exportBtn); const clearBtn = document.createElement("button"); clearBtn.textContent = "Limpiar"; // Más corto clearBtn.title = "Limpiar toda la lista"; clearBtn.style.padding = "6px 10px"; clearBtn.style.cursor = "pointer"; clearBtn.addEventListener("click", function() { if ( confirm( "¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?")) { window.dictionaryWords.clear(); renderDictionaryList(document.getElementById( "dictionaryWordsList")); // Pasar el elemento UL } }); actionButtonsContainer.appendChild(clearBtn); section.appendChild(actionButtonsContainer); // Diccionario: búsqueda const search = document.createElement("input"); search.type = "text"; search.placeholder = "Buscar en diccionario..."; search.style.display = "block"; search.style.width = "calc(100% - 14px)"; search.style.padding = "6px"; search.style.border = "1px solid #ccc"; search.style.borderRadius = "3px"; search.style.marginTop = "5px"; // On search input, render filtered list search.addEventListener("input", () => { renderDictionaryList(document.getElementById("dictionaryWordsList"), search.value.trim()); }); section.appendChild(search); // Lista UL para mostrar palabras del diccionario const listContainerElement = document.createElement("ul"); listContainerElement.id = "dictionaryWordsList"; listContainerElement.style.maxHeight = "150px"; listContainerElement.style.overflowY = "auto"; listContainerElement.style.border = "1px solid #ddd"; listContainerElement.style.padding = "5px"; listContainerElement.style.margin = "0"; listContainerElement.style.background = "#fff"; listContainerElement.style.listStyle = "none"; section.appendChild(listContainerElement); const dropArea = document.createElement("div"); dropArea.textContent = "Arrastra aquí el archivo XML del diccionario"; dropArea.style.border = "2px dashed #ccc"; dropArea.style.borderRadius = "4px"; dropArea.style.padding = "15px"; dropArea.style.marginTop = "10px"; dropArea.style.textAlign = "center"; dropArea.style.background = "#f9f9f9"; dropArea.style.color = "#555"; dropArea.addEventListener("dragover", (e) => { e.preventDefault(); dropArea.style.background = "#e9e9e9"; dropArea.style.borderColor = "#aaa"; }); dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; dropArea.style.borderColor = "#ccc"; }); dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.style.background = "#f9f9f9"; dropArea.style.borderColor = "#ccc"; const file = e.dataTransfer.files[0]; if (file && (file.type === "text/xml" || file.name.endsWith(".xml"))) { const reader = new FileReader(); reader.onload = function(evt) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(evt.target.result, "application/xml"); const parserError = xmlDoc.querySelector("parsererror"); if (parserError) { console.error("[WME PLN] Error parseando XML:", parserError.textContent); alert( "Error al parsear el archivo XML del diccionario."); return; } const xmlWords = xmlDoc.querySelectorAll("word"); let newWordsAddedCount = 0; for (let i = 0; i < xmlWords.length; i++) { const val = xmlWords[i].textContent.trim(); if (val && !window.dictionaryWords.has(val)) { window.dictionaryWords.add(val); newWordsAddedCount++; } } if (newWordsAddedCount > 0) console.log(`[WME PLN] ${ newWordsAddedCount} nuevas palabras añadidas desde XML.`); // Renderizar la lista en el panel renderDictionaryList(listContainerElement); } catch (err) { alert("Error procesando el diccionario XML."); } }; reader.readAsText(file); } else { alert("Por favor, arrastra un archivo XML válido."); } }); section.appendChild(dropArea); parentContainer.appendChild(section); renderDictionaryList(listContainerElement); } function loadReplacementWordsFromStorage() { const savedReplacements = localStorage.getItem("replacementWordsList"); if (savedReplacements) { try { replacementWords = JSON.parse(savedReplacements); if (typeof replacementWords !== 'object' || replacementWords === null) { // Asegurar que sea un objeto replacementWords = {}; } } catch (e) { console.error("[WME PLN] Error cargando lista de reemplazos desde localStorage:", e); replacementWords = {}; } } else { replacementWords = {}; // Inicializar si no hay nada guardado } console.log("[WME PLN] Reemplazos cargados:", Object.keys(replacementWords).length, "reglas."); } function saveReplacementWordsToStorage() { try { localStorage.setItem("replacementWordsList", JSON.stringify(replacementWords)); // console.log("[WME PLN] Lista de reemplazos guardada en // localStorage."); } catch (e) { console.error("[WME PLN] Error guardando lista de reemplazos en localStorage:", e); } } // Renderiza la lista de reemplazos function renderReplacementsList(ulElement) { //console.log("[WME PLN DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo"); if (!ulElement) { //console.error("[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList."); return; } ulElement.innerHTML = ""; // Limpiar lista actual const entries = Object.entries(replacementWords); if (entries.length === 0) { const li = document.createElement("li"); li.textContent = "No hay reemplazos definidos."; li.style.textAlign = "center"; li.style.color = "#777"; li.style.padding = "5px"; ulElement.appendChild(li); return; } // Ordenar alfabéticamente por la palabra original (from) entries.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase())); entries.forEach(([ from, to ]) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; const textContainer = document.createElement("div"); textContainer.style.flexGrow = "1"; textContainer.style.overflow = "hidden"; textContainer.style.textOverflow = "ellipsis"; textContainer.style.whiteSpace = "nowrap"; textContainer.title = `Reemplazar "${from}" con "${to}"`; const fromSpan = document.createElement("span"); fromSpan.textContent = from; fromSpan.style.fontWeight = "bold"; textContainer.appendChild(fromSpan); const arrowSpan = document.createElement("span"); arrowSpan.textContent = " → "; arrowSpan.style.margin = "0 5px"; textContainer.appendChild(arrowSpan); const toSpan = document.createElement("span"); toSpan.textContent = to; toSpan.style.color = "#007bff"; textContainer.appendChild(toSpan); li.appendChild(textContainer); // Botón Editar const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar este reemplazo"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px 4px"; editBtn.style.fontSize = "14px"; editBtn.style.marginLeft = "4px"; editBtn.addEventListener("click", () => { const newFrom = prompt("Editar texto original:", from); if (newFrom === null) return; const newTo = prompt("Editar texto de reemplazo:", to); if (newTo === null) return; if (!newFrom.trim()) { alert("El campo 'Texto Original' es requerido."); return; } if (newFrom === newTo) { alert("El texto original y el de reemplazo no pueden ser iguales."); return; } // Si cambia la clave, elimina la anterior if (newFrom !== from) delete replacementWords[from]; replacementWords[newFrom] = newTo; renderReplacementsList(ulElement); saveReplacementWordsToStorage(); }); // Botón Eliminar const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = `Eliminar este reemplazo`; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px 4px"; deleteBtn.style.fontSize = "14px"; deleteBtn.style.marginLeft = "4px"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`)) { delete replacementWords[from]; renderReplacementsList(ulElement); saveReplacementWordsToStorage(); } }); const btnContainer = document.createElement("span"); btnContainer.style.display = "flex"; btnContainer.style.gap = "4px"; btnContainer.appendChild(editBtn); btnContainer.appendChild(deleteBtn); li.appendChild(btnContainer); ulElement.appendChild(li); }); } function exportSharedDataToXml() { if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0) { alert( "No hay palabras especiales ni reemplazos definidos para exportar."); return; } let xmlParts = []; // Exportar palabras excluidas Array.from(excludedWords) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) .forEach(w => xmlParts.push(` <word>${xmlEscape(w)}</word>`)); // Exportar reemplazos Object.entries(replacementWords) .sort((a, b) => a[0].toLowerCase().localeCompare( b[0].toLowerCase())) // Ordenar por 'from' .forEach(([ from, to ]) => { xmlParts.push(` <replacement from="${xmlEscape(from)}">${ xmlEscape(to)}</replacement>`); }); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n${ xmlParts.join("\n")}\n</ExcludedWords>`; const blob = new Blob([ xmlContent ], { type : "application/xml;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; // Puedes darle un nombre genérico ya que contiene ambos tipos de datos a.download = "wme_normalizer_data_export.xml"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function handleXmlFileDrop(file) { if (file && (file.type === "text/xml" || file.name.endsWith(".xml"))) { const reader = new FileReader(); reader.onload = function(evt) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(evt.target.result, "application/xml"); const parserError = xmlDoc.querySelector("parsererror"); if (parserError) { alert("Error al parsear el archivo XML: " + parserError.textContent); return; } const rootTag = xmlDoc.documentElement.tagName.toLowerCase(); if (rootTag !== "excludedwords") { // Asumiendo que la raíz sigue siendo esta alert( "El archivo XML no es válido. Debe tener <ExcludedWords> como raíz."); return; } let newExcludedAdded = 0; let newReplacementsAdded = 0; let replacementsOverwritten = 0; // Importar palabras excluidas const words = xmlDoc.getElementsByTagName("word"); for (let i = 0; i < words.length; i++) { const val = words[i].textContent.trim(); if (val && !excludedWords.has(val)) { // Asegúrate que isValidExcludedWord no lo bloquee si // viene de XML const validation = isValidExcludedWord(val); // Revalidar o ajustar esta // lógica para importación if (validation.valid) { // Omitir esta validación si el XML es la fuente de // verdad excludedWords.add(val); newExcludedAdded++; } else { console.warn(`Palabra excluida omitida desde XML "${ val}": ${validation.msg}`); } } } // Importar reemplazos const replacements = xmlDoc.getElementsByTagName("replacement"); for (let i = 0; i < replacements.length; i++) { const from = replacements[i].getAttribute("from")?.trim(); const to = replacements[i].textContent.trim(); if (from && to) { // Permitir que 'to' esté vacío si se quiere "reemplazar // con nada" if (replacementWords.hasOwnProperty(from) && replacementWords[from] !== to) { replacementsOverwritten++; } else if (!replacementWords.hasOwnProperty(from)) { newReplacementsAdded++; } replacementWords[from] = to; } } // Guardar y Re-renderizar AMBAS listas saveExcludedWordsToLocalStorage(); // Asumo que tienes esta // función o la adaptas saveReplacementWordsToStorage(); // Re-renderizar las listas en sus respectivas pestañas si están // visibles o al activarse const excludedListElement = document.getElementById("excludedWordsList"); if (excludedListElement) renderExcludedWordsList(excludedListElement); const replacementsListElement = document.getElementById("replacementsListElementID"); if (replacementsListElement) renderReplacementsList(replacementsListElement); alert(`Importación completada.\nPalabras Especiales nuevas: ${ newExcludedAdded}\nReemplazos nuevos: ${ newReplacementsAdded}\nReemplazos sobrescritos: ${ replacementsOverwritten}`); } catch (err) { console.error( "[WME PLN] Error procesando el archivo XML importado:", err); alert("Ocurrió un error procesando el archivo XML."); } }; reader.readAsText(file); } else { alert("Por favor, arrastra un archivo XML válido."); } } function createReplacementsManager(parentContainer) { parentContainer.innerHTML = ''; // Limpiar por si acaso const title = document.createElement("h4"); title.textContent = "Gestión de Reemplazos de Palabras/Frases"; title.style.fontSize = "15px"; title.style.marginBottom = "10px"; parentContainer.appendChild(title); // Sección para añadir nuevos reemplazos const addSection = document.createElement("div"); addSection.style.display = "flex"; addSection.style.gap = "8px"; addSection.style.marginBottom = "12px"; addSection.style.alignItems = "flex-end"; // Alinear inputs y botón const fromInputContainer = document.createElement("div"); fromInputContainer.style.flexGrow = "1"; const fromLabel = document.createElement("label"); fromLabel.textContent = "Texto Original:"; fromLabel.style.display = "block"; fromLabel.style.fontSize = "12px"; fromLabel.style.marginBottom = "2px"; const fromInput = document.createElement("input"); fromInput.type = "text"; fromInput.placeholder = "Ej: Urb."; fromInput.style.width = "95%"; // Para que quepa bien fromInput.style.padding = "6px"; fromInput.style.border = "1px solid #ccc"; fromInputContainer.appendChild(fromLabel); fromInputContainer.appendChild(fromInput); addSection.appendChild(fromInputContainer); const toInputContainer = document.createElement("div"); toInputContainer.style.flexGrow = "1"; const toLabel = document.createElement("label"); toLabel.textContent = "Texto de Reemplazo:"; toLabel.style.display = "block"; toLabel.style.fontSize = "12px"; toLabel.style.marginBottom = "2px"; const toInput = document.createElement("input"); toInput.type = "text"; toInput.placeholder = "Ej: Urbanización"; toInput.style.width = "95%"; toInput.style.padding = "6px"; toInput.style.border = "1px solid #ccc"; toInputContainer.appendChild(toLabel); toInputContainer.appendChild(toInput); addSection.appendChild(toInputContainer); fromInput.setAttribute('spellcheck', 'false'); toInput.setAttribute('spellcheck', 'false'); const addReplacementBtn = document.createElement("button"); addReplacementBtn.textContent = "Añadir"; addReplacementBtn.style.padding = "6px 10px"; addReplacementBtn.style.cursor = "pointer"; addReplacementBtn.style.height = "30px"; // Para alinear con los inputs addSection.appendChild(addReplacementBtn); parentContainer.appendChild(addSection); // Elemento UL para la lista de reemplazos const listElement = document.createElement("ul"); listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista listElement.style.maxHeight = "150px"; listElement.style.overflowY = "auto"; listElement.style.border = "1px solid #ddd"; listElement.style.padding = "8px"; listElement.style.margin = "0 0 10px 0"; listElement.style.background = "#fff"; listElement.style.listStyle = "none"; parentContainer.appendChild(listElement); // Event listener para el botón "Añadir" addReplacementBtn.addEventListener("click", () => { const fromValue = fromInput.value.trim(); const toValue = toInput.value.trim(); if (!fromValue) { alert("El campo 'Texto Original' es requerido."); return; } if (fromValue === toValue) { alert("El texto original y el de reemplazo no pueden ser iguales."); return; } if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue) { if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`)) return; } replacementWords[fromValue] = toValue; fromInput.value = ""; toInput.value = ""; // Renderiza toda la lista (más seguro y rápido en la práctica) renderReplacementsList(listElement); saveReplacementWordsToStorage(); }); // Coloca esta función junto a aplicarReemplazosGenerales y aplicarReglasEspecialesNombre /*function aplicarReemplazosDefinidos(text, replacementRules) { let newText = text; if (typeof replacementRules !== 'object' || Object.keys(replacementRules).length === 0) { return newText; // No hay reglas o no es un objeto, devolver el texto original } // Ordenar las claves de reemplazo por longitud descendente para manejar casos como // "D1 Super" y "D1" donde "D1 Super" debe procesarse primero. const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length); for (const fromKey of sortedFromKeys) { const toValue = replacementRules[fromKey]; // Crear una expresión regular para encontrar 'fromKey' como palabra completa, // insensible a mayúsculas/minúsculas (flag 'gi'). // escapeRegExp es importante si 'fromKey' puede contener caracteres especiales de regex. const regex = new RegExp('\\b' + escapeRegExp(fromKey) + '\\b', 'giu'); // <--- MODIFICADO AQUÍ: Añadido flag 'u' newText = newText.replace(regex, (matchedInText, offset, originalString) => { // matchedInText: La cadena que coincidió en el texto (ej. "d1", "D1"). // fromKey: La clave canónica del objeto replacementWords (ej. "D1"). // toValue: El valor de reemplazo canónico (ej. "Tiendas D1"). // Condición para evitar reemplazo redundante: // Verificar si 'matchedInText' ya forma parte de 'toValue' en el contexto correcto. // Ej: fromKey="D1", toValue="Tiendas D1". // Si encontramos "D1" en "Mis Tiendas D1", queremos evitar cambiarlo a "Mis Tiendas Tiendas D1". // Encontrar dónde estaría 'fromKey' (canónico) dentro de 'toValue' (canónico) // para alinear la comprobación. Hacemos esta parte insensible a mayúsculas para la búsqueda de índice. const fromKeyLower = fromKey.toLowerCase(); const toValueLower = toValue.toLowerCase(); const indexOfFromInTo = toValueLower.indexOf(fromKeyLower); if (indexOfFromInTo !== -1) { // Calculamos el inicio de donde podría estar el 'toValue' completo en el 'originalString' const potentialExistingToStart = offset - indexOfFromInTo; if (potentialExistingToStart >= 0 && (potentialExistingToStart + toValue.length) <= originalString.length) { const substringInOriginal = originalString.substring(potentialExistingToStart, potentialExistingToStart + toValue.length); // Comparamos (insensible a mayúsculas) si el 'toValue' ya existe en esa posición if (substringInOriginal.toLowerCase() === toValueLower) { // Ya existe el 'toValue' completo en el lugar correcto, así que no reemplazamos. // Devolvemos el texto que coincidió originalmente. return matchedInText; } } } // Si no se cumple la condición anterior, realizamos el reemplazo con 'toValue'. return toValue; }); } return newText; }*/ // Botones de Acción y Drop Area (usarán la lógica compartida) const actionButtonsContainer = document.createElement("div"); actionButtonsContainer.style.display = "flex"; actionButtonsContainer.style.gap = "8px"; actionButtonsContainer.style.marginBottom = "10px"; const exportButton = document.createElement("button"); exportButton.textContent = "Exportar Todo"; exportButton.title = "Exportar Excluidas y Reemplazos a XML"; exportButton.style.padding = "6px 10px"; exportButton.addEventListener( "click", exportSharedDataToXml); // Llamar a la función compartida actionButtonsContainer.appendChild(exportButton); const clearButton = document.createElement("button"); clearButton.textContent = "Limpiar Reemplazos"; clearButton.title = "Limpiar solo la lista de reemplazos"; clearButton.style.padding = "6px 10px"; clearButton.addEventListener("click", () => { if ( confirm( "¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?")) { replacementWords = {}; saveReplacementWordsToStorage(); renderReplacementsList(listElement); } }); actionButtonsContainer.appendChild(clearButton); parentContainer.appendChild(actionButtonsContainer); const dropArea = document.createElement("div"); dropArea.textContent = "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)"; // ... (estilos para dropArea como en createExcludedWordsManager) ... dropArea.style.border = "2px dashed #ccc"; dropArea.style.borderRadius = "4px"; dropArea.style.padding = "15px"; dropArea.style.marginTop = "10px"; dropArea.style.textAlign = "center"; dropArea.style.background = "#f9f9f9"; dropArea.style.color = "#555"; dropArea.addEventListener("dragover", (e) => { e.preventDefault(); dropArea.style.background = "#e9e9e9"; }); dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; }); dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.style.background = "#f9f9f9"; handleXmlFileDrop( e.dataTransfer.files[0]); // Usar una función manejadora compartida }); parentContainer.appendChild(dropArea); // Cargar y renderizar la lista inicial renderReplacementsList(listElement); } // === Renderizar lista de palabras excluidas === function renderExcludedWordsList(ulElement, filter = "") { // AHORA RECIBE ulElement if (!ulElement) { // Intentar obtenerlo por ID como último recurso si no se pasó, // pero idealmente siempre se pasa desde el llamador. ulElement = document.getElementById("excludedWordsList"); if (!ulElement) { //console.error("[WME PLN] Contenedor 'excludedWordsList' no encontrado para renderizar."); return; } } const currentFilter = filter.toLowerCase(); /* console.log("[WME PLN] Renderizando lista. Filtro:", currentFilter, "Total palabras:", excludedWords.size);*/ ulElement.innerHTML = ""; // Limpiar lista anterior const wordsToRender = Array.from(excludedWords) .filter(word => word.toLowerCase().includes(currentFilter)) .sort((a, b) => a.toLowerCase().localeCompare( b.toLowerCase())); // Ordenar alfabéticamente if (wordsToRender.length === 0) { const li = document.createElement("li"); li.style.padding = "5px"; li.style.textAlign = "center"; li.style.color = "#777"; if (excludedWords.size === 0) { li.textContent = "La lista está vacía."; } else if (currentFilter !== "") { li.textContent = "No hay coincidencias para el filtro."; } else { li.textContent = "La lista está vacía (o error inesperado)."; // Fallback } ulElement.appendChild(li); } else { wordsToRender.forEach(word => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; // Ajuste li.style.borderBottom = "1px solid #f0f0f0"; const wordSpan = document.createElement("span"); wordSpan.textContent = word; wordSpan.style.maxWidth = "calc(100% - 60px)"; // Dejar espacio para botones wordSpan.style.overflow = "hidden"; wordSpan.style.textOverflow = "ellipsis"; wordSpan.style.whiteSpace = "nowrap"; wordSpan.title = word; li.appendChild(wordSpan); const iconContainer = document.createElement("span"); iconContainer.style.display = "flex"; iconContainer.style.gap = "8px"; // Más espacio entre iconos const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px"; editBtn.style.fontSize = "14px"; // Iconos un poco más grandes editBtn.addEventListener("click", () => { const newWord = prompt("Editar palabra:", word); if (newWord !== null && newWord.trim() !== word) { // Permitir string vacío para borrar si se quisiera, pero // trim() lo evita const trimmedNewWord = newWord.trim(); if (trimmedNewWord === "") { alert("La palabra no puede estar vacía."); return; } if (excludedWords.has(trimmedNewWord) && trimmedNewWord !== word) { alert("Esa palabra ya existe en la lista."); return; } excludedWords.delete(word); excludedWords.add(trimmedNewWord); renderExcludedWordsList(ulElement, currentFilter); } }); const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px"; deleteBtn.style.fontSize = "14px"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Estás seguro de que deseas eliminar la palabra '${ word}'?`)) { excludedWords.delete(word); renderExcludedWordsList(ulElement, currentFilter); } }); iconContainer.appendChild(editBtn); iconContainer.appendChild(deleteBtn); li.appendChild(iconContainer); ulElement.appendChild(li); }); } try { localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords))); // console.log("[WME PLN] Lista guardada en localStorage:", // Array.from(excludedWords)); } catch (e) { console.error("[WME PLN] Error guardando en localStorage:", e); // Considerar no alertar cada vez para no ser molesto si el localStorage // está lleno. Podría ser un mensaje en consola o una notificación sutil // en la UI. } } // Nueva función: renderDictionaryList function renderDictionaryList(ulElement, filter = "") { if (!ulElement || !window.dictionaryWords) return; const currentFilter = filter.toLowerCase(); ulElement.innerHTML = ""; const wordsToRender = Array.from(window.dictionaryWords) .filter(word => word.toLowerCase().startsWith(currentFilter)) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); if (wordsToRender.length === 0) { const li = document.createElement("li"); li.textContent = window.dictionaryWords.size === 0 ? "El diccionario está vacío." : "No hay coincidencias."; li.style.textAlign = "center"; li.style.color = "#777"; ulElement.appendChild(li); // Guardar diccionario también cuando está vacío try { localStorage.setItem( "dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords))); } catch (e) { console.error( "[WME PLN] Error guardando el diccionario en localStorage:", e); } return; } wordsToRender.forEach(word => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "4px 2px"; li.style.borderBottom = "1px solid #f0f0f0"; const wordSpan = document.createElement("span"); wordSpan.textContent = word; wordSpan.style.maxWidth = "calc(100% - 60px)"; wordSpan.style.overflow = "hidden"; wordSpan.style.textOverflow = "ellipsis"; wordSpan.style.whiteSpace = "nowrap"; wordSpan.title = word; li.appendChild(wordSpan); const iconContainer = document.createElement("span"); iconContainer.style.display = "flex"; iconContainer.style.gap = "8px"; const editBtn = document.createElement("button"); editBtn.innerHTML = "✏️"; editBtn.title = "Editar"; editBtn.style.border = "none"; editBtn.style.background = "transparent"; editBtn.style.cursor = "pointer"; editBtn.style.padding = "2px"; editBtn.style.fontSize = "14px"; editBtn.addEventListener("click", () => { const newWord = prompt("Editar palabra:", word); if (newWord !== null && newWord.trim() !== word) { window.dictionaryWords.delete(word); window.dictionaryWords.add(newWord.trim()); renderDictionaryList(ulElement, currentFilter); } }); const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.border = "none"; deleteBtn.style.background = "transparent"; deleteBtn.style.cursor = "pointer"; deleteBtn.style.padding = "2px"; deleteBtn.style.fontSize = "14px"; deleteBtn.addEventListener("click", () => { if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`)) { window.dictionaryWords.delete(word); renderDictionaryList(ulElement, currentFilter); } }); iconContainer.appendChild(editBtn); iconContainer.appendChild(deleteBtn); li.appendChild(iconContainer); ulElement.appendChild(li); }); // Guardar el diccionario actualizado en localStorage después de cada // render try { localStorage.setItem( "dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords))); } catch (e) { console.error("[WME PLN] Error guardando el diccionario en localStorage:", e); } } function exportExcludedWordsList() { if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0) { alert("No hay palabras especiales ni reemplazos para exportar."); return; } let xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n`; xmlContent += Array.from(excludedWords) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) .map(w => ` <word>${xmlEscape(w)}</word>`) .join("\n"); if (Object.keys(replacementWords).length > 0) { xmlContent += "\n"; xmlContent += Object.entries(replacementWords) .map(([ from, to ]) => ` <replacement from="${ xmlEscape(from)}">${xmlEscape(to)}</replacement>`) .join("\n"); } xmlContent += "\n</ExcludedWords>"; const blob = new Blob([ xmlContent ], { type : "application/xml;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "wme_excluded_words_export.xml"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function exportDictionaryWordsList() { if (window.dictionaryWords.size === 0) { alert( "La lista de palabras del diccionario está vacía. Nada que exportar."); return; } const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<diccionario>\n${ Array.from(window.dictionaryWords) .sort((a, b) => a.toLowerCase().localeCompare( b.toLowerCase())) // Exportar ordenado .map(w => ` <word>${xmlEscape(w)}</word>`) // Indentación y escape .join("\n")}\n</diccionario>`; const blob = new Blob([ xmlContent ], { type : "application/xml;charset=utf-8" }); // Añadir charset const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "wme_dictionary_words_export.xml"; // Nombre más descriptivo document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function xmlEscape(str) { return str.replace(/[<>&"']/g, function(match) { switch (match) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '"': return '"'; case "'": return '''; default: return match; } }); } waitForSidebarAPI(); })(); // Función reutilizable para mostrar el spinner de carga function showLoadingSpinner() { const scanSpinner = document.createElement("div"); scanSpinner.id = "scanSpinnerOverlay"; scanSpinner.style.position = "fixed"; scanSpinner.style.top = "0"; scanSpinner.style.left = "0"; scanSpinner.style.width = "100%"; scanSpinner.style.height = "100%"; scanSpinner.style.background = "rgba(0, 0, 0, 0.5)"; scanSpinner.style.zIndex = "10000"; scanSpinner.style.display = "flex"; scanSpinner.style.justifyContent = "center"; scanSpinner.style.alignItems = "center"; const scanContent = document.createElement("div"); scanContent.style.background = "#fff"; scanContent.style.padding = "20px"; scanContent.style.borderRadius = "8px"; scanContent.style.textAlign = "center"; const spinner = document.createElement("div"); spinner.classList.add("spinner"); spinner.style.border = "6px solid #f3f3f3"; spinner.style.borderTop = "6px solid #3498db"; spinner.style.borderRadius = "50%"; spinner.style.width = "40px"; spinner.style.height = "40px"; spinner.style.animation = "spin 1s linear infinite"; spinner.style.margin = "0 auto 10px auto"; const progressText = document.createElement("div"); progressText.id = "scanProgressText"; progressText.textContent = "Analizando lugares: 0%"; progressText.style.fontSize = "14px"; progressText.style.color = "#333"; scanContent.appendChild(spinner); scanContent.appendChild(progressText); scanSpinner.appendChild(scanContent); document.body.appendChild(scanSpinner); const style = document.createElement("style"); style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址