您需要先安装一个扩展,例如 篡改猴、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.3.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: '1400px', 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', '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(); } //**************************************************************************************************** //**************************************************************************************************** // Nombre: waitForWazeAPI // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Espera a que la API de Waze esté disponible y luego llama a tryInitializeSDK. // Parámetros: // callbackPrincipalDelScript (Function): Función a llamar después de la inicialización exitosa del SDK. //**************************************************************************************************** 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); } //**************************************************************************************************** //**************************************************************************************************** // Nombre: updateScanProgressBar // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Actualiza el progreso de la barra de progreso de la pestaña de escaneo. // Parámetros: //**************************************************************************************************** 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})`; } } //**************************************************************************************************** //**************************************************************************************************** // Nombre: escapeRegExp // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Escapa caracteres especiales en expresiones regulares. // Parámetros: // string (String): La cadena a escapar. //**************************************************************************************************** function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } //**************************************************************************************************** //**************************************************************************************************** // Nombre: removeDiacritics // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Elimina tildes/diacríticos de una cadena. // Parámetros: function removeDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } //**************************************************************************************************** //**************************************************************************************************** // Nombre: isValidExcludedWord // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Verifica si una palabra es válida para ser excluida. // Parámetros: // newWord (String): La palabra a verificar. //**************************************************************************************************** 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 }; } //**************************************************************************************************** //**************************************************************************************************** // Nombre: aplicarReemplazosGenerales // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Aplica reemplazos generales a un nombre. // Parámetros: // name (String): El nombre a procesar. //**************************************************************************************************** function aplicarReemplazosGenerales(name) { // console.log("[DEBUG aplicarReemplazosGenerales] Input name:", name, "| CharCode of |:", name.includes('|') ? name.charCodeAt(name.indexOf('|')) : 'Not found'); if (typeof window.skipGeneralReplacements === "boolean" && window.skipGeneralReplacements) { return name; } const reglas = [ // Nueva regla: reemplazar | por espacio, guion y espacio { buscar: /\|/g, reemplazar: " - " }, // Regex para el pipe estándar // Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor { buscar : /\s*\/\s*/g, reemplazar : " / " }, // Corrección: Para buscar [P] o [p] literalmente { buscar : /\[[Pp]\]/g, reemplazar : "" }, { buscar : /\s*-\s*/g, reemplazar : " - " }, ]; reglas.forEach( regla => { if (regla.buscar.source === '\\|') { // Identificar la regla del pipe const oldName = name; name = name.replace(regla.buscar, regla.reemplazar); // console.log(`[DEBUG aplicarReemplazosGenerales] After pipe rule: old='${oldName}', new='${name}', regex='${regla.buscar.toString()}'`); } else { name = name.replace(regla.buscar, regla.reemplazar); } }); name = name.replace(/\s{2,}/g, ' '); //console.log("[DEBUG aplicarReemplazosGenerales] Output name:", name); return name; } //**************************************************************************************************** //**************************************************************************************************** // Nombre: aplicarReglasEspecialesNombre // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Aplica reglas especiales a un nombre. // Parámetros: // newName (String): El nombre a procesar. //**************************************************************************************************** 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", "Ciudadela", "Edificio", "Conjunto Habitacional", "Apartamentos", "Club Campestre", "Club Residencial", "Motel", "Restaurante", "Eco Hotel", "Finca Hotel", "CR", "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()}`); console.log("[DEBUG aplicarReglasEspecialesNombre] Before hyphen capitalization:", newName); newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); console.log("[DEBUG aplicarReglasEspecialesNombre] After hyphen capitalization:", newName); return newName; } //**************************************************************************************************** // Nombre: aplicarReemplazosDefinidos // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Aplica reemplazos definidos a un texto. // Parámetros: // text (String): El texto a procesar. // replacementRules (Object): Las reglas de reemplazo. //**************************************************************************************************** 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; } //**************************************************************************************************** // Nombre: getVisiblePlaces // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Obtiene los lugares visibles en el mapa. // Parámetros: //**************************************************************************************************** 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 --- //**************************************************************************************************** // Nombre: getPlaceCategoryName // Autor: mincho77 // Fecha: 2025-05-27 // Descripción: Obtiene el nombre de la categoría de un lugar. // Parámetros: // venueFromOldModel (Object): El objeto 'venue' de WME. // venueSDKObject (Object): El objeto 'venue' de WME. // Retorna: // String: El nombre de la categoría. //**************************************************************************************************** function getPlaceCategoryName(venueFromOldModel, venueSDKObject) { // Acepta ambos tipos de venue let categoryId = null; let categoryName = null; // 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: mincho77 // Fecha: 2025-05-27 // 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' // 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 } //**************************************************************************************************** // Nombre: getLoggedInUserInfo // Autor: mincho77 // Fecha: 2025-05-29 // Descripción: Obtiene la información del usuario actualmente logueado en WME. // Retorna: // Object: Un objeto con las propiedades 'userId' (Number) y 'userName' (String), // o null si la información no está disponible o el usuario no está logueado. //**************************************************************************************************** function getLoggedInUserInfo() { if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user) { const user = W.loginManager.user; const userInfo = {}; if (typeof user.id === 'number') { userInfo.userId = user.id; } else { userInfo.userId = null; } if (typeof user.userName === 'string' && user.userName.trim() !== '') { userInfo.userName = user.userName; } else { userInfo.userName = null; } // Devuelve el objeto solo si se pudo obtener al menos un dato if (userInfo.userId !== null || userInfo.userName !== null) { return userInfo; } } // Si W, W.loginManager o W.loginManager.user no están definidos, // o no se pudo obtener ni ID ni userName. console.warn("[WME PLN] No se pudo obtener la información del usuario logueado."); return null; } //**************************************************************************************************** // Nombre: getPlaceCityInfo // Autor: mincho77 // Fecha: 2025-05-28 // Actualizado 2025-05-29 // Descripción: Determina si un lugar tiene una ciudad asignada o información de calle y devuelve información iconográfica. // Parámetros: // venueFromOldModel (Object): El objeto 'venue' del modelo antiguo de WME. // venueSDKObject (Object, opcional): El objeto 'venue' del SDK de WME. // Retorna: // Object: Un objeto con 'icon' (String), 'title' (String) y 'hasCity' (boolean indicando ciudad explícita). //**************************************************************************************************** async function getPlaceCityInfo(venueFromOldModel, venueSDKObject) { let hasExplicitCity = false; let explicitCityName = null; let hasStreetInfo = false; let cityAssociatedWithStreet = null; // --- 1. Check for EXPLICIT city --- // SDK if (venueSDKObject && venueSDKObject.address) { if (venueSDKObject.address.city && typeof venueSDKObject.address.city.name === 'string' && venueSDKObject.address.city.name.trim() !== '') { explicitCityName = venueSDKObject.address.city.name.trim(); hasExplicitCity = true; // source = "SDK (address.city.name)"; } else if (typeof venueSDKObject.address.cityName === 'string' && venueSDKObject.address.cityName.trim() !== '') { explicitCityName = venueSDKObject.address.cityName.trim(); hasExplicitCity = true; // source = "SDK (address.cityName)"; } } // Old Model (if no explicit city from SDK) if (!hasExplicitCity && venueFromOldModel && venueFromOldModel.attributes) { const cityID = venueFromOldModel.attributes.cityID; if (cityID && typeof W !== 'undefined' && W.model && W.model.cities && W.model.cities.getObjectById) { const cityObject = W.model.cities.getObjectById(cityID); if (cityObject && cityObject.attributes && typeof cityObject.attributes.name === 'string' && cityObject.attributes.name.trim() !== '') { explicitCityName = cityObject.attributes.name.trim(); hasExplicitCity = true; // source = `W.model.cities (ID: ${cityID})`; } } } // --- 2. Check for STREET information (and any city derived from it) --- // SDK street check if (venueSDKObject && venueSDKObject.address) { if ((venueSDKObject.address.street && typeof venueSDKObject.address.street.name === 'string' && venueSDKObject.address.street.name.trim() !== '') || (typeof venueSDKObject.address.streetName === 'string' && venueSDKObject.address.streetName.trim() !== '')) { hasStreetInfo = true; // source += (source ? ", " : "") + "SDK (street info)"; } } // Old Model street check (if not found via SDK or to supplement) if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.streetID) { hasStreetInfo = true; // Street ID exists in old model const streetID = venueFromOldModel.attributes.streetID; if (typeof W !== 'undefined' && W.model && W.model.streets && W.model.streets.getObjectById) { const streetObject = W.model.streets.getObjectById(streetID); if (streetObject && streetObject.attributes && streetObject.attributes.cityID) { const cityIDFromStreet = streetObject.attributes.cityID; if (W.model.cities && W.model.cities.getObjectById) { const cityObjectFromStreet = W.model.cities.getObjectById(cityIDFromStreet); if (cityObjectFromStreet && cityObjectFromStreet.attributes && typeof cityObjectFromStreet.attributes.name === 'string' && cityObjectFromStreet.attributes.name.trim() !== '') { cityAssociatedWithStreet = cityObjectFromStreet.attributes.name.trim(); // source += (source ? ", " : "") + `W.model.streets -> W.model.cities (StreetID: ${streetID}, CityID: ${cityIDFromStreet} -> ${cityAssociatedWithStreet})`; } } } } } // --- 3. Determine icon, title, and returned hasCity based on user's specified logic --- let icon; let title; const returnedHasCityBoolean = hasExplicitCity; // To be returned, indicates if an *explicit* city is set. const hasAnyAddressInfo = hasExplicitCity || hasStreetInfo; console.log(`[WME PLN DEBUG CityInfo] Calculated flags: hasExplicitCity=${hasExplicitCity} (Name: ${explicitCityName}), hasStreetInfo=${hasStreetInfo}, cityAssociatedWithStreet=${cityAssociatedWithStreet}`); console.log(`[WME PLN DEBUG CityInfo] Calculated: hasAnyAddressInfo=${hasAnyAddressInfo}`); if (hasAnyAddressInfo) { if (hasExplicitCity) { icon = "🏙️"; // Icono para ciudad asignada title = `Ciudad: ${explicitCityName}`; } else { // No tiene ciudad explícita, pero sí información de calle icon = "🛣️"; // Icono para "tiene calle pero no ciudad explícita" if (cityAssociatedWithStreet) { title = `Tiene ciudad asociada a la calle: ${cityAssociatedWithStreet}`; } else { title = "Tiene calle, sin ciudad explícita"; } } } else { // No tiene ni ciudad explícita ni información de calle icon = "🚫"; title = "Sin info. de dirección (ni ciudad ni calle)"; } // console.log(`[CITY INFO] Place ID ${venueFromOldModel ? venueFromOldModel.getID() : 'N/A'}: Icon=${icon}, Title='${title}', HasExplicitCity=${hasExplicitCity}, HasStreet=${hasStreetInfo}, CityViaStreet='${cityAssociatedWithStreet}', ReturnedHasCity=${returnedHasCityBoolean}`); return { icon: icon, title: title, hasCity: returnedHasCityBoolean // Este booleano se refiere a si hay una ciudad *explícitamente* seleccionada. }; } // --- 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 // --- 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() { const currentPlaceForLog = places[index]; const currentVenueIdForLog = currentPlaceForLog ? currentPlaceForLog.getID() : 'ID Desconocido'; const originalNameForLog = currentPlaceForLog && currentPlaceForLog.attributes ? (currentPlaceForLog.attributes.name?.value || currentPlaceForLog.attributes.name || 'Nombre Desconocido') : 'Nombre Desconocido'; //console.log(`[WME_PLN_TRACE] === Iniciando processNextPlace para índice: ${index}, ID: ${currentVenueIdForLog}, Nombre: "${originalNameForLog}" ===`); // 1. Leer estados de checkboxes y configuraciones iniciales // console.log(`[WME_PLN_TRACE] Leyendo configuraciones...`); const chkHideMyEditsChecked = document.getElementById("chk-hide-my-edits")?.checked ?? false; const useFullPipeline = true; 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; //console.log(`[WME_PLN_TRACE] Configuraciones leídas.`); // 2. Condición de salida principal (todos los lugares procesados) if (index >= places.length) { // console.log("[WME_PLN_TRACE] Todos los lugares procesados. Finalizando render..."); finalizeRender(inconsistents, places); return; } const venueFromOldModel = places[index]; 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; if (!places[index] || typeof places[index] !== 'object') { console.warn(`[WME_PLN_TRACE] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]); updateScanProgressBar(index, places.length); index++; // console.log(`[WME_PLN_TRACE] Saltando al siguiente place (lugar inválido). Próximo índice: ${index}`); setTimeout(() => processNextPlace(), 0); return; } // console.log(`[WME_PLN_TRACE] Venue Old Model obtenido: ID ${venueFromOldModel.getID()}`); // 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(`[WME_PLN_TRACE] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel); updateScanProgressBar(index, places.length); index++; // console.log(`[WME_PLN_TRACE] Saltando al siguiente place (sin nombre/inválido). Próximo índice: ${index}`); setTimeout(() => processNextPlace(), 0); return; } const originalName = venueFromOldModel?.attributes?.name?.value || venueFromOldModel?.attributes?.name || ''; const currentVenueId = venueFromOldModel.getID(); // console.log(`[WME_PLN_TRACE] Nombre original: "${originalName}", ID: ${currentVenueId}`); // --- OBTENER venueSDK UNA SOLA VEZ --- let venueSDK = null; // console.log(`[WME_PLN_TRACE] Intentando obtener venueSDK para ID: ${currentVenueId}...`); if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById) { try { venueSDK = await wmeSDK.DataModel.Venues.getById({ venueId: currentVenueId }); // console.log(`[WME_PLN_TRACE] venueSDK obtenido para ID: ${currentVenueId}`, venueSDK ? 'Exitoso' : 'Fallido (null)'); } catch (sdkError) { // console.error(`[WME_PLN_TRACE] Error al obtener venueSDK para ID ${currentVenueId}:`, sdkError); // venueSDK permanecerá null. } } else { console.log(`[WME_PLN_TRACE] SDK de WME no disponible o función getById no encontrada para venueSDK.`); } // --- FIN OBTENER venueSDK --- // 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (USANDO venueSDK si está disponible) --- //console.log(`[WME_PLN_TRACE] Obteniendo información del editor...`); let lastEditorInfoForLog = "Editor: Desconocido"; let lastEditorIdForComparison = null; let resolvedEditorName = "N/D"; let wasEditedByMe = false; let currentLoggedInUserId = null; let currentLoggedInUserName = null; 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; } } // console.log(`[WME_PLN_TRACE] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`); if (venueSDK && venueSDK.modificationData) { const updatedByDataFromSDK = venueSDK.modificationData.updatedBy; console.log(`[WME_PLN_TRACE] Info editor desde SDK:`, updatedByDataFromSDK); if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '') { lastEditorInfoForLog = `Editor (SDK): ${updatedByDataFromSDK}`; resolvedEditorName = updatedByDataFromSDK; if (currentLoggedInUserName && currentLoggedInUserName === updatedByDataFromSDK) { wasEditedByMe = true; } } else if (typeof updatedByDataFromSDK === 'number') { lastEditorInfoForLog = `Editor (SDK): ID ${updatedByDataFromSDK}`; resolvedEditorName = `ID ${updatedByDataFromSDK}`; 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; } else if (userObjectW) { lastEditorInfoForLog = `Editor (SDK ID ${updatedByDataFromSDK} -> W.model): ID ${updatedByDataFromSDK} (sin userName en W.model)`; } } } 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 { // console.log(`[WME_PLN_TRACE] Fallback a W.model para info de editor.`); const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy; if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined) { lastEditorIdForComparison = oldModelUpdatedBy; resolvedEditorName = `ID ${oldModelUpdatedBy}`; 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; } 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"; } } if (currentLoggedInUserId !== null && typeof lastEditorIdForComparison === 'number' && currentLoggedInUserId === lastEditorIdForComparison) { wasEditedByMe = true; } // console.log(`[WME_PLN_TRACE] Info editor final: ${lastEditorInfoForLog}, Editado por mi: ${wasEditedByMe}`); // ---- FIN INFO DEL EDITOR ---- // 5. --- PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA --- //console.log(`[WME_PLN_TRACE] Iniciando procesamiento palabra por palabra del nombre...`); let nombreSugeridoParcial = []; let sugerenciasLugar = {}; const originalWords = originalName.split(/\s+/); const processingStepLabel = document.getElementById("processingStep"); if (index === 0) { sugerenciasPorPalabra = {}; } const newRomanBaseRegexString = "((XC|XL|L?X{0,3})(IX|IV|V?I{0,3})?|(IX|IV|V?I{0,3}))"; const romanRegexStrict = new RegExp(`^${newRomanBaseRegexString}$`); // Sin 'i' para prueba con toUpperCase() const romanRegexStrictInsensitive = new RegExp(`^${newRomanBaseRegexString}$`, 'i'); // Con 'i' para isPotentiallyRomanNumeral originalWords.forEach((P, idx_word) => { // console.log(`[WME_PLN_TRACE_WORD] Procesando palabra #${idx_word + 1}: "${P}"`); const endsWithComma = restoreCommas && P.endsWith(","); const baseWord = endsWithComma ? P.slice(0, -1) : P; const cleaned = baseWord.trim(); if (cleaned === "") { nombreSugeridoParcial.push(cleaned); // console.log(`[WME_PLN_TRACE_WORD] Palabra vacía, continuando.`); return; } let isExcluded = false; let matchingExcludedWord = null; if (checkExcludedWords) { matchingExcludedWord = excludedArray.find(w_excluded => removeDiacritics(w_excluded.toLowerCase()) === removeDiacritics(cleaned.toLowerCase())); isExcluded = !!matchingExcludedWord; // console.log(`[WME_PLN_TRACE_WORD] Excluida: ${isExcluded}${isExcluded ? ` (coincide con: "${matchingExcludedWord}")` : ''}`); } let tempReplaced; const isCommon = commonWords.includes(cleaned.toLowerCase()); const isPotentiallyRomanNumeral = romanRegexStrictInsensitive.test(cleaned); console.log(`[WME_PLN_TRACE_WORD] Común: ${isCommon}, Potencial Romano: ${isPotentiallyRomanNumeral}`); if (isExcluded) { tempReplaced = matchingExcludedWord; if (romanRegexStrictInsensitive.test(tempReplaced)) { tempReplaced = tempReplaced.toUpperCase(); } } else { let dictionaryFormToUse = null; let foundInDictionary = false; if (checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") { const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase()); const cleanedHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(cleaned); for (const diccWord of window.dictionaryWords) { if (removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics) { foundInDictionary = true; const diccWordHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(diccWord); if (cleanedHasDiacritics && !diccWordHasDiacritics) { dictionaryFormToUse = cleaned; } else { if (isPotentiallyRomanNumeral) { if (diccWord === diccWord.toUpperCase() && romanRegexStrict.test(diccWord)) { dictionaryFormToUse = diccWord; } } else { dictionaryFormToUse = diccWord; } } break; } } // console.log(`[WME_PLN_TRACE_WORD] Encontrada en diccionario: ${foundInDictionary}${dictionaryFormToUse ? ` (usando forma: "${dictionaryFormToUse}")` : ''}`); } //Verificar si se encontró una forma en el diccionario if (dictionaryFormToUse !== null) { tempReplaced = dictionaryFormToUse; } else { tempReplaced = normalizePlaceName(cleaned); // console.log(`[WME_PLN_TRACE_WORD] Normalizada (estándar): "${tempReplaced}"`); } // Esta lógica capitaliza según si es romano, primera palabra, común, etc. // Necesitamos asegurarnos que "Mi" y "Di" no se conviertan a "MI"/"DI" aquí. if (tempReplaced.toUpperCase() === "MI" || tempReplaced.toUpperCase() === "DI" || tempReplaced.toUpperCase() === "SI") { tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase(); } else if (isPotentiallyRomanNumeral) { // No es "MI" ni "DI", pero sí un romano potencial const upperVersion = tempReplaced.toUpperCase(); if (romanRegexStrict.test(upperVersion)) { tempReplaced = upperVersion; } else { // No pasó la prueba estricta de romano, capitalizar normalmente tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase(); } } else if (idx_word === 0) { // No es "MI", "DI", ni romano, y es primera palabra tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1); } else { // No es "MI", "DI", ni romano, y no es primera palabra if (isCommon) { tempReplaced = tempReplaced.toLowerCase(); } else { tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1); } } } //console.log(`[WME_PLN_TRACE_WORD] Palabra temporalmente reemplazada a: "${tempReplaced}"`); // Generación de Sugerencias Clickeables const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase()); const tempReplacedLowerNoDiacritics = removeDiacritics(tempReplaced.toLowerCase()); if (cleaned !== tempReplaced && (!commonWords.includes(cleaned.toLowerCase()) || cleaned.toLowerCase() !== tempReplaced.toLowerCase()) && cleanedLowerNoDiacritics !== tempReplacedLowerNoDiacritics) { if (!sugerenciasLugar[baseWord]) sugerenciasLugar[baseWord] = []; if (!sugerenciasLugar[baseWord].some(s => s.word === cleaned && s.fuente === 'original_preserved')) { sugerenciasLugar[baseWord].push({ word: cleaned, similarity: 0.99, fuente: 'original_preserved' }); //console.log(`[WME_PLN_TRACE_WORD] Añadida sugerencia 'original_preserved': "${cleaned}" para "${baseWord}"`); } } if (!isExcluded && checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") { window.dictionaryWords.forEach(diccWord => { if (diccWord !== tempReplaced && diccWord !== cleaned) { if (removeDiacritics(diccWord.toLowerCase()) === tempReplacedLowerNoDiacritics || removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics) { 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'}); //console.log(`[WME_PLN_TRACE_WORD] Añadida sugerencia 'dictionary': "${diccWord}" para "${baseWord}"`); } } } }); } if (checkExcludedWords) { const similarExcluded = findSimilarWords(cleaned, excludedArray, similarityThreshold).filter(s => s.similarity < 1); if (similarExcluded.length > 0) { if (!sugerenciasLugar[baseWord]) sugerenciasLugar[baseWord] = []; similarExcluded.forEach(excludedSuggestion => { if (!sugerenciasLugar[baseWord].some(s => s.word === excludedSuggestion.word && s.fuente === 'excluded')) { sugerenciasLugar[baseWord].push({...excludedSuggestion, fuente: 'excluded' }); //console.log(`[WME_PLN_TRACE_WORD] Añadida sugerencia 'excluded': "${excludedSuggestion.word}" para "${baseWord}"`); } }); } } if (endsWithComma && !tempReplaced.endsWith(",")) { tempReplaced += ","; } nombreSugeridoParcial.push(tempReplaced); }); //console.log(`[WME_PLN_TRACE] Fin procesamiento palabra por palabra. Nombre parcial: "${nombreSugeridoParcial.join(' ')}"`); // ---- FIN PROCESAMIENTO PALABRA POR PALABRA ---- // 6. --- COMPILACIÓN DE suggestedName --- console.log(`[WME_PLN_TRACE] Compilando nombre sugerido final...`); const joinedSuggested = nombreSugeridoParcial.join(' '); console.log(`[WME_PLN_TRACE] Nombre unido: "${joinedSuggested}"`); let processedName = joinedSuggested; if (applyGeneralReplacements) { console.log(`[WME_PLN_TRACE] Aplicando reemplazos generales a: "${processedName}"`); processedName = aplicarReemplazosGenerales(processedName); console.log(`[WME_PLN_TRACE] Después de reemplazos generales: "${processedName}"`); } console.log(`[WME_PLN_TRACE] Aplicando reglas especiales a: "${processedName}"`); processedName = aplicarReglasEspecialesNombre(processedName); console.log(`[WME_PLN_TRACE] Después de reglas especiales: "${processedName}"`); console.log(`[WME_PLN_TRACE] Aplicando post-procesamiento de comillas/paréntesis a: "${processedName}"`); processedName = postProcessQuotesAndParentheses(processedName); console.log(`[WME_PLN_TRACE] Después de post-procesamiento comillas/paréntesis: "${processedName}"`); if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0) { console.log(`[WME_PLN_TRACE] Aplicando reemplazos definidos a: "${processedName}"`); processedName = aplicarReemplazosDefinidos(processedName, replacementWords); console.log(`[WME_PLN_TRACE] Después de reemplazos definidos: "${processedName}"`); } let suggestedName = processedName.replace(/\s{2,}/g, ' ').trim(); console.log(`[WME_PLN_TRACE] Nombre sugerido después de trim/espacios múltiples: "${suggestedName}"`); if (suggestedName.endsWith('.')) { suggestedName = suggestedName.slice(0, -1); console.log(`[WME_PLN_TRACE] Nombre sugerido después de quitar punto final: "${suggestedName}"`); } console.log(`[WME_PLN_TRACE] Nombre Original: "${originalName}", Sugerido Final (antes de skip): "${suggestedName}"`); // 7. --- LÓGICA DE SALTO (SKIP) CONSOLIDADA --- console.log(`[WME_PLN_TRACE] Evaluando lógica de salto...`); const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0; let shouldSkipThisPlace = false; let skipReasonLog = ""; if (originalName.trim() === suggestedName.trim()) { shouldSkipThisPlace = true; skipReasonLog = `[SKIP EXACT MATCH]`; } else if (chkHideMyEditsChecked && wasEditedByMe) { shouldSkipThisPlace = true; skipReasonLog = `[SKIP MY EDIT]`; } 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]`; } } console.log(`[WME_PLN_TRACE] Decisión de salto: ${shouldSkipThisPlace} (${skipReasonLog})`); // ---- FIN LÓGICA DE SALTO --- // 8. Registrar o no en la lista de inconsistentes if (shouldSkipThisPlace) { if (skipReasonLog) console.log(`[WME_PLN_TRACE] ${skipReasonLog} Descartado "${originalName}" porque es idéntico al nombre sugerido "${suggestedName}" o por otras reglas de salto.`); } else { console.log(`[WME_PLN_TRACE] Registrando lugar con inconsistencias...`); if (processingStepLabel) { processingStepLabel.textContent = "Registrando lugar(es) con inconsistencias..."; } let categoryNameToStore = "Sin categoría (Error)"; try { console.log(`[WME_PLN_TRACE] Obteniendo nombre de categoría...`); categoryNameToStore = getPlaceCategoryName(venueFromOldModel, venueSDK); console.log(`[WME_PLN_TRACE] Nombre de categoría obtenido: "${categoryNameToStore}"`); } catch (e) { console.error("[WME_PLN_TRACE] Error llamando a getPlaceCategoryName:", e); } let cityInfo = { icon: "❓", title: "Sin ciudad (Error al obtener)", hasCity: false }; try { console.log(`[WME_PLN_TRACE] Obteniendo información de ciudad...`); cityInfo = await getPlaceCityInfo(venueFromOldModel, venueSDK); console.log(`[WME_PLN_TRACE] Información de ciudad obtenida: icon='${cityInfo.icon}', title='${cityInfo.title}', hasCity=${cityInfo.hasCity}`); } catch (e) { console.error(`[WME_PLN_TRACE] Error al obtener información de la ciudad para el venue ID ${currentVenueId}:`, e); } inconsistents.push({ id : currentVenueId, original : originalName, normalized : suggestedName, category : categoryNameToStore, editor : resolvedEditorName, cityIcon: cityInfo.icon, cityTitle: cityInfo.title, hasCity: cityInfo.hasCity, venueSDKForRender: venueSDK // <--- AÑADIR ESTO }); sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar; console.log(`[WME_PLN_TRACE] Lugar añadido a inconsistentes.`); } // 9. Finalizar procesamiento del 'place' actual y pasar al siguiente updateScanProgressBar(index, places.length); index++; console.log(`[WME_PLN_TRACE] === Fin processNextPlace para índice: ${index -1}, ID: ${currentVenueIdForLog}. Próximo índice: ${index} ===`); setTimeout(() => processNextPlace(), 0); // Continúa con el siguiente lugar } // ---- FIN DE LA FUNCIÓN processNextPlace ---- console.log("[WME_PLN_TRACE] Iniciando primer processNextPlace..."); try { setTimeout(() => { processNextPlace(); }, 10); } catch (error) { console.error("[WME_PLN_TRACE][ERROR_CRITICAL] Fallo al iniciar processNextPlace:", error, error.stack); // Aquí podrías intentar mostrar un mensaje al usuario indicando que el script falló críticamente. const outputFallback = document.querySelector("#wme-place-inspector-output"); if (outputFallback) { outputFallback.innerHTML = `<div style='color:red; padding:10px;'><b>Error Crítico:</b> El script de normalización encontró un problema grave y no pudo continuar. Revise la consola para más detalles (F12).<br>Detalles: ${error.message}</div>`; } const scanBtn = document.querySelector("button[type='button']"); // Asumiendo que es el botón de Start Scan if(scanBtn) { scanBtn.disabled = false; scanBtn.textContent = "Start Scan... (Error Previo)"; } if (window.processingDotsInterval) { clearInterval(window.processingDotsInterval); } } 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) { // 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 totalInconsistentsOriginal = inconsistents.length; // Guardar el total original let isLimited = false; // Declarar e inicializar isLimited if (totalInconsistentsOriginal > maxRenderLimit) { inconsistents = inconsistents.slice(0, maxRenderLimit); isLimited = true; // Establecer isLimited a true si se aplica el límite 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>${ totalInconsistentsOriginal}</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 } } // --- INICIO: Mostrar contador de registros --- const resultsCounter = document.createElement("div"); resultsCounter.style.fontSize = "13px"; resultsCounter.style.color = "#555"; // Color base para el texto normal resultsCounter.style.marginBottom = "8px"; resultsCounter.style.textAlign = "left"; if (totalInconsistentsOriginal > 0) { if (isLimited) { resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando las primeras <span style="color: #ff0000;"><b>${inconsistents.length}</b></span> (límite de ${maxRenderLimit} aplicado).`; } else { resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando <span style="color: #ff0000;"><b>${inconsistents.length}</b></span>.`; } } else { // No se añaden resultados a la tabla si no hay inconsistencias, // pero el mensaje de "Todos los nombres... están correctamente normalizados" se manejará más abajo. } if (output && totalInconsistentsOriginal > 0) // Solo añadir si se encontraron inconsistencias originalmente { output.appendChild(resultsCounter); } // Si no hay inconsistencias, mostrar mensaje y salir (progreso visible) if (inconsistents.length === 0) // Esto ahora significa que o no había nada, o se limitó a 0 (aunque es improbable con el límite de 30) { // Si totalInconsistentsOriginal también es 0, entonces realmente no había nada. if (totalInconsistentsOriginal === 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); } // Si llegamos aquí con inconsistents.length === 0 PERO totalInconsistentsOriginal > 0, // significa que el límite fue tan bajo que no se muestra nada, lo cual no debería pasar con un límite de 30 // a menos que el total original fuera menor que 30 y luego se filtraran todos por alguna razón. // En este caso, el contador ya habrá mostrado el mensaje adecuado. 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"); [ "Perma", "Tipo", "Ciudad", "Editor", "Nombre Actual", "Nombre Sugerido", "Sugerencias de reemplazo", "Categoría", // Mover aquí "Icon", // Mover aquí "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 = "center"; if (header === "Icon" || header === "Tipo") th.style.width = "65px"; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); 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"); // En el render de cada fila: inconsistents.forEach(({ id, original, normalized, category, editor, cityIcon, cityTitle, hasCity, venueSDKForRender }, 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.textAlign = "center"; // Centrar el ícono 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.textAlign = "center"; // Centrar el ícono typeCell.style.padding = "4px"; typeCell.style.width = "65px"; row.appendChild(typeCell); // Columna Ciudad const cityCell = document.createElement("td"); cityCell.textContent = cityIcon; // Mostrar el ícono cityCell.title = cityTitle; // Tooltip con más detalle cityCell.style.padding = "4px"; cityCell.style.textAlign = "center"; // Centrar el ícono cityCell.style.width = "65px"; // Ancho similar a "Tipo" row.appendChild(cityCell); // 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); // Columna Categoría const categoryCell = document.createElement("td"); const categoryName = venue ? getPlaceCategoryName(venue, venueSDKForRender) : "N/A"; categoryCell.textContent = categoryName; categoryCell.title = `Categoría: ${categoryName}`; categoryCell.style.padding = "4px"; categoryCell.style.width = "130px"; row.appendChild(categoryCell); // NUEVA COLUMNA: Icono de Categoría const iconCell = document.createElement("td"); const categoryInfo = getCategoryIcon(categoryName); iconCell.innerHTML = `<span title="${categoryInfo.title}" style="font-size: 20px;">${categoryInfo.icon}</span>`; iconCell.style.textAlign = "center"; iconCell.style.padding = "4px"; iconCell.style.width = "65px"; row.appendChild(iconCell); 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, () => { // Verificar si el panel flotante está visible if (floatingPanelElement && floatingPanelElement.style.display !== 'none') { waitForWazeAPI(() => { const places = getVisiblePlaces(); renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados setTimeout(reactivateAllActionButtons, 250); }); } else { console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea."); // Opcionalmente, solo resetear el estado del inspector si el panel no está visible // resetInspectorState(); // Descomentar si se desea este comportamiento } }); W.model.actionManager.events.register("afterredoaction", null, () => { // Verificar si el panel flotante está visible if (floatingPanelElement && floatingPanelElement.style.display !== 'none') { waitForWazeAPI(() => { const places = getVisiblePlaces(); renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados setTimeout(reactivateAllActionButtons, 250); }); } else { console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea."); // Opcionalmente, solo resetear el estado del inspector si el panel no está visible // resetInspectorState(); // Descomentar si se desea este comportamiento } }); // 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,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q==" 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 // Path: .vscode/Aprendiendo Javascript/wme_pln_6.2.2.js // ... existing code ... // 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" // ESTA LÍNEA SE ELIMINA O COMENTA, YA SE MANEJÓ ARRIBA // 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; // Se asegura que la palabra no sea "MI" o "DI" antes de considerarla un romano. if (romanRegexStrict.test(word) && word.toUpperCase() !== "MI" && word.toUpperCase() !== "DI" && word.toUpperCase() !== "SI") { 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á. // No debería afectar a "MI" si se maneja antes. normalizedWord = word; } else if (/^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1 && word.toUpperCase() !== "MI" && word.toUpperCase() !== "DI" && word.toUpperCase() !== "SI") { // ACRÓNIMOS: Si es todo mayúsculas (o números) Y tiene más de 1 caracter, Y NO ES MI/DI, mantener como está. 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 // Actualizado 2025-05-29 // 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 ""; // Casos especiales "MI" y "DI" tienen la MÁS ALTA prioridad. if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI" || word.toUpperCase() === "SI") { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } // Usar la regex insensible para la detección de romanos const romanRegexInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i; // Si es un número romano (y no es "MI" o "DI", aunque ya se cubrió arriba), convertir a mayúsculas. if (romanRegexInsensitive.test(word)) { // No es necesario verificar MI/DI aquí de nuevo debido a la primera condición. 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('.')) { // Mantener "St." dentro de comillas/paréntesis. No debería afectar a "MI". resultWord = word; } else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1) { // Mantener acrónimos sin puntos (ej. "ABC"). "MI" ya no caerá aquí. resultWord = word; } else { // Capitalización estándar para todo lo demás. resultWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } 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(); const currentUser = getLoggedInUserInfo(); if (currentUser) { console.log("Usuario Logueado ID:", currentUser.userId); console.log("Usuario Logueado Nombre:", currentUser.userName); // A usar currentUser.userName o currentUser.userId } else { console.log("No se pudo determinar el usuario logueado."); } })(); // 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); } //**************************************************************************************************** // Nombre: getCategoryIcon // Autor: mincho77 // Fecha: 2024-03-19 // Descripción: Obtiene el ícono correspondiente a una categoría de WME (bilingüe) //**************************************************************************************************** function getCategoryIcon(categoryName) { // Mapa de categorías a íconos con soporte bilingüe const categoryIcons = { // Comida y Restaurantes / Food & Restaurants "FOOD_AND_DRINK": { icon: "🦞🍷", es: "Comida y Bebidas", en: "Food and Drinks" }, "RESTAURANT": { icon: "🍽️", es: "Restaurante", en: "Restaurant" }, "FAST_FOOD": { icon: "🍔", es: "Comida rápida", en: "Fast Food" }, "CAFE": { icon: "☕", es: "Cafetería", en: "Cafe" }, "BAR": { icon: "🍺", es: "Bar", en: "Bar" }, "BAKERY": { icon: "🥖", es: "Panadería", en: "Bakery" }, "ICE_CREAM": { icon: "🍦", es: "Heladería", en: "Ice Cream Shop" }, "DEPARTMENT_STORE": { icon: "🏬", es: "Tienda por departamentos", en: "Department Store" }, "PARK": { icon: "🌳", es: "Parque", en: "Park" }, // Compras y Servicios / Shopping & Services "FASHION_AND_CLOTHING": { icon: "👗", es: "Moda y Ropa", en: "Fashion and Clothing" }, "SHOPPING_AND_SERVICES": { icon: "👜👝", es: "Mercado o Tienda", en: "Shopping and Services" }, "SHOPPING_CENTER": { icon: "🛍️", es: "Centro comercial", en: "Shopping Center" }, "SUPERMARKET_GROCERY": { icon: "🛒", es: "Supermercado", en: "Supermarket" }, "MARKET": { icon: "🛒", es: "Mercado", en: "Market" }, "CONVENIENCE_STORE": { icon: "🏪", es: "Tienda", en: "Convenience Store" }, "PHARMACY": { icon: "💊", es: "Farmacia", en: "Pharmacy" }, "BANK": { icon: "🏦", es: "Banco", en: "Bank" }, "ATM": { icon: "💳", es: "Cajero automático", en: "ATM" }, "HARDWARE_STORE": { icon: "🔧", es: "Ferretería", en: "Hardware Store" }, "COURTHOUSE": { icon: "⚖️", es: "Corte", en: "Courthouse" }, "FURNITURE_HOME_STORE": { icon: "🛋️", es: "Tienda de muebles", en: "Furniture Store" }, "TOURIST_ATTRACTION_HISTORIC_SITE": { icon: "🎡", es: "Atracción turística o Sitio histórico", en: "Tourist Attraction or Historic Site" }, "PET_STORE_VETERINARIAN_SERVICES": { icon: "🐶", es: "Tienda de mascotas o Veterinaria", en: "Pet Store or Veterinary Services" }, "CEMETERY": { icon: "🪦", es: "Cementerio", en: "Cemetery" }, "KINDERGARDEN": { icon: "🍼", es: "Jardín Infantil", en: "Kindergarten" }, "JUNCTION_INTERCHANGE": { icon: "🔀", es: "Cruce o Intercambio", en: "Junction or Interchange" }, "OUTDOORS": { icon: "🏞️", es: "Aire libre", en: "Outdoors" }, "ORGANIZATION_OR_ASSOCIATION": { icon: "👔", es: "Organización o Asociación", en: "Organization or Association" }, "TRAVEL_AGENCY": { icon: "🧳", es: "Agencia de viajes", en: "Travel Agency" }, "BANK_FINANCIAL": { icon: "💰", es: "Banco o Financiera", en: "Bank or Financial Institution" }, // Transporte / Transportation "TRAIN_STATION": { icon: "🚂", es: "Estación de tren", en: "Train Station" }, "GAS_STATION": { icon: "⛽", es: "Estación de servicio", en: "Gas Station" }, "PARKING_LOT": { icon: "🅿️", es: "Estacionamiento", en: "Parking Lot" }, "BUS_STATION": { icon: "🚍", es: "Terminal de bus", en: "Bus Station" }, "AIRPORT": { icon: "✈️", es: "Aeropuerto", en: "Airport" }, "CAR_WASH": { icon: "🚗💦", es: "Lavado de autos", en: "Car Wash" }, "FOREST_GROVE": { icon: "🌳", es: "Bosque", en: "Forest Grove" }, "GARAGE_AUTOMOTIVE_SHOP": { icon: "🔧🚗", es: "Taller mecánico", en: "Automotive Garage" }, "GIFTS": { icon: "🎁", es: "Tienda de regalos", en: "Gift Shop" }, "TOLL_BOOTH": { icon: "🚧", es: "Peaje", en: "Toll Booth" }, "CHARGING_STATION": { icon: "🔋", es: "Estación de carga", en: "Charging Station" }, "CAR_SERVICES": { icon: "🚗🔧", es: "Servicios de automóviles", en: "Car Services" }, "STADIUM_ARENA": { icon: "🏟️", es: "Estadio o Arena", en: "Stadium or Arena" }, "CAR_DEALERSHIP": { icon: "🚘🏢", es: "Concesionario de autos", en: "Car Dealership" }, // Alojamiento / Lodging "HOTEL": { icon: "🏨", es: "Hotel", en: "Hotel" }, "HOSTEL": { icon: "🛏️", es: "Hostal", en: "Hostel" }, "LODGING": { icon: "⛺", es: "Alojamiento", en: "Lodging" }, "MOTEL": { icon: "🛕", es: "Motel", en: "Motel" }, "SWIMMING_POOL": { icon: "🏊", es: "Piscina", en: "Swimming Pool" }, "RIVER_STREAM": { icon: "🌊", es: "Río o Arroyo", en: "River or Stream" }, "CAMPING_TRAILER_PARK": { icon: "🏕️", es: "Camping o Parque de Trailers", en: "Camping or Trailer Park" }, "SEA_LAKE_POOL": { icon: "🏖️", es: "Mar, Lago o Piscina", en: "Sea, Lake or Pool" }, // Salud / Healthcare "HOSPITAL": { icon: "🏥", es: "Hospital", en: "Hospital" }, "DOCTOR_CLINIC": { icon: "🏥⚕️", es: "Clínica", en: "Clinic" }, "DOCTOR": { icon: "👨⚕️", es: "Consultorio médico", en: "Doctor's Office" }, "VETERINARY": { icon: "🐾", es: "Veterinaria", en: "Veterinary" }, "PERSONAL_CARE": { icon: "💅💇", es: "Cuidado personal", en: "Personal Care" }, "FACTORY_INDUSTRIAL": { icon: "🏭", es: "Fábrica o Industrial", en: "Factory or Industrial" }, "MILITARY": { icon: "🪖", es: "Militar", en: "Military" }, // Educación / Education "UNIVERSITY": { icon: "🎓", es: "Universidad", en: "University" }, "COLLEGE_UNIVERSITY": { icon: "🏫", es: "Colegio", en: "College" }, "SCHOOL": { icon: "🎒", es: "Escuela", en: "School" }, "LIBRARY": { icon: "📖", es: "Biblioteca", en: "Library" }, "FLOWERS": { icon: "💐", es: "Floristería", en: "Flower Shop" }, "CONVENTIONS_EVENT_CENTER": { icon: "🎤🥂", es: "Centro de convenciones o eventos", en: "Convention or Event Center" }, "CLUB": { icon: "♣", es: "Club", en: "Club" }, // Entretenimiento / Entertainment "CINEMA": { icon: "🎬", es: "Cine", en: "Cinema" }, "THEATER": { icon: "🎭", es: "Teatro", en: "Theater" }, "MUSEUM": { icon: "🖼", es: "Museo", en: "Museum" }, "CULTURE_AND_ENTERTAINEMENT": { icon: "🎨", es: "Cultura y Entretenimiento", en: "Culture and Entertainment" }, "STADIUM": { icon: "🏟️", es: "Estadio", en: "Stadium" }, "GYM": { icon: "💪", es: "Gimnasio", en: "Gym" }, "GYM_FITNESS": { icon: "🏋️", es: "Gimnasio o Fitness", en: "Gym or Fitness" }, "GAME_CLUB": { icon: "⚽🏓", es: "Club de juegos", en: "Game Club" }, "BOOKSTORE": { icon: "📖📚", es: "Librería", en: "Bookstore" }, "ELECTRONICS": { icon: "📱💻", es: "Electrónica", en: "Electronics" }, "SPORTS_COURT": { icon: "⚽🏀", es: "Cancha deportiva", en: "Sports Court" }, "GOLF_COURSE": { icon: "⛳", es: "Campo de golf", en: "Golf Course" }, // Gobierno y Servicios Públicos / Government & Public Services "GOVERNMENT": { icon: "🏛️", es: "Oficina gubernamental", en: "Government Office" }, "POLICE_STATION": { icon: "👮", es: "Estación de policía", en: "Police Station" }, "FIRE_STATION": { icon: "🚒", es: "Estación de bomberos", en: "Fire Station" }, "POST_OFFICE": { icon: "📫", es: "Correo", en: "Post Office" }, "TRANSPORTATION": { icon: "🚌", es: "Transporte", en: "Transportation" }, // Religión / Religion "RELIGIOUS_CENTER": { icon: "⛪", es: "Iglesia", en: "Church" }, // Otros / Others "RESIDENTIAL": { icon: "🏘️", es: "Residencial", en: "Residential" }, "OFFICES": { icon: "🏢", es: "Oficina", en: "Office" }, "FACTORY": { icon: "🏭", es: "Fábrica", en: "Factory" }, "CONSTRUCTION_SITE": { icon: "🏗️", es: "Construcción", en: "Construction" }, "MONUMENT": { icon: "🗽", es: "Monumento", en: "Monument" }, "BRIDGE": { icon: "🌉", es: "Puente", en: "Bridge" }, "PROFESSIONAL_AND_PUBLIC": { icon: "🗄💼", es: "Profesional y Público", en: "Professional and Public" }, "OTHER": { icon: "🚪", es: "Otro", en: "Other" }, "ARTS_AND_CRAFTS": { icon: "🎨", es: "Artes y Manualidades", en: "Arts and Crafts" }, "COTTAGE_CABIN": { icon: "🏡", es: "Cabaña", en: "Cottage Cabin" }, "TELECOM": { icon: "📡", es: "Telecomunicaciones", en: "Telecommunications" } }; // Si no hay categoría, devolver ícono por defecto if (!categoryName) { return { icon: "❓", title: "Sin categoría / No category" }; } // Normalizar el nombre de la categoría const normalizedInput = categoryName.toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .trim(); console.log("[WME PLN DEBUG] Buscando ícono para categoría:", categoryName); console.log("[WME PLN DEBUG] Nombre normalizado:", normalizedInput); // 1. Buscar coincidencia exacta por clave interna (ej: "PARK") for (const [key, data] of Object.entries(categoryIcons)) { if (key.toLowerCase() === normalizedInput) { return { icon: data.icon, title: `${data.es} / ${data.en}` }; } } // Buscar coincidencia en el mapa de categorías for (const [key, data] of Object.entries(categoryIcons)) { // Normalizar los nombres en español e inglés para la comparación const normalizedES = data.es.toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .trim(); const normalizedEN = data.en.toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .trim(); if (normalizedInput === normalizedES || normalizedInput === normalizedEN) { return { icon: data.icon, title: `${data.es} / ${data.en}` }; } } // Si no se encuentra coincidencia, devolver ícono por defecto console.log("[WME PLN DEBUG] No se encontró coincidencia, usando ícono por defecto"); return { icon: "⚪", title: `${categoryName} (Sin coincidencia / No match)` }; }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址