// ==UserScript==
// @name WME Places Name Normalizer
// @namespace https://gf.qytechs.cn/en/users/mincho77
// @version 6.1.1
// @description Normaliza nombres de lugares en Waze Map Editor (WME)
// @license MIT
// @include https://beta.waze.com/*
// @include https://www.waze.com/editor*
// @include https://www.waze.com/*/editor*
// @exclude https://www.waze.com/user/editor*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
// Variables globales básicas
const SCRIPT_NAME = GM_info.script.name;
const VERSION = GM_info.script.version.toString();
const commonWords = [
'de', 'del', 'el', 'la', 'los', 'las', 'y', 'e',
'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en',
'con', 'sin', 'sobre', 'tras', 'por', 'al', 'lo'
];
// --- Definir nombres de pestañas cortos antes de la generación de botones ---
const tabNames = [
{ label: "Gene", icon: "⚙️" },
{ label: "Espe", icon: "🏷️" },
{ label: "Dicc", icon: "📘" },
{ label: "Reemp", icon: "🔂" }
];
let wmeSDK = null; // Declarar wmeSDK global
function tryInitializeSDK(finalCallback)
{
let attempts = 0;
const maxAttempts = 60; // Aumentado a 30 segundos (60 * 500ms)
const intervalTime = 500;
let sdkAttemptInterval = null; // Guardar la referencia al intervalo
// console.log("[SDK INIT ATTEMPT] Iniciando intentos para obtener getWmeSdk().");
function attempt()
{
// console.log(`[SDK INIT ATTEMPT] Intento #${attempts + 1}`);
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(); // Llama al callback final
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(); // Llama al callback final igualmente
}
}
// Iniciar el intervalo para los reintentos.
sdkAttemptInterval = setInterval(attempt, intervalTime);
attempt();
}
function waitForWazeAPI(callbackPrincipalDelScript)
{
let wAttempts = 0;
const wMaxAttempts = 40;
const wInterval = setInterval(() => {
wAttempts++;
if (typeof W !== 'undefined' && W.map && W.loginManager && W.model &&
W.model.venues && W.userscripts &&
typeof W.userscripts.registerSidebarTab === 'function')
{
clearInterval(wInterval);
// console.log("✅ Waze API (objeto W) cargado correctamente.");
tryInitializeSDK(callbackPrincipalDelScript);
}
else if (wAttempts >= wMaxAttempts)
{
clearInterval(wInterval);
// console.error("Error: No se pudo cargar la API principal de Waze (objeto W) después de varios intentos.");
callbackPrincipalDelScript();
}
}, 500);
}
function updateScanProgressBar(currentIndex, totalPlaces)
{
if (totalPlaces === 0)
{
return;
}
let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100);
progressPercent = Math.min(progressPercent, 100);
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = `${progressPercent}%`;
const currentDisplay = Math.min(currentIndex + 1, totalPlaces);
progressBarTextTab.textContent =
`Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`;
}
}
// Función para escapar caracteres especiales en expresiones regulares
function escapeRegExp(string)
{
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Función para eliminar tildes/diacríticos de una cadena
function removeDiacritics(str)
{
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
// Función para calcular la similitud entre dos cadenas
function isValidExcludedWord(newWord)
{
if (!newWord)
return { valid : false, msg : "La palabra no puede estar vacía." };
const lowerNewWord = newWord.toLowerCase();
// No permitir palabras de un solo caracter
if (newWord.length === 1)
{
return {
valid : false,
msg : "No se permite agregar palabras de un solo caracter."
};
}
// No permitir caracteres especiales solos
if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord))
{
return {
valid : false,
msg : "No se permite agregar solo caracteres especiales."
};
}
// No permitir si ya está en el diccionario (ignorando mayúsculas)
if (window.dictionaryWords &&
Array.from(window.dictionaryWords)
.some(w => w.toLowerCase() === lowerNewWord))
{
return {
valid : false,
msg :
"La palabra ya existe en el diccionario. No se puede agregar a especiales."
};
}
// No permitir palabras comunes
if (commonWords.includes(lowerNewWord))
{
return {
valid : false,
msg : "Esa palabra es muy común y no debe agregarse a la lista."
};
}
// No permitir duplicados en excluidas
if (excludedWords &&
Array.from(excludedWords).some(w => w.toLowerCase() === lowerNewWord))
{
return {
valid : false,
msg : "La palabra ya está en la lista (ignorando mayúsculas)."
};
}
return { valid : true };
}
// Función para calcular la similitud entre dos cadenas
function aplicarReemplazosGenerales(name)
{
if (typeof window.skipGeneralReplacements === "boolean" &&
window.skipGeneralReplacements)
{
return name;
}
const reglas = [
// Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor
{ buscar : /\s*\/\s*/g, reemplazar : " / " },
{ buscar : /\[P\]|\[p\]/g, reemplazar : "" },
{ buscar : /\s*-\s*/g, reemplazar : " - " },
];
reglas.forEach(
regla => { name = name.replace(regla.buscar, regla.reemplazar); });
name = name.replace(/\s{2,}/g, ' ');
return name;
}
function aplicarReglasEspecialesNombre(newName)
{
newName = newName.replace(/([A-Za-z])'([A-Za-z])/g,
(match, before, after) =>
`${before}'${after.toLowerCase()}`);
newName = newName.replace(/-\s*([a-z])/g,
(match, letter) => `- ${letter.toUpperCase()}`);
newName = newName.replace(/\.\s+([a-z])/g,
(match, letter) => `. ${letter.toUpperCase()}`);
// --- INICIO: NUEVA REGLA PARA CAPITALIZAR DESPUÉS DE PARÉNTESIS DE APERTURA ---
newName = newName.replace(/(\(\s*)([a-z])/g,
(match, P1, P2) => {
// P1 es el paréntesis de apertura y cualquier espacio después (ej. "( " o "(")
// P2 es la primera letra minúscula después de eso.
return P1 + P2.toUpperCase();
}
);
// --- FIN: NUEVA REGLA ---
const frasesAlFinal = [
"Conjunto Residencial",
"Urbanización",
"Conjunto Cerrado",
"Unidad Residencial",
"Parcelación",
"Condominio Campestre",
"Condominio",
"Edificio",
"Conjunto Habitacional",
"Apartamentos",
"Club Campestre",
"Club Residencial",
"Motel",
"Hotel",
"Restaurante"
];
for (const frase of frasesAlFinal)
{
const regex = new RegExp(`\\s+${escapeRegExp(frase)}$`, 'i');
if (regex.test(newName))
{
const match = newName.match(regex);
const fraseEncontrada = match[0].trim();
const restoDelNombre = newName.replace(regex, '').trim();
newName = `${fraseEncontrada} ${restoDelNombre}`;
break;
}
}
newName = newName.replace(/\s([a-zA-Z])$/,
(match, letter) => ` ${letter.toUpperCase()}`);
return newName;
}
function aplicarReemplazosDefinidos(text, replacementRules) {
let newText = text;
// Verificar si replacementRules es un objeto y tiene claves
if (typeof replacementRules !== 'object' || replacementRules === null || Object.keys(replacementRules).length === 0) {
return newText; // No hay reglas o no es un objeto, devolver el texto original
}
// Ordenar las claves de reemplazo por longitud descendente para manejar correctamente
// los casos donde una clave puede ser subcadena de otra (ej. "D1 Super" y "D1").
const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);
for (const fromKey of sortedFromKeys) {
const toValue = replacementRules[fromKey];
// Crear una expresión regular para encontrar 'fromKey' como palabra completa (usando \b),
// de forma global ('g') e insensible a mayúsculas/minúsculas ('i').
// escapeRegExp es crucial si 'fromKey' puede contener caracteres especiales de regex.
const regex = new RegExp('\\b' + escapeRegExp(fromKey) + '\\b', 'gi');
newText = newText.replace(regex, (matchedInText, offset, originalString) => {
// Parámetros del callback de replace:
// matchedInText: La cadena exacta que coincidió en el texto (ej. "d1", "D1", etc.).
// offset: La posición inicial de 'matchedInText' en 'originalString'.
// originalString: La cadena completa sobre la que se está haciendo el reemplazo en esta iteración.
// Convertimos a minúsculas para la lógica de comparación y búsqueda de índice.
const fromKeyLower = fromKey.toLowerCase(); // La clave canónica de tus reglas, en minúsculas.
const toValueLower = toValue.toLowerCase(); // El valor de reemplazo canónico, en minúsculas.
// Encontrar dónde estaría 'fromKeyLower' dentro de 'toValueLower'.
const indexOfFromInTo = toValueLower.indexOf(fromKeyLower);
if (indexOfFromInTo !== -1) {
// Si 'fromKey' es parte de 'toValue' (ej. from="D1", to="Tiendas D1").
// Calculamos el inicio de donde el 'toValue' completo podría ya existir en 'originalString',
// alineándolo con la posición actual de 'matchedInText'.
const potentialExistingToStart = offset - indexOfFromInTo;
// Verificar que el substring potencial esté dentro de los límites de originalString.
if (potentialExistingToStart >= 0 &&
(potentialExistingToStart + toValue.length) <= originalString.length) {
// Extraer el substring del texto original que podría ser el 'toValue' ya existente.
const substringInOriginal = originalString.substring(potentialExistingToStart, potentialExistingToStart + toValue.length);
// Comparamos (insensible a mayúsculas) si el 'toValue' ya existe en esa posición.
if (substringInOriginal.toLowerCase() === toValueLower) {
// ¡Coincidencia! El 'toValue' completo ya está presente y contextualiza
// correctamente al 'matchedInText'. No reemplazamos para evitar duplicados.
// Devolvemos el texto que coincidió originalmente sin cambios.
return matchedInText;
}
}
}
// Si no se cumplió la condición de "ya existe contextualizado", realizamos el reemplazo.
// Se usa 'toValue' (con su capitalización original definida en tus reglas).
return toValue;
});
}
return newText;
}
function getVisiblePlaces()
{
if (typeof W === 'undefined' || !W.map || !W.model || !W.model.venues)
{
console.warn('Waze Map Editor no está completamente cargado.');
return [];
}
const venues = W.model.venues.objects;
const visiblePlaces = Object.values(venues).filter(venue => {
const olGeometry = venue.getOLGeometry?.();
const bounds = olGeometry?.getBounds?.();
return bounds && W.map.getExtent().intersectsBounds(bounds);
});
return visiblePlaces;
}
function renderPlacesInFloatingPanel(places)
{
createFloatingPanel(0); // Asegurar que el panel flotante exista antes de renderizar
const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10);
if (places.length > maxPlacesToScan)
{
places = places.slice(0, maxPlacesToScan);
}
// console.log("[DEBUG] Preparando panel flotante. Total places a analizar:",places.length);
// --- Funciones auxiliares para tipo y categoría ---
function getPlaceCategoryName(venueFromOldModel, venueSDKObject)
{ // Acepta ambos tipos de venue
let categoryId = null;
let categoryName = null;
let source = ""; // Para saber de dónde vino la info
// Intento 1: Usar el venueSDKObject si está disponible y tiene la info
if (venueSDKObject)
{
// Suposición 1: El SDK tiene una propiedad 'mainCategory' que es un
// objeto con 'id' y 'name'
if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id)
{
categoryId = venueSDKObject.mainCategory.id;
if (venueSDKObject.mainCategory.name)
{
categoryName = venueSDKObject.mainCategory.name;
source = "SDK (mainCategory.name)";
}
}
// Suposición 2: El SDK tiene un array 'categories', y el primer
// elemento tiene 'id' y '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)";
}
}
// Suposición 3: El primer elemento del array 'categories' es
// directamente el nombre
else if (typeof firstCategorySDK === 'string')
{
categoryName = firstCategorySDK;
source = "SDK (categories[0] as string)";
// No tendríamos ID en este caso desde esta fuente directa.
}
}
// Suposición 4: El SDK tiene solo el ID de la categoría principal
// en una propiedad simple
else if (venueSDKObject.primaryCategoryID)
{ // Nombre de propiedad hipotético
categoryId = venueSDKObject.primaryCategoryID;
source = "SDK (primaryCategoryID)";
}
// (Añade más suposiciones aquí basándote en lo que veas en
// console.dir(venueSDKObject))
}
// Si tenemos un nombre de categoría del SDK, lo usamos.
if (categoryName)
{
// console.log(`[CATEGORÍA] Usando nombre de categoría de ${source}: ${categoryName} ${categoryId ? `(ID: ${categoryId})` : ''}`);
return categoryName;
}
// Intento 2: Si no obtuvimos nombre del SDK pero sí un ID, o si no
// usamos el SDK, usamos W.model Primero, obtener el categoryId si aún
// no lo tenemos (del venueFromOldModel)
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";
}
// Ahora, resolver el categoryId (sea del SDK o de W.model) usando
// W.model.categories
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}`);
return categoryObjWModel.attributes.name;
}
// Fallback final: si solo tenemos el ID y no pudimos resolver el nombre
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.`);
return `${categoryId}`;
}
return "Sin categoría";
}
function getPlaceTypeInfo(venue)
{
const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null;
const isArea = geometry?.CLASS_NAME?.endsWith("Polygon");
return {
isArea,
icon : isArea ? "⭔" : "⊙",
title : isArea ? "Área" : "Punto"
};
}
// --- Renderizar barra de progreso en el TAB PRINCIPAL justo después del
// slice ---
const tabOutput = document.querySelector("#wme-normalization-tab-output");
if (tabOutput)
{
// Reiniciar el estilo del mensaje en el tab al valor predeterminado
tabOutput.style.color = "#000";
tabOutput.style.fontWeight = "normal";
// Crear barra de progreso visual
const progressBarWrapperTab = document.createElement("div");
progressBarWrapperTab.style.margin = "10px 0";
progressBarWrapperTab.style.marginTop = "10px";
progressBarWrapperTab.style.height = "18px";
progressBarWrapperTab.style.backgroundColor = "transparent";
const progressBarTab = document.createElement("div");
progressBarTab.style.height = "100%";
progressBarTab.style.width = "0%";
progressBarTab.style.backgroundColor = "#007bff";
progressBarTab.style.transition = "width 0.2s";
progressBarTab.id = "progressBarInnerTab";
progressBarWrapperTab.appendChild(progressBarTab);
const progressTextTab = document.createElement("div");
progressTextTab.style.fontSize = "12px";
progressTextTab.style.marginTop = "5px";
progressTextTab.id = "progressBarTextTab";
tabOutput.appendChild(progressBarWrapperTab);
tabOutput.appendChild(progressTextTab);
}
// Asegurar que la barra de progreso en el tab se actualice desde el
// principio
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "0%";
progressBarTextTab.textContent = `Progreso: 0% (0/${places.length})`;
}
// Mostrar el panel flotante desde el inicio
// createFloatingPanel(0); // Mostrar el panel flotante desde el inicio
// --- PANEL FLOTANTE: limpiar y preparar salida ---
const output = document.querySelector("#wme-place-inspector-output");
if (!output)
{
console.error("❌ Panel flotante no está disponible");
return;
}
output.innerHTML =
""; // Limpia completamente el contenido del panel flotante
output.innerHTML =
"<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:11px; color:#555;'>Inicializando escaneo...</div></div></div>";
// Animación de puntos suspensivos
const dotsSpan = output.querySelector(".dots");
if (dotsSpan)
{
const dotStates = [ "", ".", "..", "..." ];
let dotIndex = 0;
window.processingDotsInterval = setInterval(() => {
dotIndex = (dotIndex + 1) % dotStates.length;
dotsSpan.textContent = dotStates[dotIndex];
}, 500);
}
output.style.height = "calc(55vh - 40px)"; // Altura para el panel flotante
// Si no hay places, mostrar mensaje y salir
if (!places.length)
{
output.appendChild(
document.createTextNode("No hay places visibles para analizar."));
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
existingOverlay.remove();
return;
}
// --- Procesamiento incremental para evitar congelamiento ---
let inconsistents = [];
let index = 0;
// Remover ícono de ✔ previo si existe
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
const existingCheck = scanBtn.querySelector("span");
if (existingCheck)
{
existingCheck.remove();
}
}
// --- Sugerencias por palabra global para toda la ejecución ---
let sugerenciasPorPalabra = {};
// Convertir excludedWords a array solo una vez al inicio del análisis,
// seguro ante undefined
const excludedArray =
(typeof excludedWords !== "undefined" && Array.isArray(excludedWords))
? excludedWords
: (typeof excludedWords !== "undefined" ? Array.from(excludedWords)
: []);
async function processNextPlace()
{
// 1. Leer estados de checkboxes y configuraciones iniciales
const hideMyEdits = false;
const useFullPipeline =
true; // Como definimos, siempre ejecutar el flujo completo
const applyGeneralReplacements =
useFullPipeline ||
(document.getElementById("chk-general-replacements")?.checked ??
true); // Asume que este ID podría existir si reañades el check
const checkExcludedWords =
useFullPipeline ||
(document.getElementById("chk-check-excluded")?.checked ?? false);
const checkDictionaryWords =
true; // O leer de checkbox si lo reintroduces
const restoreCommas =
document.getElementById("chk-restore-commas")?.checked ?? false;
const similarityThreshold =
parseFloat(document.getElementById("similarityThreshold")?.value ||
"85") /
100;
// 2. Condición de salida principal (todos los lugares procesados)
if (index >= places.length)
{
//console.log("[DEBUG] Todos los lugares procesados. Finalizando render...");
finalizeRender(inconsistents, places);
return;
}
const venueFromOldModel =
places[index]; // Usaremos este para datos base y fallback
const currentVenueNameObj = venueFromOldModel?.attributes?.name;
const nameValue = typeof currentVenueNameObj === 'object' && currentVenueNameObj !== null &&
typeof currentVenueNameObj.value === 'string' ? currentVenueNameObj.value.trim() !== ''
? currentVenueNameObj.value : undefined : typeof currentVenueNameObj === 'string' &&
currentVenueNameObj.trim() !== '' ? currentVenueNameObj : undefined;
// Protección extra si el place en el índice es inválido
if (!places[index] || typeof places[index] !== 'object') {
// console.warn(`[DEBUG] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]);
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
return;
}
// 3. Salto temprano si el venue es inválido o no tiene nombre
if (!venueFromOldModel || typeof venueFromOldModel !== 'object' ||
!venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '')
{
//console.warn(`[DEBUG] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
return;
}
const originalName = venueFromOldModel?.attributes?.name?.value ||
venueFromOldModel?.attributes?.name || '';
const currentVenueId = venueFromOldModel.getID();
// 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (PRIORIZANDO
// SDK) ---
let lastEditorInfoForLog = "Editor: Desconocido";
let lastEditorIdForComparison = null;
let wasEditedByMe = false;
let currentLoggedInUserId = null;
let currentLoggedInUserName =
null; // Para comparar si el SDK devuelve nombre
if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user)
{
if (typeof W.loginManager.user.id === 'number')
{
currentLoggedInUserId = W.loginManager.user.id;
}
if (typeof W.loginManager.user.userName === 'string')
{
currentLoggedInUserName = W.loginManager.user.userName;
}
}
if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues &&
wmeSDK.DataModel.Venues.getById)
{
// console.log(`[SDK] Intentando obtener venue ID: ${currentVenueId} con sdk.DataModel.Venues.getById`);
try
{
const venueSDK = await wmeSDK.DataModel.Venues.getById(
{ venueId : currentVenueId });
if (venueSDK && venueSDK.modificationData)
{
const updatedByDataFromSDK =
venueSDK.modificationData.updatedBy;
/* console.log(
`[SDK] Para ID ${
currentVenueId}, venue.modificationData.updatedBy: `,
updatedByDataFromSDK,
`(Tipo: ${typeof updatedByDataFromSDK})`);*/
if (typeof updatedByDataFromSDK === 'string' &&
updatedByDataFromSDK.trim() !== '')
{
lastEditorInfoForLog =
`Editor (SDK): ${updatedByDataFromSDK}`;
resolvedEditorName =
updatedByDataFromSDK; // SDK provided username
// directly
if (currentLoggedInUserName &&
currentLoggedInUserName === updatedByDataFromSDK)
{
wasEditedByMe = true;
}
}
else if (typeof updatedByDataFromSDK === 'number')
{
lastEditorInfoForLog =
`Editor (SDK): ID ${updatedByDataFromSDK}`;
resolvedEditorName =
`ID ${updatedByDataFromSDK}`; // Default to ID
lastEditorIdForComparison = updatedByDataFromSDK;
if (typeof W !== 'undefined' && W.model &&
W.model.users)
{
const userObjectW =
W.model.users.getObjectById(updatedByDataFromSDK);
if (userObjectW && userObjectW.userName)
{
lastEditorInfoForLog = `Editor (SDK ID ${
updatedByDataFromSDK} -> W.model): ${
userObjectW.userName}`;
resolvedEditorName =
userObjectW
.userName; // Username found via W.model
}
else if (userObjectW)
{
lastEditorInfoForLog = `Editor (SDK ID ${
updatedByDataFromSDK} -> W.model): ID ${
updatedByDataFromSDK} (sin userName en W.model)`;
// resolvedEditorName remains `ID
// ${updatedByDataFromSDK}`
}
}
}
else if (updatedByDataFromSDK === null)
{
lastEditorInfoForLog =
"Editor (SDK): N/D (updatedBy es null)";
resolvedEditorName = "N/D";
}
else
{
lastEditorInfoForLog =
`Editor (SDK): Valor inesperado para updatedBy ('${
updatedByDataFromSDK}')`;
resolvedEditorName = "Inesperado (SDK)";
}
}
else
{ // venueSDK o venueSDK.modificationData no encontrados
lastEditorInfoForLog =
"Editor (SDK): No venue/modificationData. Usando fallback W.model.";
if (venueSDK)
console.log(
"[SDK] venueSDK.modificationData no encontrado. Venue SDK:",
venueSDK);
else
console.log("[SDK] venueSDK no fue obtenido para ID:", currentVenueId);
const oldModelUpdatedBy =
venueFromOldModel.attributes.updatedBy;
if (oldModelUpdatedBy !== null &&
oldModelUpdatedBy !== undefined)
{
lastEditorIdForComparison = oldModelUpdatedBy;
resolvedEditorName =
`ID ${oldModelUpdatedBy}`; // Default to ID
let usernameFromOldModel = `ID ${oldModelUpdatedBy}`;
if (typeof W !== 'undefined' && W.model &&
W.model.users)
{
const userObjectW =
W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
{
usernameFromOldModel = userObjectW.userName;
resolvedEditorName =
userObjectW.userName; // Username found
}
else if (userObjectW)
{
usernameFromOldModel =
`ID ${oldModelUpdatedBy} (sin userName)`;
}
}
lastEditorInfoForLog =
`Editor (W.model Fallback): ${usernameFromOldModel}`;
}
else
{
lastEditorInfoForLog = "Editor (W.model Fallback): N/D";
resolvedEditorName = "N/D";
}
}
}
catch (e)
{
// console.error(`[SDK] Error al obtener venue ${currentVenueId} con el SDK:`, e);
lastEditorInfoForLog = "Editor: Error con SDK (usando fallback W.model)";
resolvedEditorName = "Error SDK"; // Initial state for error
const oldModelUpdatedBy =
venueFromOldModel.attributes.updatedBy;
if (oldModelUpdatedBy !== null &&
oldModelUpdatedBy !== undefined)
{
lastEditorIdForComparison = oldModelUpdatedBy;
resolvedEditorName =
`ID ${oldModelUpdatedBy}`; // Default to ID on fallback
let usernameFromOldModel = `ID ${oldModelUpdatedBy}`;
if (typeof W !== 'undefined' && W.model && W.model.users)
{
const userObjectW =
W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
{
usernameFromOldModel = userObjectW.userName;
resolvedEditorName =
userObjectW.userName; // Username found
}
else if (userObjectW)
{
usernameFromOldModel =
`ID ${oldModelUpdatedBy} (sin userName)`;
}
}
lastEditorInfoForLog =
`Editor (W.model Fallback): ${usernameFromOldModel}`;
}
else
{
lastEditorInfoForLog = "Editor (W.model Fallback): N/D";
resolvedEditorName = "N/D";
}
}
}
else
{
// Fallback completo a W.model si el SDK no está inicializado
const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined)
{
lastEditorIdForComparison = oldModelUpdatedBy;
resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Default to ID
let usernameFromOldModel = `ID ${oldModelUpdatedBy}`;
if (typeof W !== 'undefined' && W.model && W.model.users)
{
const userObjectW =
W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
{
usernameFromOldModel = userObjectW.userName;
resolvedEditorName =
userObjectW.userName; // Username found
}
else if (userObjectW)
{
usernameFromOldModel =
`ID ${oldModelUpdatedBy} (sin userName)`;
}
}
lastEditorInfoForLog =
`Editor (W.model): ${usernameFromOldModel}`;
}
else
{
lastEditorInfoForLog = "Editor (W.model): N/D";
resolvedEditorName = "N/D";
}
}
// Actualizar wasEditedByMe basado en ID numérico si lo tenemos
if (currentLoggedInUserId !== null &&
typeof lastEditorIdForComparison === 'number' &&
currentLoggedInUserId === lastEditorIdForComparison)
{
wasEditedByMe = true;
}
//console.log(`[DEBUG] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`);
const editedByMeText = wasEditedByMe ? ' (Editado por mí)' : '';
/* console.log(`[INFO EDITOR LUGAR] Nombre: "${originalName}" (ID: ${
currentVenueId}) | ${lastEditorInfoForLog}${editedByMeText}`);*/
// ---- FIN INFO DEL EDITOR ----
// 5. --- PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA ---
let nombreSugeridoParcial = [];
let sugerenciasLugar = {};
const originalWords = originalName.split(/\s+/);
const processingStepLabel = document.getElementById("processingStep");
if (index === 0)
{
sugerenciasPorPalabra = {};
}
originalWords.forEach((P, idx) => {
const endsWithComma = restoreCommas && P.endsWith(",");
const baseWord = endsWithComma ? P.slice(0, -1) : P;
const cleaned = baseWord.trim();
if (cleaned === "")
{
nombreSugeridoParcial.push(cleaned);
return;
}
let isExcluded = false;
let matchingExcludedWord = null;
if (checkExcludedWords)
{
matchingExcludedWord = excludedArray.find(
w_excluded => removeDiacritics(w_excluded.toLowerCase()) ===
removeDiacritics(cleaned.toLowerCase()));
isExcluded = !!matchingExcludedWord;
}
let tempReplaced = cleaned;
const isArticle = commonWords.includes(cleaned.toLowerCase());
if (isExcluded)
{
tempReplaced = matchingExcludedWord;
}
else
{
if (isArticle)
{
tempReplaced = cleaned.toLowerCase();
if (idx === 0 && tempReplaced.length > 0)
{
tempReplaced = tempReplaced.charAt(0).toUpperCase() +
tempReplaced.slice(1);
}
}
else
{
tempReplaced = normalizePlaceName(cleaned);
}
}
if (!isExcluded && checkDictionaryWords && window.dictionaryWords &&
typeof window.dictionaryWords.forEach === "function")
{
window.dictionaryWords.forEach(diccWord => {
const cleanTemp =
removeDiacritics(tempReplaced.toLowerCase());
const cleanDicc = removeDiacritics(diccWord.toLowerCase());
if (cleanTemp.charAt(0) !== cleanDicc.charAt(0))
return;
const sim = calculateSimilarity(cleanTemp, cleanDicc);
if (sim >= similarityThreshold)
{ // Usar el umbral del slider
if (!sugerenciasLugar[baseWord])
sugerenciasLugar[baseWord] = [];
if (!sugerenciasLugar[baseWord].some(
s => s.word.toLowerCase() ===
diccWord.toLowerCase()))
{
sugerenciasLugar[baseWord].push({
word : diccWord,
similarity : sim,
fuente : 'dictionary'
});
}
}
});
}
if (checkExcludedWords)
{
const similarExcluded =
findSimilarWords(cleaned, excludedArray, similarityThreshold)
.filter(s => s.similarity < 1);
if (similarExcluded.length > 0)
{
sugerenciasLugar[baseWord] =
(sugerenciasLugar[baseWord] || [])
.concat(similarExcluded.map(
s => ({...s, fuente : 'excluded' })));
}
}
if (endsWithComma && !tempReplaced.endsWith(","))
{
tempReplaced += ",";
}
nombreSugeridoParcial.push(tempReplaced);
});
// ---- FIN PROCESAMIENTO PALABRA POR PALABRA ----
// 6. --- COMPILACIÓN DE suggestedName ---
const joinedSuggested = nombreSugeridoParcial.join(' ');
// const cleanedSuggestedForLog = // Esta variable parece no usarse para la lógica, solo para logs.
// joinedSuggested.replace(/\s{2,}/g, ' ').trim();
let processedName = joinedSuggested;
if (applyGeneralReplacements)
{
processedName = aplicarReemplazosGenerales(processedName);
}
processedName = aplicarReglasEspecialesNombre(processedName);
// ---- APLICAR REEMPLAZOS DEFINIDOS POR EL USUARIO ----
if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0) {
// console.log(`[REEMPLAZOS] Antes: "${processedName}"`, JSON.stringify(replacementWords)); // Para depuración
processedName = aplicarReemplazosDefinidos(processedName, replacementWords); // La llamada
// console.log(`[REEMPLAZOS] Después: "${processedName}"`); // Para depuración
}
// ---- FIN NUEVO PASO ----
const suggestedName = processedName.replace(/\s{2,}/g, ' ').trim();
// ---- FIN COMPILACIÓN DE suggestedName ----
// 7. --- LÓGICA DE SALTO (SKIP) CONSOLIDADA ---
const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
let shouldSkipThisPlace = false;
let skipReasonLog = "";
if (originalName.trim() === suggestedName.trim())
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP EXACT MATCH] Descartado "${
originalName}" porque es idéntico al nombre sugerido "${
suggestedName}".`;
}
else
{
const normalizedCleanCheck = suggestedName.toLowerCase();
const originalCleanCheck =
originalName.replace(/\s+/g, ' ').trim().toLowerCase();
const isAlreadyNormalizedCheck =
normalizedCleanCheck === originalCleanCheck;
if (isAlreadyNormalizedCheck && !tieneSugerencias)
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP] Se descartó "${
originalName}" porque ya se considera normalizado (${
originalCleanCheck} vs ${
normalizedCleanCheck}) y sin sugerencias.`;
}
else if (hideMyEdits && wasEditedByMe)
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP MY EDIT] Descartado "${
originalName}" (ID: ${
currentVenueId}) porque fue editado por mí y la opción está activa.`;
}
}
// ---- FIN LÓGICA DE SALTO ---
// 8. Registrar o no en la lista de inconsistentes
if (shouldSkipThisPlace)
{
if (skipReasonLog)
console.log(skipReasonLog);
}
else
{
if (processingStepLabel)
{
processingStepLabel.textContent =
"Registrando lugar con inconsistencias...";
}
let categoryNameToStore = "Sin categoría (Error)";
if (venueFromOldModel)
{
let venueSDKObjectForCategory =
null; // Necesitaríamos obtener el venueSDKObject de nuevo
// aquí si no lo guardamos o pasarlo si lo guardamos
// antes. Para esta iteración, si el SDK dio el venue
// antes, podríamos reusarlo, pero por simplicidad,
// getPlaceCategoryName puede intentar obtenerlo si le
// pasamos el ID, o solo usar W.model por ahora.
// simplificar y solo pasar venueFromOldModel,
// getPlaceCategoryName usará W.model.
// use el SDK para categoría, getPlaceCategoryName debe
// modificarse para aceptar ID y hacer la llamada async
// al SDK.
try
{
// Pasamos el venue del W.model; getPlaceCategoryName
// intentará usar SDK si wmeSDK está disponible y se le pasa
// el venueSDK Para esto, necesitaríamos haber guardado el
// venueSDK que obtuvimos arriba. Por ahora,
// getPlaceCategoryName ya intenta usar un
// venueSDKObject si se le pasa. Lo obtendremos de nuevo
// aquí para la categoría para mantenerlo encapsulado en
// esta prueba.
let venueSDKForCategory = null;
if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues &&
wmeSDK.DataModel.Venues.getById)
{
try
{
venueSDKForCategory =
await wmeSDK.DataModel.Venues.getById(
{ venueId : currentVenueId });
}
catch (catSDKError)
{
console.error("[SDK CATEGORY] Error obteniendo venueSDK para categoría:", catSDKError);
}
}
categoryNameToStore = getPlaceCategoryName(
venueFromOldModel, venueSDKForCategory);
}
catch (e)
{
console.error("Error llamando a getPlaceCategoryName desde processNextPlace:", e);
}
}
inconsistents.push({
id : currentVenueId,
original : originalName,
normalized : suggestedName,
category : categoryNameToStore,
editor : resolvedEditorName // Add the resolved editor name here
});
sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;
}
// 9. Finalizar procesamiento del 'place' actual y pasar al siguiente
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
} // ---- FIN DE LA FUNCIÓN processNextPlace ----
/* console.log("[DEBUG] ¿excludedArray es array?:",
Array.isArray(excludedArray));
console.log("[DEBUG] ¿processNextPlace está definido?:",
typeof processNextPlace === "function");
console.log("[DEBUG] Justo antes de llamar a processNextPlace");*/
// Inicializar procesamiento incremental
try
{
// console.log("[DEBUG] Justo antes de llamar a processNextPlace");
setTimeout(() => { processNextPlace(); }, 10);
}
catch (error)
{
console.error("[ERROR] Fallo en processNextPlace:", error);
}
// Fallback automático si finalizeRender no se ejecuta
scanFallbackTimeoutId = setTimeout(() => {
if (!document.querySelector("#wme-place-inspector-output")?.innerHTML)
{
/* console.warn(
"[WME PLN] Fallback activado: finalizeRender no se llamó en 30s. Mostrando panel flotante vacío.");
console.error(
"[WME PLN] Error: finalizeRender no se completó. Se invocó fallback tras 30 segundos de espera.");*/
createFloatingPanel(0); // Mostrar panel aunque esté vacío
const output =
document.querySelector("#wme-place-inspector-output");
if (output)
{
output.innerHTML =
"El proceso tomó demasiado tiempo o se interrumpió. No se completó el análisis.";
}
}
}, 30000);
// 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;
}
if (output)
{
output.innerHTML =
""; // Limpiar el mensaje de procesamiento y spinner
// Mostrar el panel flotante al terminar el procesamiento
createFloatingPanel(inconsistents.length);
}
// Limitar a 30 resultados y mostrar advertencia si excede
const maxRenderLimit = 30;
const totalInconsistents = inconsistents.length;
if (totalInconsistents > maxRenderLimit)
{
inconsistents = inconsistents.slice(0, maxRenderLimit);
if (!sessionStorage.getItem("popupShown"))
{
const 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 = "20px";
modal.style.zIndex = "10000";
modal.style.width = "400px";
modal.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)";
modal.style.borderRadius = "8px";
modal.style.fontFamily = "sans-serif";
// Fondo suave azul y mejor presentación
modal.style.backgroundColor = "#f0f8ff";
modal.style.border = "1px solid #aad";
modal.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)";
// --- Insertar ícono visual de información arriba del mensaje
// ---
const icon = document.createElement("div");
icon.innerHTML = "ℹ️";
icon.style.fontSize = "24px";
icon.style.marginBottom = "10px";
modal.appendChild(icon);
const message = document.createElement("p");
message.innerHTML = `Se encontraron <strong>${
totalInconsistents}</strong> lugares con nombres no normalizados.<br><br>Solo se mostrarán los primeros <strong>${
maxRenderLimit}</strong>.<br><br>Una vez corrijas estos, presiona nuevamente <strong>'Start Scan...'</strong> para continuar con el análisis del resto.`;
message.style.marginBottom = "20px";
modal.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");
modal.remove();
});
modal.appendChild(acceptBtn);
document.body.appendChild(modal);
}
}
// Si no hay inconsistencias, mostrar mensaje y salir (progreso visible)
if (inconsistents.length === 0)
{
output.appendChild(document.createTextNode(
"Todos los nombres de lugares visibles están correctamente normalizados."));
// Mensaje visual de análisis finalizado sin inconsistencias
const checkIcon = document.createElement("div");
checkIcon.innerHTML = "✔ Análisis finalizado sin inconsistencias.";
checkIcon.style.marginTop = "10px";
checkIcon.style.fontSize = "14px";
checkIcon.style.color = "green";
output.appendChild(checkIcon);
// Mensaje visual adicional solicitado
const successMsg = document.createElement("div");
successMsg.textContent =
"Todos los nombres están correctamente normalizados.";
successMsg.style.marginTop = "10px";
successMsg.style.fontSize = "14px";
successMsg.style.color = "green";
successMsg.style.fontWeight = "bold";
output.appendChild(successMsg);
const existingOverlay =
document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
existingOverlay.remove();
// Actualizar barra de progreso 100%
const progressBarInnerTab =
document.getElementById("progressBarInnerTab");
const progressBarTextTab =
document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent =
`Progreso: 100% (${placesArr.length}/${placesArr.length})`;
}
// Mensaje adicional en el tab principal (pestaña)
const outputTab =
document.getElementById("wme-normalization-tab-output");
if (outputTab)
{
outputTab.innerHTML =
`✔ Todos los nombres están normalizados. Se analizaron ${
placesArr.length} lugares.`;
outputTab.style.color = "green";
outputTab.style.fontWeight = "bold";
}
// Restaurar el texto y estado del botón de escaneo
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
scanBtn.textContent = "Start Scan...";
scanBtn.disabled = false;
scanBtn.style.opacity = "1";
scanBtn.style.cursor = "pointer";
// Agregar check verde al lado del botón al finalizar sin
// errores
const iconCheck = document.createElement("span");
iconCheck.textContent = " ✔";
iconCheck.style.marginLeft = "8px";
iconCheck.style.color = "green";
scanBtn.appendChild(iconCheck);
}
return;
}
// Mostrar spinner solo si hay inconsistencias a procesar
// showLoadingSpinner();
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.fontSize = "12px";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
[
"Permalink",
"Tipo",
"Categoría",
"Editor",
"Nombre Actual",
"Nombre Sugerido",
"Sugerencias de reemplazo",
"Acción"
].forEach(header => {
const th = document.createElement("th");
th.textContent = header;
th.style.borderBottom = "1px solid #ccc";
th.style.padding = "4px";
th.style.textAlign = "left";
headerRow.appendChild(th);
});
thead.style.position = "sticky";
thead.style.top = "0";
thead.style.background = "#f1f1f1";
thead.style.zIndex = "10";
headerRow.style.backgroundColor = "#003366";
headerRow.style.color = "#ffffff";
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
inconsistents.forEach(({ id, original, normalized, category, editor },
index) => {
// Actualizar barra de progreso visual EN EL TAB PRINCIPAL
const progressPercent =
Math.floor(((index + 1) / inconsistents.length) * 100);
// Actualiza barra de progreso en el tab principal
const progressBarInnerTab =
document.getElementById("progressBarInnerTab");
const progressBarTextTab =
document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = `${progressPercent}%`;
progressBarTextTab.textContent = `Progreso: ${
progressPercent}% (${index + 1}/${inconsistents.length})`;
}
const row = document.createElement("tr");
const permalinkCell = document.createElement("td");
const link = document.createElement("a");
link.href = "#";
// Reemplazado onclick por addEventListener para mejor
// compatibilidad y centrado de mapa
link.addEventListener("click", (e) => {
e.preventDefault();
const venue = W.model.venues.getObjectById(id);
if (!venue)
return;
// Centrar mapa y seleccionar el lugar
const geometry = venue.getGeometry();
if (geometry && geometry.getCentroid)
{
const center = geometry.getCentroid();
W.map.setCenter(center, null, false, 0);
}
if (W.selectionManager &&
typeof W.selectionManager.select === "function")
{
W.selectionManager.select(venue);
}
else if (W.selectionManager &&
typeof W.selectionManager.setSelectedModels ===
"function")
{
W.selectionManager.setSelectedModels([ venue ]);
}
});
link.title = "Abrir en panel lateral";
link.textContent = "🔗";
permalinkCell.appendChild(link);
permalinkCell.style.padding = "4px";
permalinkCell.style.width = "65px";
row.appendChild(permalinkCell);
// Columna Tipo de place
const venue = W.model.venues.getObjectById(id);
const { icon : typeIcon, title : typeTitle } =
getPlaceTypeInfo(venue);
const typeCell = document.createElement("td");
typeCell.textContent = typeIcon;
typeCell.title = `Lugar tipo ${typeTitle}`;
typeCell.style.padding = "4px";
typeCell.style.width = "65px";
row.appendChild(typeCell);
// Columna Categoría del place
const categoryCell = document.createElement("td");
const categoryName = getPlaceCategoryName(venue);
categoryCell.textContent = categoryName;
categoryCell.title = `Categoría: ${categoryName}`;
categoryCell.style.padding = "4px";
categoryCell.style.width = "130px";
row.appendChild(categoryCell);
// Columna Editor (username)
const editorCell = document.createElement("td");
editorCell.textContent =
editor || "Desconocido"; // Use the stored editor name
editorCell.title = "Último editor";
editorCell.style.padding = "4px";
editorCell.style.width = "140px";
row.appendChild(editorCell);
const originalCell = document.createElement("td");
const inputOriginal = document.createElement("input");
inputOriginal.type = "text";
const venueLive = W.model.venues.getObjectById(id);
const currentLiveName = venueLive?.attributes?.name?.value ||
venueLive?.attributes?.name || "";
inputOriginal.value = currentLiveName;
// --- Resaltar en rojo si hay diferencia con el sugerido ---
if (currentLiveName.trim().toLowerCase() !==
normalized.trim().toLowerCase())
{
inputOriginal.style.border = "1px solid red";
inputOriginal.title =
"Este nombre difiere del original mostrado en el panel";
}
inputOriginal.disabled = true;
inputOriginal.style.width = "270px";
inputOriginal.style.backgroundColor = "#eee";
originalCell.appendChild(inputOriginal);
originalCell.style.padding = "4px";
originalCell.style.width = "270px";
row.appendChild(originalCell);
const suggestionCell = document.createElement("td");
// Nueva columna: sugerencia de reemplazo seleccionada
const suggestionListCell = document.createElement("td");
suggestionListCell.style.padding = "4px";
suggestionListCell.style.fontSize = "11px";
suggestionListCell.style.color = "#333";
// Permitir múltiples líneas en la celda de sugerencias
suggestionListCell.style.whiteSpace = "pre-wrap";
suggestionListCell.style.wordBreak = "break-word";
suggestionListCell.style.width = "270px";
// Calcular sugerencias similares de especiales aquí
// --- Nueva lógica para separar sugerencias por fuente ---
const allSuggestions = sugerenciasPorPalabra?.[id] || {};
const similarList = {};
const similarDictList = {};
Object.entries(allSuggestions)
.forEach(([ originalWord, suggestions ]) => {
suggestions.forEach(s => {
if (s.fuente === 'excluded')
{
if (!similarList[originalWord])
similarList[originalWord] = [];
similarList[originalWord].push(s);
}
else if (s.fuente === 'dictionary')
{
if (!similarDictList[originalWord])
similarDictList[originalWord] = [];
similarDictList[originalWord].push(s);
}
});
});
// --- NUEVA LÓGICA: aplicar reemplazos automáticos y solo mostrar
// sugerencias < 1 ---
let autoApplied = false;
let localNormalized = normalized;
// 1. Procesar sugerencias de especiales (similarList)
let hasExcludedSuggestionsToShow = false;
if (similarList && Object.keys(similarList).length > 0)
{
Object.entries(similarList)
.forEach(([ originalWord, suggestions ]) => {
suggestions.forEach(s => {
if (s.similarity === 1)
{
// Si encontramos una sugerencia 100%, ya fue
// aplicada en el pipeline principal.
autoApplied = true;
// NO mostrar sugerencia clickable ni texto en
// columna de sugerencias.
}
else if (s.similarity < 1)
{
hasExcludedSuggestionsToShow = true;
}
});
});
}
// 2. Procesar sugerencias de diccionario (similarDictList)
let hasDictSuggestionsToShow = false;
if (similarDictList && Object.keys(similarDictList).length > 0)
{
Object.entries(similarDictList)
.forEach(([ originalWord, suggestions ]) => {
suggestions.forEach(s => {
if (s.similarity < 1)
{
hasDictSuggestionsToShow = true;
}
});
});
}
// --- EVITAR DUPLICADOS DE SUGERENCIAS ENTRE "ESPECIALES" Y
// "DICCIONARIO" --- Crear set de palabras ya procesadas en
// sugerencias especiales
const palabrasYaProcesadas = new Set();
if (similarList && Object.keys(similarList).length > 0)
{
Object.keys(similarList)
.forEach(palabra =>
palabrasYaProcesadas.add(palabra.toLowerCase()));
}
// Render input de sugerencia
const inputReplacement = document.createElement("input");
inputReplacement.type = "text";
let mainSuggestion = localNormalized;
inputReplacement.value = mainSuggestion;
inputReplacement.style.width = "270px";
// Visual cue if change was due to excluded word
if (localNormalized !== normalized)
{
inputReplacement.style.backgroundColor =
"#fff3cd"; // color amarillo claro
inputReplacement.title = "Contiene palabra excluida reemplazada";
}
else if (autoApplied)
{
inputReplacement.style.backgroundColor =
"#c8e6c9"; // verde claro
inputReplacement.title =
"Reemplazo automático aplicado (100% similitud)";
}
else if (normalized !== inputReplacement.value)
{
inputReplacement.style.backgroundColor =
"#e6f7ff"; // Azul claro para cambios automáticos del
// diccionario (≥ 90%)
inputReplacement.title =
"Cambio automático basado en diccionario (≥ 90%)";
}
else
{
inputReplacement.title = "Nombre normalizado";
}
// NUEVA LÓGICA: marcar si contiene palabra sugerida por el
// diccionario
const palabrasDelDiccionario = new Set();
Object.values(similarDictList).forEach(arr => {
arr.forEach(s => {
if (mainSuggestion.toLowerCase().includes(
s.word.toLowerCase()))
{
palabrasDelDiccionario.add(s.word.toLowerCase());
}
});
});
if (palabrasDelDiccionario.size > 0)
{
inputReplacement.style.backgroundColor = "#cce5ff"; // Azul claro
inputReplacement.title =
"Contiene sugerencias del diccionario aplicadas manualmente";
}
suggestionCell.appendChild(inputReplacement);
suggestionCell.style.padding = "4px";
suggestionCell.style.width = "270px";
// --- Función debounce ---
function debounce(func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
// --- Activar/desactivar el botón Aplicar según si hay cambios ---
inputReplacement.addEventListener('input', debounce(() => {
if (inputReplacement.value.trim() !== original)
{
applyButton.disabled = false;
applyButton.style.color = "";
}
else
{
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
}, 300));
// --- Listener para inputOriginal con debounce (puede personalizarse la lógica) ---
inputOriginal.addEventListener('input', debounce(() => {
// Opcional: alguna lógica si se desea manejar cambios en inputOriginal
}, 300));
// Renderizar solo sugerencias < 1 en sugerencias de reemplazo
// especiales primero
if (similarList && Object.keys(similarList).length > 0) {
Object.entries(similarList).forEach(([originalWord, suggestions]) => {
suggestions.forEach(s => { // 's' aquí es { word: candidate, similarity: sim, fuente: 'excluded' }
// Mostrar todas las sugerencias < 100% de similitud,
// o incluso las de 100% si la forma en la lista de excluidas es diferente (ej. mayúsculas/minúsculas)
// a la originalWord.
// La condición original s.similarity < 1 es buena para evitar sugerir lo mismo que ya está.
if (s.similarity < 1 || (s.similarity === 1 && originalWord !== s.word) ) {
const suggestionDiv = document.createElement("div");
const icono = "🏷️"; // Icono para palabras especiales/excluidas
suggestionDiv.textContent =
`${icono} ¿"${originalWord}" por "${s.word}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`;
suggestionDiv.style.cursor = "pointer";
suggestionDiv.style.padding = "2px 4px";
suggestionDiv.style.margin = "2px 0";
suggestionDiv.style.border = "1px solid #ddd";
suggestionDiv.style.backgroundColor = "#f3f9ff"; // Color distintivo si quieres
suggestionDiv.addEventListener("click", () => {
const currentSuggestedValue = inputReplacement.value; // Valor actual del campo "Nombre Sugerido"
// console.log("--- DEBUG: Clic en Sugerencia ESPECIAL ---");
// console.log("Palabra Original (base de la sugerencia):", originalWord);
// console.log("Sugerencia (palabra a usar de lista Especiales):", s.word);
// 1. Normalizamos la 'originalWord' para saber qué forma buscar
const normalizedOriginalWord = normalizePlaceName(originalWord);
//console.log("Forma Normalizada de Palabra Original (para buscar):", normalizedOriginalWord);
// 2. Normalizamos la palabra sugerida 's.word' de la lista de Especiales
const wordToInsert = normalizePlaceName(s.word);
//console.log("Palabra Normalizada para Insertar:", wordToInsert);
//console.log("Valor ACTUAL del campo 'Nombre Sugerido':", currentSuggestedValue);
// 3. Creamos la regex para buscar la 'normalizedOriginalWord'
const searchRegex = new RegExp("\\b" + escapeRegExp(normalizedOriginalWord) + "\\b", "gi");
//console.log("Regex para buscar en 'Nombre Sugerido':", searchRegex.toString());
if (!searchRegex.test(currentSuggestedValue)) {
console.warn("¡ADVERTENCIA ESPECIAL! La forma normalizada de la palabra original ('" + normalizedOriginalWord + "') no se encontró en el 'Nombre Sugerido' actual ('" + currentSuggestedValue + "'). No se hará reemplazo.");
}
const newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert);
//console.log("Valor DESPUÉS de .replace() (Especial):", newSuggestedValue);
if (currentSuggestedValue !== newSuggestedValue)
{
inputReplacement.value = newSuggestedValue;
// console.log("Campo 'Nombre Sugerido' ACTUALIZADO (Especial) a:", newSuggestedValue);
}
else
{
console.log("No hubo cambios en 'Nombre Sugerido' (Especial) (el nuevo valor es idéntico al anterior o la palabra a reemplazar no se encontró/ya era igual).");
}
inputReplacement.dispatchEvent(new Event("input"));
// console.log("--- FIN DEBUG (Especial) ---");
});
suggestionListCell.appendChild(suggestionDiv);
}
});
});
}
// Diccionario después, evitando duplicados de palabras ya sugeridas
// en especiales
if (similarDictList && Object.keys(similarDictList).length > 0)
{
Object.entries(similarDictList)
.forEach(([ originalWord, suggestions ]) => {
if (palabrasYaProcesadas.has(originalWord.toLowerCase()))
return;
suggestions.forEach(s => {
if (s.similarity < 1 ||
(s.similarity === 1 && originalWord !== s.word))
{
const suggestionItem =
document.createElement("div");
const icono = s.fuente === 'dictionary' ? "📘"
: s.fuente === 'excluded' ? "🏷️"
: "❓";
suggestionItem.textContent = `${icono} ¿"${
originalWord}" por "${s.word}"? (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; // Valor actual del campo "Nombre Sugerido"
// originalWord: Es la palabra del nombre original del lugar que generó esta sugerencia (ej. "ROJA")
// s.word: Es la palabra del diccionario/lista de especiales (ej. "rojo")
// console.log("--- DEBUG: Clic en Sugerencia (Nueva Lógica) ---");
// console.log("Palabra Original (base de la sugerencia):", originalWord);
// console.log("Sugerencia (palabra a usar desde lista):", s.word);
// 1. Normalizamos la 'originalWord' para saber qué forma buscar en el 'Nombre Sugerido' actual.
// Esto asume que el 'Nombre Sugerido' ya contiene formas normalizadas.
const normalizedOriginalWord = normalizePlaceName(originalWord);
//console.log("Forma Normalizada de Palabra Original (para buscar):", normalizedOriginalWord);
// 2. Normalizamos la palabra sugerida 's.word' para la inserción.
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' en el 'currentSuggestedValue'.
// Usamos 'gi' para que sea global (reemplace todas las ocurrencias) e insensible a mayúsculas.
// Esto es importante porque 'normalizePlaceName' podría haber cambiado la capitalización.
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! La forma normalizada de la palabra original ('" + normalizedOriginalWord + "') no se encontró en el 'Nombre Sugerido' actual ('" + currentSuggestedValue + "'). No se hará reemplazo.");
// Consideración: Si 'originalWord' era, por ejemplo, una abreviatura que ya fue expandida
// por la pestaña de "Reemplazos Automáticos", entonces 'normalizedOriginalWord'
// (que sería la normalización de la abreviatura) tampoco se encontraría.
// En este caso, no hacer nada es probablemente correcto.
}
const newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert);
//console.log("Valor DESPUÉS de .replace():", newSuggestedValue);
if (currentSuggestedValue !== newSuggestedValue)
{
inputReplacement.value = newSuggestedValue;
// console.log("Campo 'Nombre Sugerido' ACTUALIZADO a:", newSuggestedValue);
}
else
{
console.log("No hubo cambios en 'Nombre Sugerido' (el nuevo valor es idéntico al anterior o la palabra a reemplazar no se encontró/ya era igual).");
}
inputReplacement.dispatchEvent(new Event("input")); // Para actualizar el estado del botón Aplicar (✔)
//console.log("--- FIN DEBUG ---");
});
suggestionListCell.appendChild(suggestionItem);
}
});
});
}
row.appendChild(suggestionCell);
row.appendChild(suggestionListCell);
const actionCell = document.createElement("td");
actionCell.style.padding = "4px";
actionCell.style.width = "120px";
const buttonGroup = document.createElement("div");
buttonGroup.style.display = "flex";
buttonGroup.style.gap = "4px";
const applyButton = document.createElement("button");
applyButton.textContent = "✔";
applyButton.title = "Aplicar sugerencia";
applyButton.style.padding = "4px 8px";
applyButton.style.cursor = "pointer";
const deleteButton = document.createElement("button");
deleteButton.textContent = "💣";
deleteButton.title = "Eliminar lugar";
deleteButton.style.padding = "4px 8px";
deleteButton.style.cursor = "pointer";
applyButton.relatedDelete = deleteButton;
deleteButton.relatedApply = applyButton;
applyButton.addEventListener("click", () => {
const venue = W.model.venues.getObjectById(id);
if (!venue)
{
alert(
"Error: El lugar no está disponible o ya fue eliminado.");
return;
}
const newName = inputReplacement.value.trim();
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(venue, { name : newName });
W.model.actionManager.add(action);
applyButton.disabled = true;
applyButton.style.color = "#bbb";
applyButton.style.opacity = "0.5";
if (applyButton.relatedDelete)
{
applyButton.relatedDelete.disabled = true;
applyButton.relatedDelete.style.color = "#bbb";
applyButton.relatedDelete.style.opacity = "0.5";
}
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.marginLeft = "5px";
applyButton.parentElement.appendChild(successIcon);
}
catch (e)
{
alert("Error al actualizar: " + e.message);
}
});
deleteButton.addEventListener("click", () => {
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 #ccc";
confirmModal.style.padding = "20px";
confirmModal.style.zIndex = "10001";
confirmModal.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "6px";
const message = document.createElement("p");
const venue = W.model.venues.getObjectById(id);
const placeName =
venue?.attributes?.name?.value || "este lugar";
message.textContent =
`¿Estás seguro que deseas eliminar "${placeName}" del mapa?`;
confirmModal.appendChild(message);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "flex-end";
buttonWrapper.style.gap = "10px";
buttonWrapper.style.marginTop = "10px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "4px 8px";
cancelBtn.addEventListener("click",
() => { confirmModal.remove(); });
confirmBtn.addEventListener("click", () => {
const checked = modal.querySelectorAll("input[type=checkbox]:checked");
let anyAdded = false;
checked.forEach(c => {
const validation = isValidExcludedWord(c.value);
if (!validation.valid)
{
alert(`"${c.value}": ${validation.msg}`);
return;
}
excludedWords.add(c.value);
anyAdded = true;
});
if (anyAdded)
renderExcludedWordsList();
// Actualizar estado de botón aplicar según diferencia
if (suggestionInput.value !== original)
{
applyButton.disabled = false;
applyButton.style.color = "";
}
else
{
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
modal.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 = "10000";
modal.style.maxWidth = "300px";
const title = document.createElement("h4");
title.textContent = "Agregar palabra a especiales";
modal.appendChild(title);
const list = document.createElement("ul");
list.style.listStyle = "none";
list.style.padding = "0";
words.forEach(w => {
// --- Filtro: palabras vacías, comunes, o ya existentes
// (ignorar mayúsculas) ---
if (w.trim() === '')
return;
const lowerW = w.trim().toLowerCase();
/* const commonWords = [
'de', 'del', 'el', 'la', 'los', 'las',
'y', 'e', 'o', 'u', 'un', 'una',
'y', 'unos', 'unas', 'a', 'en', 'con',
'sin', 'sobre', 'tras', 'por'
];*/
const alreadyExists =
Array.from(excludedWords)
.some(existing => existing.toLowerCase() === lowerW);
if (commonWords.includes(lowerW) || alreadyExists)
return;
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = w;
checkbox.id = `cb-exc-${w.replace(/[^a-zA-Z0-9]/g, "")}`;
li.appendChild(checkbox);
const label = document.createElement("label");
label.htmlFor = checkbox.id;
label.appendChild(document.createTextNode(" " + w));
li.appendChild(label);
list.appendChild(li);
});
modal.appendChild(list);
const confirmBtn = document.createElement("button");
confirmBtn.textContent = "Añadir Seleccionadas";
confirmBtn.addEventListener("click", () => {
const checked = modal.querySelectorAll("input[type=checkbox]:checked");
let wordsActuallyAdded = false; // Para saber si se añadió algo nuevo
checked.forEach(c => {
// Antes de añadir, podrías verificar si ya existe para evitar
// trabajo innecesario, aunque un Set maneja duplicados.
// También, considera si quieres aplicar isValidExcludedWord aquí,
// aunque usualmente las palabras del nombre del lugar son candidatas directas.
if (!excludedWords.has(c.value)) {
excludedWords.add(c.value);
wordsActuallyAdded = true;
}
});
if (wordsActuallyAdded) {
// Llama a renderExcludedWordsList para actualizar la UI en la pestaña "Especiales"
// y para guardar en localStorage
if (typeof renderExcludedWordsList === 'function') {
// Es mejor pasar el elemento si se puede, o dejar que la función lo encuentre
const excludedListElement = document.getElementById("excludedWordsList");
if (excludedListElement) {
renderExcludedWordsList(excludedListElement);
} else {
renderExcludedWordsList(); // Fallback
}
}
}
modal.remove();
});
modal.appendChild(confirmBtn);
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.marginLeft = "8px";
cancelBtn.addEventListener("click", () => modal.remove());
modal.appendChild(cancelBtn);
document.body.appendChild(modal);
});
buttonGroup.appendChild(addToExclusionBtn);
// buttonGroup.appendChild(addToDictionaryBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
// Añadir borde inferior visible entre cada lugar
row.style.borderBottom = "1px solid #ddd";
row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff";
tbody.appendChild(row);
// Actualizar progreso al final del ciclo usando setTimeout para
// liberar el hilo visual
setTimeout(() => {
const progress =
Math.floor(((index + 1) / inconsistents.length) * 100);
const progressElem =
document.getElementById("scanProgressText");
if (progressElem)
{
progressElem.textContent = `Analizando lugares: ${
progress}% (${index + 1}/${inconsistents.length})`;
}
}, 0);
});
table.appendChild(tbody);
output.appendChild(table);
// Log de cierre
// console.log("✔ Panel finalizado y tabla renderizada.");
// Quitar overlay spinner justo antes de mostrar la tabla
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
{
existingOverlay.remove();
}
// Al finalizar, actualizar el texto final en el tab principal (progreso
// 100%)
const progressBarInnerTab =
document.getElementById("progressBarInnerTab");
const progressBarTextTab =
document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent =
`Progreso: 100% (${inconsistents.length}/${placesArr.length})`;
}
// Función para reactivar todos los botones de acción en el panel
// flotante
function reactivateAllActionButtons()
{
document.querySelectorAll("#wme-place-inspector-output button")
.forEach(btn => {
btn.disabled = false;
btn.style.color = "";
btn.style.opacity = "";
});
}
W.model.actionManager.events.register("afterundoaction", null, () => {
waitForWazeAPI(() => {
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons,
250); // Esperar a que el panel se reconstruya
});
});
W.model.actionManager.events.register("afterredoaction", null, () => {
waitForWazeAPI(() => {
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons, 250);
});
});
// Mostrar el panel flotante al terminar el procesamiento
// createFloatingPanel(inconsistents.length); // Ahora se invoca arriba
// si output existe
}
}
function getLevenshteinDistance(a, b)
{
const matrix = Array.from(
{ length : b.length + 1 },
(_, i) => Array.from({ length : a.length + 1 },
(_, j) => (i === 0 ? j : (j === 0 ? i : 0))));
for (let i = 1; i <= b.length; i++)
{
for (let j = 1; j <= a.length; j++)
{
if (b.charAt(i - 1) === a.charAt(j - 1))
{
matrix[i][j] = matrix[i - 1][j - 1];
}
else
{
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + 1 // substitution
);
}
}
}
return matrix[b.length][a.length];
}
function calculateSimilarity(word1, word2)
{
const distance =
getLevenshteinDistance(word1.toLowerCase(), word2.toLowerCase());
const maxLen = Math.max(word1.length, word2.length);
return 1 - distance / maxLen;
}
function findSimilarWords(word, excludedWords, threshold)
{
const userThreshold =
parseFloat(document.getElementById("similarityThreshold")?.value ||
"85") /
100;
const lowerWord = word.toLowerCase();
// excludedWords is now always an array
const firstChar = lowerWord.charAt(0);
let candidates = excludedWords;
if (typeof excludedWords === 'object' && !Array.isArray(excludedWords))
{
// Estamos usando el índice
candidates = excludedWords[firstChar] || [];
}
return candidates
.map(candidate => {
const similarity =
calculateSimilarity(lowerWord, candidate.toLowerCase());
return { word : candidate, similarity };
})
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity);
}
function suggestExcludedReplacements(currentName, excludedWords)
{
const words = currentName.split(/\s+/);
const suggestions = {};
const threshold =
parseFloat(document.getElementById("similarityThreshold")?.value ||
"85") /
100;
words.forEach(word => {
const similar =
findSimilarWords(word, Array.from(excludedWords), threshold);
if (similar.length > 0)
{
suggestions[word] = similar;
}
});
return suggestions;
}
// Reset del inspector: progreso y texto de tab
function resetInspectorState()
{
const inner = document.getElementById("progressBarInnerTab");
const text = document.getElementById("progressBarTextTab");
const outputTab = document.getElementById("wme-normalization-tab-output");
if (inner)
inner.style.width = "0%";
if (text)
text.textContent = `Progreso: 0% (0/0)`;
if (outputTab)
outputTab.textContent =
"Presiona 'Start Scan...' para analizar los lugares visibles.";
}
function createFloatingPanel(numInconsistents = 0)
{
const existing = document.querySelector("#wme-place-inspector-panel");
if (existing)
{
existing.style.display = 'block';
return;
}
const panel = document.createElement("div");
const closeBtn = document.createElement("span");
closeBtn.textContent = "×";
closeBtn.style.position = "absolute";
closeBtn.style.top = "5px";
closeBtn.style.right = "8px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontSize = "16px";
closeBtn.style.color = "#aaa";
closeBtn.title = "Cerrar panel";
closeBtn.addEventListener("click", () => {
panel.style.display = 'none'; // Ocultar el panel
// Restaurar estado del botón escanear
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 iconCheck = scanBtn.querySelector("span");
if (iconCheck)
{
iconCheck.remove(); // Remover icono de check si quedó
}
}
// Restaurar el mensaje en el tab
const outputTab =
document.getElementById("wme-normalization-tab-output");
if (outputTab)
{
outputTab.textContent =
"Presiona 'Start Scan...' para analizar los Lugares visibles.";
outputTab.style.color = "#000";
outputTab.style.fontWeight = "normal";
}
// Resetear barra de progreso/tab
resetInspectorState();
});
panel.appendChild(closeBtn);
panel.id = "wme-place-inspector-panel";
panel.style.position = "fixed";
panel.style.top = "55%";
panel.style.left = "70%";
panel.style.transform = "translate(-70%, -55%)";
panel.style.width = "1500px";
panel.style.height = "60vh";
panel.style.overflow =
"hidden"; // El scroll interno lo maneja #wme-place-inspector-output
panel.style.zIndex = "9999";
panel.style.background = "#fff";
panel.style.border = "1px solid #ccc";
panel.style.borderRadius = "6px";
panel.style.boxShadow = "0 2px 6px rgba(0,0,0,0.2)";
panel.style.padding = "10px";
panel.style.fontFamily = "sans-serif";
panel.style.display = 'block'; // Asegurar que esté visible al crear
const title = document.createElement("h4");
title.textContent =
numInconsistents > 0
? `Lugares con Nombres No Normalizados (${numInconsistents})`
: "Resultado del Análisis";
title.style.marginTop = "0";
title.style.marginBottom = "10px"; // Espacio antes de la tabla
panel.appendChild(title);
const output = document.createElement("div");
output.id = "wme-place-inspector-output"; // Este es el output para la tabla
// de lugares
output.style.height = "calc(100% - 40px)";
output.style.overflowY = "auto";
output.style.border = "1px solid #eee";
output.style.padding = "6px";
output.style.fontSize = "12px";
output.style.backgroundColor = "#f9f9f9";
panel.appendChild(output);
document.body.appendChild(panel);
if (!document.getElementById("wme-pln-spinner-style"))
{
const styleTag = document.createElement("style");
styleTag.id = "wme-pln-spinner-style";
styleTag.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(styleTag);
}
}
// Escuchar el botón Guardar de WME para resetear el inspector
const wmeSaveBtn = document.querySelector(
"button.action.save, button[title='Guardar'], button[aria-label='Guardar']");
if (wmeSaveBtn)
{
wmeSaveBtn.addEventListener("click", () => resetInspectorState());
}
function createSidebarTab()
{
try
{
// 1. Verificar si WME y la función para registrar pestañas están listos
if (!W || !W.userscripts ||
typeof W.userscripts.registerSidebarTab !== 'function')
{
console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral.");
return;
}
// 2. Registrar la pestaña principal del script en WME y obtener tabPane
let registration;
try
{
registration = W.userscripts.registerSidebarTab(
"NrmliZer"); // Nombre del Tab que aparece en WME
}
catch (e)
{
if (e.message.includes("already been registered"))
{
console.warn(
"[WME PLN] Tab 'NrmliZer' ya registrado. El script puede no funcionar como se espera si hay múltiples instancias.");
// Podrías intentar obtener el tabPane existente o simplemente
// retornar. Para evitar mayor complejidad, si ya está
// registrado, no continuaremos con la creación de la UI de la
// pestaña.
return;
}
//console.error("[WME PLN] Error registrando el sidebar tab:", e);
throw e; // Relanzar otros errores para que se vean en consola
}
const { tabLabel, tabPane } = registration;
if (!tabLabel || !tabPane)
{
//console.error("[WME PLN] Falló el registro del Tab: 'tabLabel' o 'tabPane' no fueron retornados.");
return;
}
// Configurar el ícono y nombre de la pestaña principal del script
tabLabel.innerHTML = `
<img src="data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q=="
style="height: 16px; vertical-align: middle; margin-right: 5px;">
NrmliZer
`;
// 3. Inicializar las pestañas internas (General, Especiales,
// Diccionario, Reemplazos)
const tabsContainer = document.createElement("div");
tabsContainer.style.display = "flex";
tabsContainer.style.marginBottom = "8px";
tabsContainer.style.gap = "8px";
const tabButtons = {};
const tabContents = {}; // Objeto para guardar los divs de contenido
// Crear botones para cada pestaña
tabNames.forEach(({ label, icon }) => {
const btn = document.createElement("button");
btn.innerHTML = icon
? `<span style="display: inline-flex; align-items: center; font-size: 11px;">
<span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label}
</span>`
: `<span style="font-size: 11px;">${label}</span>`;
btn.style.fontSize = "11px";
btn.style.padding = "4px 8px";
btn.style.marginRight = "4px";
btn.style.minHeight = "28px";
btn.style.border = "1px solid #ccc";
btn.style.borderRadius = "4px 4px 0 0";
btn.style.cursor = "pointer";
btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada
btn.className = "custom-tab-style";
// Agrega aquí el tooltip personalizado para cada tab
if (label === "Gene") btn.title = "Configuración general";
else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)";
else if (label === "Dicc") btn.title = "Diccionario de palabras válidas";
else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos";
// Estilo inicial: la primera pestaña es la activa
if (label === tabNames[0].label) {
btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco)
btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa
btn.style.fontWeight = "bold";
} else {
btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro)
btn.style.fontWeight = "normal";
}
btn.addEventListener("click", () => {
tabNames.forEach(({label : tabLabel_inner}) => {
const isActive = (tabLabel_inner === label);
const currentButton = tabButtons[tabLabel_inner];
if (tabContents[tabLabel_inner]) {
tabContents[tabLabel_inner].style.display = isActive ? "block" : "none";
}
if (currentButton) {
// Aplicar/Quitar estilos de pestaña activa directamente
if (isActive) {
currentButton.style.backgroundColor = "#ffffff"; // Activo
currentButton.style.borderBottom = "2px solid #007bff";
currentButton.style.fontWeight = "bold";
} else {
currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo
currentButton.style.borderBottom = "none";
currentButton.style.fontWeight = "normal";
}
}
// Llamar a la función de renderizado correspondiente
if (isActive) {
if (tabLabel_inner === "Espe")
{
const ul = document.getElementById("excludedWordsList");
if (ul && typeof renderExcludedWordsList === 'function') renderExcludedWordsList(ul);
}
else if (tabLabel_inner === "Dicc")
{
const ulDict = document.getElementById("dictionaryWordsList");
if (ulDict && typeof renderDictionaryList === 'function') renderDictionaryList(ulDict);
}
else if (tabLabel_inner === "Reemp")
{
const ulReemplazos = document.getElementById("replacementsListElementID");
if (ulReemplazos && typeof renderReplacementsList === 'function') renderReplacementsList(ulReemplazos);
}
}
});
});
tabButtons[label] = btn;
tabsContainer.appendChild(btn);
});
tabPane.appendChild(tabsContainer);
// Crear los divs contenedores para el contenido de cada pestaña
tabNames.forEach(({ label }) => {
const contentDiv = document.createElement("div");
contentDiv.style.display = label === tabNames[0].label ? "block" : "none"; // Mostrar solo la primera
contentDiv.style.padding = "10px";
tabContents[label] = contentDiv; // Guardar referencia
tabPane.appendChild(contentDiv);
});
// --- POBLAR EL CONTENIDO DE CADA PESTAÑA ---
// 4. Poblar el contenido de la pestaña "General"
const containerGeneral = tabContents["Gene"];
if (containerGeneral)
{
let initialUsernameAttempt =
"Pendiente"; // Para la etiqueta simplificada
// No es necesario el polling complejo si solo es para la lógica
// interna del checkbox
if (typeof W !== 'undefined' && W.loginManager &&
W.loginManager.user && W.loginManager.user.userName)
{
initialUsernameAttempt =
W.loginManager.user
.userName; // Se usará internamente en processNextPlace
}
const mainTitle = document.createElement("h3");
mainTitle.textContent = "NormliZer";
mainTitle.style.textAlign = "center";
mainTitle.style.fontSize = "18px";
mainTitle.style.marginBottom = "2px";
containerGeneral.appendChild(mainTitle);
const versionInfo = document.createElement("div");
versionInfo.textContent =
"V. " + VERSION; // VERSION global
versionInfo.style.textAlign = "right";
versionInfo.style.fontSize = "10px";
versionInfo.style.color = "#777";
versionInfo.style.marginBottom = "15px";
containerGeneral.appendChild(versionInfo);
const normSectionTitle = document.createElement("h4");
normSectionTitle.textContent = "Análisis de Nombres de Places";
normSectionTitle.style.fontSize = "15px";
normSectionTitle.style.marginTop = "10px";
normSectionTitle.style.marginBottom = "5px";
normSectionTitle.style.borderBottom = "1px solid #eee";
normSectionTitle.style.paddingBottom = "3px";
containerGeneral.appendChild(normSectionTitle);
const scanButton = document.createElement("button");
scanButton.textContent = "Start Scan...";
scanButton.setAttribute("type", "button");
scanButton.style.marginBottom = "10px";
scanButton.style.width = "100%";
scanButton.style.padding = "8px";
scanButton.style.border = "none";
scanButton.style.borderRadius = "4px";
scanButton.style.backgroundColor = "#007bff";
scanButton.style.color = "#fff";
scanButton.style.cursor = "pointer";
scanButton.addEventListener("click", () => {
const places = getVisiblePlaces();
const outputDiv =
document.getElementById("wme-normalization-tab-output");
if (!outputDiv)
{ // Mover esta verificación antes
// console.error("Div de salida (wme-normalization-tab-output) no encontrado en el tab.");
return;
}
if (places.length === 0)
{
outputDiv.textContent = "No se encontraron lugares visibles para analizar.";
return;
}
const maxPlacesInput = document.getElementById("maxPlacesInput");
const maxPlacesToScan = parseInt(maxPlacesInput?.value || "100", 10);
const scannedCount = Math.min(places.length, maxPlacesToScan);
outputDiv.textContent = `Escaneando ${scannedCount} lugares...`;
setTimeout(() => {renderPlacesInFloatingPanel(places.slice(0, maxPlacesToScan));
}, 10);
});
containerGeneral.appendChild(scanButton);
const maxWrapper = document.createElement("div");
maxWrapper.style.display = "flex";
maxWrapper.style.alignItems = "center";
maxWrapper.style.gap = "8px";
maxWrapper.style.marginBottom = "8px";
const maxLabel = document.createElement("label");
maxLabel.textContent = "Máximo de places a revisar:";
maxLabel.style.fontSize = "13px";
maxWrapper.appendChild(maxLabel);
const maxInput = document.createElement("input");
maxInput.type = "number";
maxInput.id = "maxPlacesInput";
maxInput.min = "1";
maxInput.value = "100";
maxInput.style.width = "80px";
maxWrapper.appendChild(maxInput);
containerGeneral.appendChild(maxWrapper);
const presets = [ 25, 50, 100, 250, 500 ];
const presetContainer = document.createElement("div");
presetContainer.style.textAlign = "center";
presetContainer.style.marginBottom = "8px";
presets.forEach(preset => {
const btn = document.createElement("button");
btn.textContent = preset.toString();
btn.style.margin = "2px";
btn.style.padding = "4px 6px";
btn.addEventListener("click", () => {
if (maxInput)
maxInput.value = preset.toString();
});
presetContainer.appendChild(btn);
});
containerGeneral.appendChild(presetContainer);
const similarityLabel = document.createElement("label");
similarityLabel.textContent = "Similitud mínima para sugerencia de palabras:";
similarityLabel.title = "Ajusta el umbral mínimo de similitud (entre 80% y 95%) que se usará para sugerencias de reemplazo en nombres. El 100% es un reemplazo directo.";
similarityLabel.style.fontSize = "12px";
similarityLabel.style.display = "block";
similarityLabel.style.marginTop = "8px";
similarityLabel.style.marginBottom = "4px";
containerGeneral.appendChild(similarityLabel);
const similaritySlider = document.createElement("input");
similaritySlider.type = "range";
similaritySlider.min = "80";
similaritySlider.max = "95";
similaritySlider.value = "85";
similaritySlider.id = "similarityThreshold";
similaritySlider.style.width = "100%";
similaritySlider.title = "Desliza para ajustar la similitud mínima";
const similarityValueDisplay = document.createElement("span");
similarityValueDisplay.textContent = similaritySlider.value + "%";
similarityValueDisplay.style.marginLeft = "8px";
similaritySlider.addEventListener("input", () => {
similarityValueDisplay.textContent =
similaritySlider.value + "%";
});
containerGeneral.appendChild(similaritySlider);
containerGeneral.appendChild(similarityValueDisplay);
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);
}
}
function normalizePlaceName(word)
{
if (!word || typeof word !== "string")
{
return "";
}
// --- Mejorar: Normalizar palabras dentro de paréntesis, quitando espacios
// extra --- Detecta si la palabra está entre paréntesis, permitiendo
// espacios
const parenMatch = word.match(/^\(\s*(.+?)\s*\)$/);
if (parenMatch)
{
let innerContent = parenMatch[1].replace(/\s+/g, ' ').trim(); // Contenido limpio dentro del paréntesis
// Dividir el contenido interno en palabras y normalizar cada una
const wordsInsideParentheses = innerContent.split(' ');
const normalizedWordsInside = wordsInsideParentheses.map(singleWord => {
// Aplicar la normalización básica de capitalización a cada palabra
// (primera letra mayúscula, resto minúsculas),
// ignorando aquí si `singleWord` está en `excludedWords` para este caso específico.
if (!singleWord) return ""; // Manejar múltiples espacios que resultan en palabras vacías
// Lógica de normalización de capitalización simple (puedes refinarla si es necesario)
// Esta es la parte de normalizePlaceName que se encarga de la capitalización
// cuando no es un número romano o "MI".
if (singleWord.toUpperCase() === "MI")
{ // Mantener excepción para "MI"
return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
}
if (singleWord.toUpperCase() === "DI")
{ // Mantener excepción para "DI"
return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
}
const romanRegexStrict = /^M{0,3}(CM|CD?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
if (romanRegexStrict.test(singleWord)) {
return singleWord.toUpperCase();
}
// Normalización estándar de capitalización para otras palabras
return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
});
return "(" + normalizedWordsInside.join(' ') + ")";
}
// --- Lógica existente para palabras fuera de paréntesis (MANTENER) ---
// Manejar palabras con "/" primero, normalizando cada parte recursivamente
if (word.includes("/"))
{
return word.split("/").map(part => normalizePlaceName(part)).join("/");
}
// Excepción específica para "MI"
if (word.toUpperCase() === "MI")
{
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); // Resulta en "Mi"
}
// Regex para OTROS números romanos (incluyendo los que usan M, C, D, etc.)
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))
{
// Si la palabra es un número romano válido, convertirla TODA a
// mayúsculas.
return word.toUpperCase();
}
else
{
// Si hay un número seguido de letra (sin espacio), convertir la letra
// en mayúscula
word = word.replace(/(\d)([a-z])/g, (_, num, letter) => `${num}${letter.toUpperCase()}`);
// Si NO es un número romano, aplicar la normalización estándar:
// primera letra mayúscula, resto minúsculas.
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
}
// === Palabras especiales ===
let excludedWords = new Set(); // Inicializada en waitForSidebarAPI
let replacementWords = {}; // { "Urb.": "Urbanización", ... }
let dictionaryWords = new Set(); // O window.dictionaryWords = new Set();
function createExcludedWordsManager(parentContainer)
{
const section = document.createElement("div");
section.id = "excludedWordsManagerSection"; // ID para la sección
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
const title =
document.createElement("h4"); // Cambiado a h4 para jerarquía
title.textContent = "Gestión de Palabras Especiales";
title.style.fontSize =
"15px"; // Consistente con el otro título de sección
title.style.marginBottom = "10px"; // Más espacio abajo
section.appendChild(title);
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems =
"center"; // Alinear verticalmente
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra o frase";
input.style.flexGrow = "1";
input.style.padding = "6px"; // Mejor padding
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px"; // Mejor padding
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function() {
const newWord = input.value.trim();
const validation = isValidExcludedWord(newWord);
if (!validation.valid)
{
alert(validation.msg);
return;
}
// isValidExcludedWord ya comprueba duplicados en excludedWords y
// commonWords y ahora también si existe en dictionaryWords.
excludedWords.add(newWord);
input.value = "";
renderExcludedWordsList(
document.getElementById("excludedWordsList"));
saveExcludedWordsToLocalStorage(); // Asumiendo que tienes esta
// función
});
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px"; // Más espacio
const exportBtn = document.createElement("button");
exportBtn.textContent = "Exportar"; // Más corto
exportBtn.title = "Exportar Lista a XML";
exportBtn.style.padding = "6px 10px";
exportBtn.style.cursor = "pointer";
exportBtn.addEventListener("click", exportSharedDataToXml);
actionButtonsContainer.appendChild(exportBtn);
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar"; // Más corto
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function() {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?"))
{
excludedWords.clear();
renderExcludedWordsList(document.getElementById(
"excludedWordsList")); // Pasar el elemento UL
}
});
actionButtonsContainer.appendChild(clearBtn);
section.appendChild(actionButtonsContainer);
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en especiales...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)"; // Considerar padding y borde
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginBottom = "5px";
search.addEventListener("input", () => {
// Pasar el ulElement directamente
renderExcludedWordsList(
document.getElementById("excludedWordsList"),
search.value.trim());
});
section.appendChild(search);
const listContainerElement = document.createElement("ul");
listContainerElement.id = "excludedWordsList"; // Este es el UL
listContainerElement.style.maxHeight = "150px";
listContainerElement.style.overflowY = "auto";
listContainerElement.style.border = "1px solid #ddd";
listContainerElement.style.padding = "5px"; // Padding interno
listContainerElement.style.margin = "0"; // Resetear margen
listContainerElement.style.background = "#fff";
listContainerElement.style.listStyle = "none";
section.appendChild(listContainerElement);
const dropArea = document.createElement("div");
dropArea.textContent =
"Arrastra aquí el archivo XML de palabras especiales";
dropArea.style.border = "2px dashed #ccc"; // Borde más visible
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px"; // Más padding
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
dropArea.addEventListener("dragleave", () => {
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0]);
if (file &&
(file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(
evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
alert("Error al parsear el archivo XML.");
return;
}
// Detectar raíz
const rootTag =
xmlDoc.documentElement.tagName.toLowerCase();
if (rootTag !== "excludedwords" &&
rootTag !== "diccionario")
{
alert(
"El archivo XML no es válido. Debe tener <ExcludedWords> o <diccionario> como raíz.");
return;
}
// Importar palabras
const words = xmlDoc.getElementsByTagName("word");
let newWordsAddedCount = 0;
for (let i = 0; i < words.length; i++)
{
const val = words[i].textContent.trim();
if (val && !excludedWords.has(val))
{
excludedWords.add(val);
newWordsAddedCount++;
}
}
// Importar reemplazos si existen
const replacements =
xmlDoc.getElementsByTagName("replacement");
for (let i = 0; i < replacements.length; i++)
{
const from = replacements[i].getAttribute("from");
const to = replacements[i].textContent.trim();
if (from && to)
{
replacementWords[from] = to;
}
}
renderExcludedWordsList(
document.getElementById("excludedWordsList"));
alert(`Importación completada. Palabras nuevas: ${
newWordsAddedCount}`);
}
catch (err)
{
alert("Error procesando el archivo XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
parentContainer.appendChild(section);
}
// === Diccionario ===
function createDictionaryManager(parentContainer)
{
const section = document.createElement("div");
section.id = "dictionaryManagerSection";
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
const title = document.createElement("h4");
title.textContent = "Gestión del Diccionario";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
section.appendChild(title);
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems = "center"; // Alinear verticalmente
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra";
input.style.flexGrow = "1";
input.style.padding = "6px"; // Mejor padding
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px"; // Mejor padding
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function() {
const newWord = input.value.trim();
if (newWord)
{
const lowerNewWord = newWord.toLowerCase();
const alreadyExists =
Array.from(window.dictionaryWords)
.some(w => w.toLowerCase() === lowerNewWord);
if (commonWords.includes(lowerNewWord))
{
alert(
"La palabra es muy común y no debe agregarse a la lista.");
return;
}
if (alreadyExists)
{
alert("La palabra ya está en la lista (ignorando mayúsculas).");
return;
}
window.dictionaryWords.add(newWord);
input.value = "";
renderDictionaryList(
document.getElementById("dictionaryWordsList"));
}
});
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px"; // Más espacio
const exportBtn = document.createElement("button");
exportBtn.textContent = "Exportar"; // Más corto
exportBtn.title = "Exportar Diccionario a XML";
exportBtn.style.padding = "6px 10px";
exportBtn.style.cursor = "pointer";
exportBtn.addEventListener("click", exportDictionaryWordsList);
actionButtonsContainer.appendChild(exportBtn);
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar"; // Más corto
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function() {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?"))
{
window.dictionaryWords.clear();
renderDictionaryList(document.getElementById(
"dictionaryWordsList")); // Pasar el elemento UL
}
});
actionButtonsContainer.appendChild(clearBtn);
section.appendChild(actionButtonsContainer);
// Diccionario: búsqueda
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en diccionario...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)";
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginTop = "5px";
// On search input, render filtered list
search.addEventListener("input", () => {
renderDictionaryList(document.getElementById("dictionaryWordsList"),
search.value.trim());
});
section.appendChild(search);
// Lista UL para mostrar palabras del diccionario
const listContainerElement = document.createElement("ul");
listContainerElement.id = "dictionaryWordsList";
listContainerElement.style.maxHeight = "150px";
listContainerElement.style.overflowY = "auto";
listContainerElement.style.border = "1px solid #ddd";
listContainerElement.style.padding = "5px";
listContainerElement.style.margin = "0";
listContainerElement.style.background = "#fff";
listContainerElement.style.listStyle = "none";
section.appendChild(listContainerElement);
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML del diccionario";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
dropArea.addEventListener("dragleave", () => {
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
const file = e.dataTransfer.files[0];
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(evt.target.result,
"application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
console.error("[WME PLN] Error parseando XML:",
parserError.textContent);
alert(
"Error al parsear el archivo XML del diccionario.");
return;
}
const xmlWords = xmlDoc.querySelectorAll("word");
let newWordsAddedCount = 0;
for (let i = 0; i < xmlWords.length; i++)
{
const val = xmlWords[i].textContent.trim();
if (val && !window.dictionaryWords.has(val))
{
window.dictionaryWords.add(val);
newWordsAddedCount++;
}
}
if (newWordsAddedCount > 0)
console.log(`[WME PLN] ${
newWordsAddedCount} nuevas palabras añadidas desde XML.`);
// Renderizar la lista en el panel
renderDictionaryList(listContainerElement);
}
catch (err)
{
alert("Error procesando el diccionario XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
parentContainer.appendChild(section);
renderDictionaryList(listContainerElement);
}
function loadReplacementWordsFromStorage()
{
const savedReplacements = localStorage.getItem("replacementWordsList");
if (savedReplacements)
{
try
{
replacementWords = JSON.parse(savedReplacements);
if (typeof replacementWords !== 'object' ||
replacementWords === null)
{ // Asegurar que sea un objeto
replacementWords = {};
}
}
catch (e)
{
console.error("[WME PLN] Error cargando lista de reemplazos desde localStorage:", e);
replacementWords = {};
}
}
else
{
replacementWords = {}; // Inicializar si no hay nada guardado
}
console.log("[WME PLN] Reemplazos cargados:",
Object.keys(replacementWords).length,
"reglas.");
}
function saveReplacementWordsToStorage()
{
try
{
localStorage.setItem("replacementWordsList",
JSON.stringify(replacementWords));
// console.log("[WME PLN] Lista de reemplazos guardada en
// localStorage.");
}
catch (e)
{
console.error("[WME PLN] Error guardando lista de reemplazos en localStorage:", e);
}
}
// Renderiza la lista de reemplazos
function renderReplacementsList(ulElement)
{
//console.log("[WME PLN DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo");
if (!ulElement)
{
//console.error("[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList.");
return;
}
ulElement.innerHTML = ""; // Limpiar lista actual
const entries = Object.entries(replacementWords);
if (entries.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay reemplazos definidos.";
li.style.textAlign = "center";
li.style.color = "#777";
li.style.padding = "5px";
ulElement.appendChild(li);
return;
}
// Ordenar alfabéticamente por la palabra original (from)
entries.sort((a, b) =>
a[0].toLowerCase().localeCompare(b[0].toLowerCase()));
entries.forEach(([ from, to ]) => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
const textContainer = document.createElement("div");
textContainer.style.flexGrow = "1";
textContainer.style.overflow = "hidden";
textContainer.style.textOverflow = "ellipsis";
textContainer.style.whiteSpace = "nowrap";
textContainer.title = `Reemplazar "${from}" con "${to}"`;
const fromSpan = document.createElement("span");
fromSpan.textContent = from;
fromSpan.style.fontWeight = "bold";
textContainer.appendChild(fromSpan);
const arrowSpan = document.createElement("span");
arrowSpan.textContent = " → ";
arrowSpan.style.margin = "0 5px";
textContainer.appendChild(arrowSpan);
const toSpan = document.createElement("span");
toSpan.textContent = to;
toSpan.style.color = "#007bff";
textContainer.appendChild(toSpan);
li.appendChild(textContainer);
// Botón Editar
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar este reemplazo";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px 4px";
editBtn.style.fontSize = "14px";
editBtn.style.marginLeft = "4px";
editBtn.addEventListener("click", () => {
const newFrom = prompt("Editar texto original:", from);
if (newFrom === null) return;
const newTo = prompt("Editar texto de reemplazo:", to);
if (newTo === null) return;
if (!newFrom.trim()) {
alert("El campo 'Texto Original' es requerido.");
return;
}
if (newFrom === newTo) {
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
// Si cambia la clave, elimina la anterior
if (newFrom !== from) delete replacementWords[from];
replacementWords[newFrom] = newTo;
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
});
// Botón Eliminar
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = `Eliminar este reemplazo`;
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px 4px";
deleteBtn.style.fontSize = "14px";
deleteBtn.style.marginLeft = "4px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`))
{
delete replacementWords[from];
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
}
});
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "4px";
btnContainer.appendChild(editBtn);
btnContainer.appendChild(deleteBtn);
li.appendChild(btnContainer);
ulElement.appendChild(li);
});
}
function exportSharedDataToXml()
{
if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
{
alert(
"No hay palabras especiales ni reemplazos definidos para exportar.");
return;
}
let xmlParts = [];
// Exportar palabras excluidas
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.forEach(w => xmlParts.push(` <word>${xmlEscape(w)}</word>`));
// Exportar reemplazos
Object.entries(replacementWords)
.sort((a, b) => a[0].toLowerCase().localeCompare(
b[0].toLowerCase())) // Ordenar por 'from'
.forEach(([ from, to ]) => {
xmlParts.push(` <replacement from="${xmlEscape(from)}">${
xmlEscape(to)}</replacement>`);
});
const xmlContent =
`<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n${
xmlParts.join("\n")}\n</ExcludedWords>`;
const blob =
new Blob([ xmlContent ], { type : "application/xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// Puedes darle un nombre genérico ya que contiene ambos tipos de datos
a.download = "wme_normalizer_data_export.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function handleXmlFileDrop(file)
{
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc =
parser.parseFromString(evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
alert("Error al parsear el archivo XML: " +
parserError.textContent);
return;
}
const rootTag = xmlDoc.documentElement.tagName.toLowerCase();
if (rootTag !== "excludedwords")
{ // Asumiendo que la raíz sigue siendo esta
alert(
"El archivo XML no es válido. Debe tener <ExcludedWords> como raíz.");
return;
}
let newExcludedAdded = 0;
let newReplacementsAdded = 0;
let replacementsOverwritten = 0;
// Importar palabras excluidas
const words = xmlDoc.getElementsByTagName("word");
for (let i = 0; i < words.length; i++)
{
const val = words[i].textContent.trim();
if (val && !excludedWords.has(val))
{ // Asegúrate que isValidExcludedWord no lo bloquee si
// viene de XML
const validation =
isValidExcludedWord(val); // Revalidar o ajustar esta
// lógica para importación
if (validation.valid)
{ // Omitir esta validación si el XML es la fuente de
// verdad
excludedWords.add(val);
newExcludedAdded++;
}
else
{
console.warn(`Palabra excluida omitida desde XML "${
val}": ${validation.msg}`);
}
}
}
// Importar reemplazos
const replacements = xmlDoc.getElementsByTagName("replacement");
for (let i = 0; i < replacements.length; i++)
{
const from = replacements[i].getAttribute("from")?.trim();
const to = replacements[i].textContent.trim();
if (from && to)
{ // Permitir que 'to' esté vacío si se quiere "reemplazar
// con nada"
if (replacementWords.hasOwnProperty(from) &&
replacementWords[from] !== to)
{
replacementsOverwritten++;
}
else if (!replacementWords.hasOwnProperty(from))
{
newReplacementsAdded++;
}
replacementWords[from] = to;
}
}
// Guardar y Re-renderizar AMBAS listas
saveExcludedWordsToLocalStorage(); // Asumo que tienes esta
// función o la adaptas
saveReplacementWordsToStorage();
// Re-renderizar las listas en sus respectivas pestañas si están
// visibles o al activarse
const excludedListElement =
document.getElementById("excludedWordsList");
if (excludedListElement)
renderExcludedWordsList(excludedListElement);
const replacementsListElement =
document.getElementById("replacementsListElementID");
if (replacementsListElement)
renderReplacementsList(replacementsListElement);
alert(`Importación completada.\nPalabras Especiales nuevas: ${
newExcludedAdded}\nReemplazos nuevos: ${
newReplacementsAdded}\nReemplazos sobrescritos: ${
replacementsOverwritten}`);
}
catch (err)
{
console.error(
"[WME PLN] Error procesando el archivo XML importado:", err);
alert("Ocurrió un error procesando el archivo XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
}
function createReplacementsManager(parentContainer)
{
parentContainer.innerHTML = ''; // Limpiar por si acaso
const title = document.createElement("h4");
title.textContent = "Gestión de Reemplazos de Palabras/Frases";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
parentContainer.appendChild(title);
// Sección para añadir nuevos reemplazos
const addSection = document.createElement("div");
addSection.style.display = "flex";
addSection.style.gap = "8px";
addSection.style.marginBottom = "12px";
addSection.style.alignItems = "flex-end"; // Alinear inputs y botón
const fromInputContainer = document.createElement("div");
fromInputContainer.style.flexGrow = "1";
const fromLabel = document.createElement("label");
fromLabel.textContent = "Texto Original:";
fromLabel.style.display = "block";
fromLabel.style.fontSize = "12px";
fromLabel.style.marginBottom = "2px";
const fromInput = document.createElement("input");
fromInput.type = "text";
fromInput.placeholder = "Ej: Urb.";
fromInput.style.width = "95%"; // Para que quepa bien
fromInput.style.padding = "6px";
fromInput.style.border = "1px solid #ccc";
fromInputContainer.appendChild(fromLabel);
fromInputContainer.appendChild(fromInput);
addSection.appendChild(fromInputContainer);
const toInputContainer = document.createElement("div");
toInputContainer.style.flexGrow = "1";
const toLabel = document.createElement("label");
toLabel.textContent = "Texto de Reemplazo:";
toLabel.style.display = "block";
toLabel.style.fontSize = "12px";
toLabel.style.marginBottom = "2px";
const toInput = document.createElement("input");
toInput.type = "text";
toInput.placeholder = "Ej: Urbanización";
toInput.style.width = "95%";
toInput.style.padding = "6px";
toInput.style.border = "1px solid #ccc";
toInputContainer.appendChild(toLabel);
toInputContainer.appendChild(toInput);
addSection.appendChild(toInputContainer);
fromInput.setAttribute('spellcheck', 'false');
toInput.setAttribute('spellcheck', 'false');
const addReplacementBtn = document.createElement("button");
addReplacementBtn.textContent = "Añadir";
addReplacementBtn.style.padding = "6px 10px";
addReplacementBtn.style.cursor = "pointer";
addReplacementBtn.style.height = "30px"; // Para alinear con los inputs
addSection.appendChild(addReplacementBtn);
parentContainer.appendChild(addSection);
// Elemento UL para la lista de reemplazos
const listElement = document.createElement("ul");
listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista
listElement.style.maxHeight = "150px";
listElement.style.overflowY = "auto";
listElement.style.border = "1px solid #ddd";
listElement.style.padding = "8px";
listElement.style.margin = "0 0 10px 0";
listElement.style.background = "#fff";
listElement.style.listStyle = "none";
parentContainer.appendChild(listElement);
// Event listener para el botón "Añadir"
addReplacementBtn.addEventListener("click", () => {
const fromValue = fromInput.value.trim();
const toValue = toInput.value.trim();
if (!fromValue)
{
alert("El campo 'Texto Original' es requerido.");
return;
}
if (fromValue === toValue)
{
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue)
{
if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`))
return;
}
replacementWords[fromValue] = toValue;
fromInput.value = "";
toInput.value = "";
// Renderiza toda la lista (más seguro y rápido en la práctica)
renderReplacementsList(listElement);
saveReplacementWordsToStorage();
});
// Coloca esta función junto a aplicarReemplazosGenerales y aplicarReglasEspecialesNombre
/*function aplicarReemplazosDefinidos(text, replacementRules)
{
let newText = text;
if (typeof replacementRules !== 'object' || Object.keys(replacementRules).length === 0) {
return newText; // No hay reglas o no es un objeto, devolver el texto original
}
// Ordenar las claves de reemplazo por longitud descendente para manejar casos como
// "D1 Super" y "D1" donde "D1 Super" debe procesarse primero.
const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);
for (const fromKey of sortedFromKeys) {
const toValue = replacementRules[fromKey];
// Crear una expresión regular para encontrar 'fromKey' como palabra completa,
// insensible a mayúsculas/minúsculas (flag 'gi').
// escapeRegExp es importante si 'fromKey' puede contener caracteres especiales de regex.
const regex = new RegExp('\\b' + escapeRegExp(fromKey) + '\\b', 'gi');
newText = newText.replace(regex, (matchedInText, offset, originalString) => {
// matchedInText: La cadena que coincidió en el texto (ej. "d1", "D1").
// fromKey: La clave canónica del objeto replacementWords (ej. "D1").
// toValue: El valor de reemplazo canónico (ej. "Tiendas D1").
// Condición para evitar reemplazo redundante:
// Verificar si 'matchedInText' ya forma parte de 'toValue' en el contexto correcto.
// Ej: fromKey="D1", toValue="Tiendas D1".
// Si encontramos "D1" en "Mis Tiendas D1", queremos evitar cambiarlo a "Mis Tiendas Tiendas D1".
// Encontrar dónde estaría 'fromKey' (canónico) dentro de 'toValue' (canónico)
// para alinear la comprobación. Hacemos esta parte insensible a mayúsculas para la búsqueda de índice.
const fromKeyLower = fromKey.toLowerCase();
const toValueLower = toValue.toLowerCase();
const indexOfFromInTo = toValueLower.indexOf(fromKeyLower);
if (indexOfFromInTo !== -1) {
// Calculamos el inicio de donde podría estar el 'toValue' completo en el 'originalString'
const potentialExistingToStart = offset - indexOfFromInTo;
if (potentialExistingToStart >= 0 &&
(potentialExistingToStart + toValue.length) <= originalString.length) {
const substringInOriginal = originalString.substring(potentialExistingToStart, potentialExistingToStart + toValue.length);
// Comparamos (insensible a mayúsculas) si el 'toValue' ya existe en esa posición
if (substringInOriginal.toLowerCase() === toValueLower) {
// Ya existe el 'toValue' completo en el lugar correcto, así que no reemplazamos.
// Devolvemos el texto que coincidió originalmente.
return matchedInText;
}
}
}
// Si no se cumple la condición anterior, realizamos el reemplazo con 'toValue'.
return toValue;
});
}
return newText;
}*/
// Botones de Acción y Drop Area (usarán la lógica compartida)
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px";
const exportButton = document.createElement("button");
exportButton.textContent = "Exportar Todo";
exportButton.title = "Exportar Excluidas y Reemplazos a XML";
exportButton.style.padding = "6px 10px";
exportButton.addEventListener(
"click", exportSharedDataToXml); // Llamar a la función compartida
actionButtonsContainer.appendChild(exportButton);
const clearButton = document.createElement("button");
clearButton.textContent = "Limpiar Reemplazos";
clearButton.title = "Limpiar solo la lista de reemplazos";
clearButton.style.padding = "6px 10px";
clearButton.addEventListener("click", () => {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?"))
{
replacementWords = {};
saveReplacementWordsToStorage();
renderReplacementsList(listElement);
}
});
actionButtonsContainer.appendChild(clearButton);
parentContainer.appendChild(actionButtonsContainer);
const dropArea = document.createElement("div");
dropArea.textContent =
"Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)";
// ... (estilos para dropArea como en createExcludedWordsManager) ...
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
});
dropArea.addEventListener("dragleave",
() => { dropArea.style.background = "#f9f9f9"; });
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
handleXmlFileDrop(
e.dataTransfer.files[0]); // Usar una función manejadora compartida
});
parentContainer.appendChild(dropArea);
// Cargar y renderizar la lista inicial
renderReplacementsList(listElement);
}
// === Renderizar lista de palabras excluidas ===
function renderExcludedWordsList(ulElement, filter = "")
{ // AHORA RECIBE ulElement
if (!ulElement)
{
// Intentar obtenerlo por ID como último recurso si no se pasó,
// pero idealmente siempre se pasa desde el llamador.
ulElement = document.getElementById("excludedWordsList");
if (!ulElement)
{
//console.error("[WME PLN] Contenedor 'excludedWordsList' no encontrado para renderizar.");
return;
}
}
const currentFilter = filter.toLowerCase();
/* console.log("[WME PLN] Renderizando lista. Filtro:",
currentFilter,
"Total palabras:",
excludedWords.size);*/
ulElement.innerHTML = ""; // Limpiar lista anterior
const wordsToRender =
Array.from(excludedWords)
.filter(word => word.toLowerCase().includes(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(
b.toLowerCase())); // Ordenar alfabéticamente
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.style.padding = "5px";
li.style.textAlign = "center";
li.style.color = "#777";
if (excludedWords.size === 0)
{
li.textContent = "La lista está vacía.";
}
else if (currentFilter !== "")
{
li.textContent = "No hay coincidencias para el filtro.";
}
else
{
li.textContent =
"La lista está vacía (o error inesperado)."; // Fallback
}
ulElement.appendChild(li);
}
else
{
wordsToRender.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px"; // Ajuste
li.style.borderBottom = "1px solid #f0f0f0";
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.maxWidth =
"calc(100% - 60px)"; // Dejar espacio para botones
wordSpan.style.overflow = "hidden";
wordSpan.style.textOverflow = "ellipsis";
wordSpan.style.whiteSpace = "nowrap";
wordSpan.title = word;
li.appendChild(wordSpan);
const iconContainer = document.createElement("span");
iconContainer.style.display = "flex";
iconContainer.style.gap = "8px"; // Más espacio entre iconos
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px"; // Iconos un poco más grandes
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word)
{ // Permitir string vacío para borrar si se quisiera, pero
// trim() lo evita
const trimmedNewWord = newWord.trim();
if (trimmedNewWord === "")
{
alert("La palabra no puede estar vacía.");
return;
}
if (excludedWords.has(trimmedNewWord) &&
trimmedNewWord !== word)
{
alert("Esa palabra ya existe en la lista.");
return;
}
excludedWords.delete(word);
excludedWords.add(trimmedNewWord);
renderExcludedWordsList(ulElement, currentFilter);
}
});
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Estás seguro de que deseas eliminar la palabra '${
word}'?`))
{
excludedWords.delete(word);
renderExcludedWordsList(ulElement, currentFilter);
}
});
iconContainer.appendChild(editBtn);
iconContainer.appendChild(deleteBtn);
li.appendChild(iconContainer);
ulElement.appendChild(li);
});
}
try
{
localStorage.setItem("excludedWordsList",
JSON.stringify(Array.from(excludedWords)));
// console.log("[WME PLN] Lista guardada en localStorage:",
// Array.from(excludedWords));
}
catch (e)
{
console.error("[WME PLN] Error guardando en localStorage:", e);
// Considerar no alertar cada vez para no ser molesto si el localStorage
// está lleno. Podría ser un mensaje en consola o una notificación sutil
// en la UI.
}
}
// Nueva función: renderDictionaryList
function renderDictionaryList(ulElement, filter = "")
{
if (!ulElement || !window.dictionaryWords)
return;
const currentFilter = filter.toLowerCase();
ulElement.innerHTML = "";
const wordsToRender =
Array.from(window.dictionaryWords)
.filter(word => word.toLowerCase().startsWith(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = window.dictionaryWords.size === 0
? "El diccionario está vacío."
: "No hay coincidencias.";
li.style.textAlign = "center";
li.style.color = "#777";
ulElement.appendChild(li);
// Guardar diccionario también cuando está vacío
try
{
localStorage.setItem(
"dictionaryWordsList",
JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error( "[WME PLN] Error guardando el diccionario en localStorage:", e);
}
return;
}
wordsToRender.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.maxWidth = "calc(100% - 60px)";
wordSpan.style.overflow = "hidden";
wordSpan.style.textOverflow = "ellipsis";
wordSpan.style.whiteSpace = "nowrap";
wordSpan.title = word;
li.appendChild(wordSpan);
const iconContainer = document.createElement("span");
iconContainer.style.display = "flex";
iconContainer.style.gap = "8px";
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word)
{
window.dictionaryWords.delete(word);
window.dictionaryWords.add(newWord.trim());
renderDictionaryList(ulElement, currentFilter);
}
});
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`))
{
window.dictionaryWords.delete(word);
renderDictionaryList(ulElement, currentFilter);
}
});
iconContainer.appendChild(editBtn);
iconContainer.appendChild(deleteBtn);
li.appendChild(iconContainer);
ulElement.appendChild(li);
});
// Guardar el diccionario actualizado en localStorage después de cada
// render
try
{
localStorage.setItem(
"dictionaryWordsList",
JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando el diccionario en localStorage:", e);
}
}
function exportExcludedWordsList()
{
if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
{
alert("No hay palabras especiales ni reemplazos para exportar.");
return;
}
let xmlContent =
`<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n`;
xmlContent +=
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map(w => ` <word>${xmlEscape(w)}</word>`)
.join("\n");
if (Object.keys(replacementWords).length > 0)
{
xmlContent += "\n";
xmlContent +=
Object.entries(replacementWords)
.map(([ from, to ]) => ` <replacement from="${
xmlEscape(from)}">${xmlEscape(to)}</replacement>`)
.join("\n");
}
xmlContent += "\n</ExcludedWords>";
const blob =
new Blob([ xmlContent ], { type : "application/xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wme_excluded_words_export.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function exportDictionaryWordsList()
{
if (window.dictionaryWords.size === 0)
{
alert(
"La lista de palabras del diccionario está vacía. Nada que exportar.");
return;
}
const xmlContent =
`<?xml version="1.0" encoding="UTF-8"?>\n<diccionario>\n${
Array.from(window.dictionaryWords)
.sort((a, b) => a.toLowerCase().localeCompare(
b.toLowerCase())) // Exportar ordenado
.map(w => ` <word>${xmlEscape(w)}</word>`) // Indentación y escape
.join("\n")}\n</diccionario>`;
const blob =
new Blob([ xmlContent ],
{ type : "application/xml;charset=utf-8" }); // Añadir charset
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wme_dictionary_words_export.xml"; // Nombre más descriptivo
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function xmlEscape(str)
{
return str.replace(/[<>&"']/g, function(match) {
switch (match)
{
case '<':
return '<';
case '>':
return '>';
case '&':
return '&';
case '"':
return '"';
case "'":
return ''';
default:
return match;
}
});
}
waitForSidebarAPI();
})();
// Función reutilizable para mostrar el spinner de carga
function showLoadingSpinner()
{
const scanSpinner = document.createElement("div");
scanSpinner.id = "scanSpinnerOverlay";
scanSpinner.style.position = "fixed";
scanSpinner.style.top = "0";
scanSpinner.style.left = "0";
scanSpinner.style.width = "100%";
scanSpinner.style.height = "100%";
scanSpinner.style.background = "rgba(0, 0, 0, 0.5)";
scanSpinner.style.zIndex = "10000";
scanSpinner.style.display = "flex";
scanSpinner.style.justifyContent = "center";
scanSpinner.style.alignItems = "center";
const scanContent = document.createElement("div");
scanContent.style.background = "#fff";
scanContent.style.padding = "20px";
scanContent.style.borderRadius = "8px";
scanContent.style.textAlign = "center";
const spinner = document.createElement("div");
spinner.classList.add("spinner");
spinner.style.border = "6px solid #f3f3f3";
spinner.style.borderTop = "6px solid #3498db";
spinner.style.borderRadius = "50%";
spinner.style.width = "40px";
spinner.style.height = "40px";
spinner.style.animation = "spin 1s linear infinite";
spinner.style.margin = "0 auto 10px auto";
const progressText = document.createElement("div");
progressText.id = "scanProgressText";
progressText.textContent = "Analizando lugares: 0%";
progressText.style.fontSize = "14px";
progressText.style.color = "#333";
scanContent.appendChild(spinner);
scanContent.appendChild(progressText);
scanSpinner.appendChild(scanContent);
document.body.appendChild(scanSpinner);
const style = document.createElement("style");
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}