// ==UserScript==
// @name WME Places Name Normalizer
// @namespace https://gf.qytechs.cn/en/users/mincho77
// @version 6.3.4
// @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
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// ==/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",
"Casas Club",
"Club Campestre",
"Club Residencial",
"Motel",
"Restaurante",
"Eco Hotel",
"Finca Hotel",
"CR",
"Hotel",
"Panadería y Pastelería",
"Panadería",
"Pastelería",
"Pizzería",
"Heladería",
"Carnicería",
"Supermercado",
"Odontólogo",
"Residencias",
"Ferretería",
"Peluquería"
];
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" .
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();
// REEMPLAZO DE BLOQUE DE CONDICIÓN SKIP NORMALIZED
const nombre = tempOriginalNormalized;
const normalizadoFinal = suggestedName;
const nombreClean = nombre.trim();
const normalizadoClean = normalizadoFinal.trim();
if (nombreClean === normalizadoClean) {
console.log("[DEBUG COMPARACIÓN] Detectados como iguales sin cambios:", nombreClean, "===", normalizadoClean);
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP NORMALIZED]`;
// No return porque estamos en un bloque, así que solo marcamos el skip
}
}
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);
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);
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);
}
// 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",
"Icon",
"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";
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' 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();
// evitar caracteres especiales solos
if(!/[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9]/.test(lowerW) || // No contiene letras ni números
/^[^a-zA-Z0-9]+$/.test(lowerW) ) // Solo tiene caracteres especiales
return;
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 => {
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 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";
const hideMyEditsLabel = document.createElement("label");
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;
hideMyEditsLabel.htmlFor = "chk-hide-my-edits";
hideMyEditsLabel.style.fontSize = "13px";
hideMyEditsLabel.style.cursor = "pointer";
hideMyEditsWrapper.appendChild(hideMyEditsCheckbox);
hideMyEditsWrapper.appendChild(hideMyEditsLabel);
// containerGeneral.appendChild(hideMyEditsWrapper);
const tabProgressWrapper = document.createElement("div");
tabProgressWrapper.style.margin = "10px 0";
tabProgressWrapper.style.height = "18px";
tabProgressWrapper.style.backgroundColor = "transparent";
const tabProgressBar = document.createElement("div");
tabProgressBar.style.height = "100%";
tabProgressBar.style.width = "0%";
tabProgressBar.style.backgroundColor = "#007bff";
tabProgressBar.style.transition = "width 0.2s";
tabProgressBar.id = "progressBarInnerTab";
tabProgressWrapper.appendChild(tabProgressBar);
containerGeneral.appendChild(tabProgressWrapper);
const tabProgressText = document.createElement("div");
tabProgressText.style.fontSize = "12px";
tabProgressText.style.marginTop = "5px";
tabProgressText.id = "progressBarTextTab";
tabProgressText.textContent = "Progreso: 0% (0/0)";
containerGeneral.appendChild(tabProgressText);
const outputNormalizationInTab = document.createElement("div");
outputNormalizationInTab.id = "wme-normalization-tab-output";
outputNormalizationInTab.style.fontSize = "12px";
outputNormalizationInTab.style.minHeight = "20px";
outputNormalizationInTab.style.padding = "5px";
outputNormalizationInTab.style.marginBottom = "15px";
outputNormalizationInTab.textContent =
"Presiona 'Start Scan...' para analizar los places visibles.";
containerGeneral.appendChild(outputNormalizationInTab);
}
else
{
console.error("[WME PLN] No se pudo poblar la pestaña 'General' porque su contenedor no existe.");
}
// 5. Poblar las otras pestañas
if (tabContents["Espe"])
{
createExcludedWordsManager(tabContents["Espe"]) ;
}
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'.");
}
if (tabContents["Dicc"])
{
createDictionaryManager(tabContents["Dicc"]);
}
else
{
console.error(
"[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Diccionario'.");
}
// --- LLAMADA A LA FUNCIÓN PARA POBLAR LA NUEVA PESTAÑA "Reemplazos"
// ---
if (tabContents["Reemp"])
{
createReplacementsManager(
tabContents["Reemp"]); // Esta es la llamada clave
}
else
{
console.error(
"[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'.");
}
}
catch (error)
{
console.error("[WME PLN] Error catastrófico creando la pestaña lateral:", error, error.stack);
}
} // Fin de createSidebarTab
function waitForSidebarAPI()
{
if (W && W.userscripts && W.userscripts.registerSidebarTab)
{
const savedExcluded = localStorage.getItem("excludedWordsList");
if (savedExcluded)
{
try
{
const parsed = JSON.parse(savedExcluded);
excludedWords = new Set(parsed);
/* console.log(
"[WME PLN] Palabras especiales restauradas desde
localStorage:", Array.from(excludedWords));*/
}
catch (e)
{
/*console.error(
"[WME PLN] Error al cargar excludedWordsList del localStorage:",
e);*/
excludedWords = new Set();
}
}
else
{
excludedWords = new Set();
/* console.log(
"[WME PLN] No se encontraron palabras especiales en
localStorage.");*/
}
// --- Cargar diccionario desde localStorage ---
const savedDictionary = localStorage.getItem("dictionaryWordsList");
if (savedDictionary)
{
try
{
const parsed = JSON.parse(savedDictionary);
window.dictionaryWords = new Set(parsed);
window.dictionaryIndex = {};
parsed.forEach(word => {
const letter = word.charAt(0).toLowerCase();
if (!window.dictionaryIndex[letter])
{
window.dictionaryIndex[letter] = [];
}
window.dictionaryIndex[letter].push(word);
});
/* console.log(
"[WME PLN] Diccionario restaurado desde localStorage:",
parsed);*/
}
catch (e)
{
/* console.error(
"[WME PLN] Error al cargar dictionaryWordsList del
localStorage:", e);*/
window.dictionaryWords = new Set();
}
}
else
{
window.dictionaryWords = new Set();
// console.log("[WME PLN] No se encontró diccionario en
// localStorage.");
}
loadReplacementWordsFromStorage();
waitForWazeAPI(() => { createSidebarTab(); });
}
else
{
// console.log("[WME PLN] Esperando W.userscripts API...");
setTimeout(waitForSidebarAPI, 1000);
}
}
// 1. MODIFICAR normalizePlaceName
function normalizePlaceName(word)
{
console.log("[NORMALIZER] Analizando nombre:", 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("/");
}
// Números romanos: todo en mayúsculas. No elimina puntos .
const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
if (romanRegexStrict.test(word) && 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('.'))
normalizedWord = word; // Mantener como está si contiene un punto
else if (/^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length <= 4 && word.toUpperCase() !== "MI" && word.toUpperCase() !== "DI" && word.toUpperCase() !== "SI") {
normalizedWord = word; // Mantener acrónimos cortos sin cambios (como EPS, EPM, SURA)
} else {
normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); // Capitalización estándar
}
// Detectar íconos/emoticonos y capitalizar la siguiente palabra
const iconRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
if (iconRegex.test(word)) {
normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
return normalizedWord;
}
//*****************************************************************************************************************************
function createCategoryDropdown(currentCategoryKey, rowIndex, venue)
{
const select = document.createElement("select");
select.style.padding = "4px";
select.style.borderRadius = "4px";
select.style.fontSize = "12px";
select.title = "Selecciona una categoría";
select.id = `categoryDropdown-${rowIndex}`;
Object.entries(categoryIcons).forEach(([key, value]) => {
const option = document.createElement("option");
option.value = key;
option.textContent = `${value.icon} ${value.en}`;
if (key === currentCategoryKey) {
option.selected = true;
}
select.appendChild(option);
});
// Evento: al cambiar la categoría
select.addEventListener("change", (e) => {
const selectedCategory = e.target.value;
if (!venue || !venue.model || !venue.model.attributes) {
console.error("Venue inválido al intentar actualizar la categoría");
return;
}
// Actualizar la categoría en el modelo
venue.model.attributes.categories = [selectedCategory];
venue.model.save();
// Mensaje opcional de confirmación
WazeWrap.Alerts.success("Categoría actualizada", `Nueva categoría: ${categoryIcons[selectedCategory].en}`);
});
return select;
}
//***************************************************************************
// 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 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á .
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.");
return;
}
window.dictionaryWords.add(lowerNewWord);
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;
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();
});
// 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" },
"SPORTING_GOODS": { icon: "🛼🏀🏐", es: "Artículos deportivos", en: "Sporting Goods" },
"TOY_STORE": { icon: "🧸", es: "Tienda de juguetes", en: "Toy Store" },
"CURRENCY_EXCHANGE": { icon: "💶💱", es: "Casa de cambio", en: "Currency Exchange" },
"PHOTOGRAPHY": { icon: "📸", es: "Fotografía", en: "Photography" },
"DESSERT": { icon: "🍰", es: "Postre", en: "Dessert" },
"FOOD_COURT": { icon: "🥗", es: "Comedor o Patio de comidas", en: "Food Court" },
"CANAL": { icon: "〰", es: "Canal", en: "Canal" },
"JEWELRY": { icon: "💍", es: "Joyería", en: "Jewelry" },
// 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" },
"TAXI_STATION": { icon: "🚕", es: "Estación de taxis", en: "Taxi Station" },
"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" },
"FERRY_PIER": { icon: "⛴️", es: "Muelle de ferry", en: "Ferry Pier" },
"INFORMATION_POINT": { icon: "ℹ️", es: "Punto de información", en: "Information Point" },
"REST_AREAS": { icon: "🏜", es: "Áreas de descanso", en: "Rest Areas" },
"MUSIC_VENUE": { icon: "🎶", es: "Lugar de música", en: "Music Venue" },
"CASINO": { icon: "🎰", es: "Casino", en: "Casino" },
"CITY_HALL": { icon: "🎩", es: "Ayuntamiento", en: "City Hall" },
"PERFORMING_ARTS_VENUE": { icon: "🎭", es: "Lugar de artes escénicas", en: "Performing Arts Venue" },
"TUNNEL": { icon: "🔳", es: "Túnel", en: "Tunnel" },
"SEAPORT_MARINA_HARBOR": { icon: "⚓", es: "Puerto o Marina", en: "Seaport or Marina" },
// 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" },
"FARM": { icon: "🚜", es: "Granja", en: "Farm" },
"NATURAL_FEATURES": { icon: "🌲", es: "Características naturales", en: "Natural Features" },
// Salud / Healthcare
"HOSPITAL": { icon: "🏥", es: "Hospital", en: "Hospital" },
"HOSPITAL_URGENT_CARE": { icon: "🏥🚑", es: "Urgencias", en: "Urgent Care" },
"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" },
"LAUNDRY_DRY_CLEAN": { icon: "🧺", es: "Lavandería o Tintorería", en: "Laundry or Dry Clean" },
"PLAYGROUND": { icon: "🛝", es: "Parque infantil", en: "Playground" },
// 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" },
"ART_GALLERY": { icon: "🖼️", es: "Galería de arte", en: "Art Gallery" },
"NATURAL_FEATURES": { icon: "🌄", es: "Características naturales", en: "Natural Features" },
// 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" },
"SKI_AREA": { icon: "⛷️", es: "Área de esquí", en: "Ski Area" },
// 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" },
"FIRE_DEPARTMENT": { icon: "🚒", es: "Departamento de bomberos", en: "Fire Department" },
"POST_OFFICE": { icon: "📫", es: "Correo", en: "Post Office" },
"TRANSPORTATION": { icon: "🚌", es: "Transporte", en: "Transportation" },
"PRISON_CORRECTIONAL_FACILITY": { icon: "👁️🗨️", es: "Prisión o Centro Correccional", en: "Prison or Correctional Facility" },
// Religión / Religion
"RELIGIOUS_CENTER": { icon: "⛪", es: "Iglesia", en: "Church" },
// Otros / Others
"RESIDENTIAL": { icon: "🏘️", es: "Residencial", en: "Residential" },
"RESIDENCE_HOME": { icon: "🏠", es: "Residencia o Hogar", en: "Residence or Home" },
"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)`
};
}
/*
function normalizePlaceName(nombre) {
console.log("[NORMALIZER] Analizando nombre:", nombre);
var palabrasExcluidas = [];
if (typeof excludedWords !== "undefined") {
if (Array.isArray(excludedWords)) {
palabrasExcluidas = excludedWords;
} else if (typeof excludedWords.forEach === "function") {
palabrasExcluidas = Array.from(excludedWords);
}
}
console.log("[NORMALIZER] Lista excluidas:", palabrasExcluidas);
var normalizadoFinal = nombre;
// Nueva condición de salto más estricta ANTES del log de SKIP NORMALIZED
const nombreClean = nombre.trim();
const normalizadoClean = normalizadoFinal.trim();
if (nombreClean === normalizadoClean) {
console.log("[DEBUG COMPARACIÓN] Detectados como iguales sin cambios:", nombreClean, "===", normalizadoClean);
return null;
}
// console.log(`[WME_PLN_TRACE] [SKIP NORMALIZED] Descartado "${nombre}" porque es idéntico al nombre sugerido "${normalizadoFinal}" o por otras reglas de salto.`);
// Justo antes de retornar:
console.log("[NORMALIZER] Resultado para", nombre, "=>", normalizadoFinal);
return {
original: nombre,
normalizado: normalizadoFinal
};
}*/
//****************************************************************************************************
// Nombre: addWordToDictionary
// Descripción: Captura el valor del campo de texto y agrega la palabra al diccionario en minúsculas.
//****************************************************************************************************
function addWordToDictionary(input) {
// Forzar que la palabra se almacene en minúscula
const newWord = input.value.trim().toLowerCase();
// ... resto del código para agregar la palabra ...
// Ejemplo:
if (!window.dictionaryWords) window.dictionaryWords = new Set();
window.dictionaryWords.add(newWord);
// Limpiar el input
input.value = "";
}