אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyforks.org/scripts/548864/1657867/WME%20PLN%20Module%20-%20UI%20Handler.js
// ==UserScript==
// @name WME PLN Module - UI Handler
// @version 9.0.0
// @description Módulo de UI para WME Place Normalizer. No funciona por sí solo.
// @author mincho77
// @license MIT
// @grant none
// ==/UserScript==
// Ícono en base64 para la pestaña principal del script
let floatingPanelElement = null;
// Función para crear la pestaña lateral del script
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')
{
plnLog('warn', "[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"))
{
plnLog('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;
}
throw e; // Relanzar otros errores para que se vean en consola
}
const { tabLabel, tabPane } = registration;
if (!tabLabel || !tabPane)
{
plnLog('ui', "[WME PLN] Registro de pestaña incompleto (sin label o pane).");
return;
}
// Configurar el ícono y nombre de la pestaña principal del script
// Corrección aquí: usar directamente MAIN_TAB_ICON_BASE64 en el src
tabLabel.innerHTML = `
<img src="${MAIN_TAB_ICON_BASE64}" style="height: 16px; vertical-align: middle; margin-right: 5px;">
NrmliZer
`;
// 3. Inicializar las pestañas internas (General, Especiales,
// Diccionario, Reemplazos)
const tabsContainer = document.createElement("div");
tabsContainer.style.display = "flex";
tabsContainer.style.marginBottom = "8px";
tabsContainer.style.gap = "8px";
const tabButtons = {};
const tabContents = {}; // Objeto para guardar los divs de contenido
// Crear botones para cada pestaña
tabNames.forEach(({ label, icon }) =>
{
const btn = document.createElement("button");
btn.innerHTML = icon
? `<span style="display: inline-flex; align-items: center; font-size: 11px;">
<span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label}
</span>`
: `<span style="font-size: 11px;">${label}</span>`;
btn.style.fontSize = "11px";
btn.style.padding = "4px 8px";
btn.style.marginRight = "4px";
btn.style.minHeight = "28px";
btn.style.border = "1px solid #ccc";
btn.style.borderRadius = "4px 4px 0 0";
btn.style.cursor = "pointer";
btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada
btn.className = "custom-tab-style";
// Agrega el tooltip personalizado para cada tab
if (label === "Gene") btn.title = "Configuración general";
else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)";
else if (label === "Dicc") btn.title = "Diccionario de palabras válidas";
else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos";
// Estilo inicial: la primera pestaña es la activa
if (label === tabNames[0].label)
{
btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco)
btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa
btn.style.fontWeight = "bold";
}
else
{
btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro)
btn.style.fontWeight = "normal";
}
btn.addEventListener("click", () =>
{
tabNames.forEach(({ label: tabLabel_inner }) =>
{
const isActive = (tabLabel_inner === label);
const currentButton = tabButtons[tabLabel_inner];
if (tabContents[tabLabel_inner])
{
tabContents[tabLabel_inner].style.display = isActive ? "block" : "none";
}
if (currentButton)
{
// Aplicar/Quitar estilos de pestaña activa directamente
if (isActive)
{
currentButton.style.backgroundColor = "#ffffff"; // Activo
currentButton.style.borderBottom = "2px solid #007bff";
currentButton.style.fontWeight = "bold";
}
else
{
currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo
currentButton.style.borderBottom = "none";
currentButton.style.fontWeight = "normal";
}
}
// Llamar a la función de renderizado correspondiente
if (isActive)
{
if (tabLabel_inner === "Espe")
{
const ul = document.getElementById("excludedWordsList");
if (ul && typeof window.renderExcludedWordsList === 'function') window.renderExcludedWordsList(ul);
}
else if (tabLabel_inner === "Dicc")
{
const ulDict = document.getElementById("dictionaryWordsList");
if (ulDict && typeof window.renderDictionaryList === 'function') window.renderDictionaryList(ulDict);
}
else if (tabLabel_inner === "Reemp")
{
const ulReemplazos = document.getElementById("replacementsListElementID");
if (ulReemplazos && typeof window.renderReplacementsList === 'function') window.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)
{
// Crear el contenedor principal
const mainTitle = document.createElement("h3");
mainTitle.textContent = "NormliZer";
mainTitle.style.textAlign = "center";
mainTitle.style.fontSize = "20px";
mainTitle.style.marginBottom = "2px";
containerGeneral.appendChild(mainTitle);
// Crear el subtítulo (información de la versión)
const versionInfo = document.createElement("div");
versionInfo.textContent = "V. " + (window.PLN_META?.version || window.VERSION || '9.0.0'); // VERSION segura
versionInfo.style.textAlign = "right";
versionInfo.style.fontSize = "10px";
versionInfo.style.color = "#777";
versionInfo.style.marginBottom = "15px";
containerGeneral.appendChild(versionInfo);
//Crear un div para mostrar el ID del usuario
const userIdInfo = document.createElement("div"); //
userIdInfo.id = "wme-pln-user-id"; //
userIdInfo.textContent = "Cargando usuario..."; //
userIdInfo.style.textAlign = "right"; //
userIdInfo.style.fontSize = "10px"; //
userIdInfo.style.color = "#777"; //
userIdInfo.style.marginBottom = "15px"; //
containerGeneral.appendChild(userIdInfo); //
// Esta función reemplaza la necesidad de las funciones getCurrentEditorViaSdk, etc.
const pollAndDisplayUserInfo = () =>
{
let pollingAttempts = 0;
const maxPollingAttempts = 60;
const pollInterval = setInterval(async () =>
{
let currentUserInfoLocal = null; //: Usar una variable local temporal
// Primero intentar con wmeSDK.State.getUserInfo() ***
if (wmeSDK && wmeSDK.State && typeof wmeSDK.State.getUserInfo === 'function')
{
try
{
const sdkUserInfo = await wmeSDK.State.getUserInfo();
if (sdkUserInfo && sdkUserInfo.userName)
{
currentUserInfoLocal = {
// Si sdkUserInfo.id NO existe, usar sdkUserInfo.userName DIRECTAMENTE (sin Number())
id: sdkUserInfo.id !== undefined ? sdkUserInfo.id : sdkUserInfo.userName, //
name: sdkUserInfo.userName,
privilege: sdkUserInfo.privilege || 'N/A'
};
// Asegurarse de que el ID es válido para el log
const displayId = typeof currentUserInfoLocal.id === 'number' ? currentUserInfoLocal.id : `"${currentUserInfoLocal.id}"`; //
}
else
{
}
}
catch (e)
{
}
}
else
{
plnLog('warn',`[WME_PLN][DEBUG] SDK.State.getUserInfo no disponible. wmeSDK:`, wmeSDK);
}
// Fallback a W.loginManager (si SDK.State no funcionó)
if (!currentUserInfoLocal && typeof W !== 'undefined' && W.loginManager && W.loginManager.userName && W.loginManager.userId) { //: Usar currentUserInfoLocal
currentUserInfoLocal = {
id: Number(W.loginManager.userId), // Convertir a número
name: W.loginManager.userName,
privilege: W.loginManager.userPrivilege || 'N/A'
};
plnLog('sdk', `[WME PLN][DEBUG] W.loginManager SUCCESS: Usuario obtenido: ${currentUserInfoLocal.name} (ID: ${currentUserInfoLocal.id})`);
}
else if (!currentUserInfoLocal)
{ //: Solo logear si aún no se encontró en ningún método
plnLog('warn',`[WME_PLN][DEBUG] W.loginManager devolvió datos incompletos o null:`, W?.loginManager);
}
if (currentUserInfoLocal && currentUserInfoLocal.id && currentUserInfoLocal.name)
{
clearInterval(pollInterval);
window.currentGlobalUserInfo = currentUserInfoLocal;
userIdInfo.textContent = `Editor Actual: ${window.currentGlobalUserInfo.name}`;
userIdInfo.title = `Privilegio: ${window.currentGlobalUserInfo.privilege}`;
window.updateStatsDisplay?.();//: Actualizar estadísticas con el nuevo usuario
plnLog('init', '[WME_PLN][DEBUG] USUARIO CARGADO EXITOSAMENTE mediante polling.');
const labelToUpdate = document.querySelector('label[for="chk-avoid-my-edits"]');
if (labelToUpdate)
{
labelToUpdate.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #007bff; font-weight: normal;">${currentGlobalUserInfo.name}</span>`;
}
const avoidMyEditsCheckbox = document.getElementById("chk-avoid-my-edits");
if (avoidMyEditsCheckbox)
{
avoidMyEditsCheckbox.disabled = false;
avoidMyEditsCheckbox.style.opacity = "1";
avoidMyEditsCheckbox.style.cursor = "pointer";
}
}
else if (pollingAttempts >= maxPollingAttempts - 1)
{
clearInterval(pollInterval);
userIdInfo.textContent = "Usuario no detectado (agotados intentos)";
plnLog('init', '[WME PLN][DEBUG] Polling agotado. Usuario no detectado después de varios intentos.');
// Asignar el estado de fallo a currentGlobalUserInfo
window.currentGlobalUserInfo = { id: 0, name: 'No detectado', privilege: 'N/A' };
// Actualizar el texto del checkbox para evitar ediciones del usuario
const avoidTextSpanToUpdate = document.querySelector("#chk-avoid-my-edits + label span");
//: Actualizar el texto del checkbox para evitar ediciones del usuario
if (avoidTextSpanToUpdate)
{
//: Usa innerHTML y estilo atenuado para el nombre "No detectado"
avoidTextSpanToUpdate.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #777; opacity: 0.5;">No detectado</span>`; //
avoidTextSpanToUpdate.style.opacity = "1"; //: Asegurar opacidad base para el span principal
// avoidTextSpanToUpdate.style.color = "#777"; //: Puedes quitar esta línea si el color del span es suficiente
}
const avoidMyEditsCheckbox = document.getElementById("chk-avoid-my-edits");
//: Deshabilitar el checkbox si no se detecta el usuario
if (avoidMyEditsCheckbox)
{
avoidMyEditsCheckbox.disabled = true;
avoidMyEditsCheckbox.style.opacity = "0.5";
avoidMyEditsCheckbox.style.cursor = "not-allowed";
}
}
pollingAttempts++;
}, 200);
};
// Iniciar el polling para la información del usuario
pollAndDisplayUserInfo(); //Llamada directa a la nueva función de polling
// Título de la sección de normalización
const normSectionTitle = document.createElement("h4");
normSectionTitle.textContent = "Análisis de Nombres de Places";
normSectionTitle.style.fontSize = "16px";
normSectionTitle.style.marginTop = "10px";
normSectionTitle.style.marginBottom = "5px";
normSectionTitle.style.borderBottom = "1px solid #eee";
normSectionTitle.style.paddingBottom = "3px";
containerGeneral.appendChild(normSectionTitle);
// Descripción de la sección
const scanButton = document.createElement("button");
scanButton.id = "pln-start-scan-btn";
scanButton.textContent = "Start Scan...";
scanButton.setAttribute("type", "button");
scanButton.style.marginBottom = "10px";
scanButton.style.fontSize = "14px";
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", () =>
{
window.disableScanControls?.();
scanButton.textContent = "Escaneando...";
const outputDiv = document.getElementById("wme-normalization-tab-output");
if (!outputDiv)
{
enableScanControls();
return;
}
let places = (typeof window.getVisiblePlaces === 'function') ? window.getVisiblePlaces() : [];
// Filtrar lugares excluidos ANTES de mostrar el conteo.
places = places.filter(place => !(window.excludedPlaces instanceof Map && window.excludedPlaces.has(place.getID())));
const totalPlacesToScan = places.length;
if (totalPlacesToScan === 0)
{
outputDiv.textContent = "No hay lugares visibles para analizar (o todos están excluidos).";
window.enableScanControls?.();
return;
}
// Mostrar el conteo correcto y verificado.
outputDiv.textContent = `Escaneando ${totalPlacesToScan} lugares...`;
// Llamar a la función de renderizado con la lista ya filtrada.
setTimeout(() => {
renderPlacesInFloatingPanel(places);
}, 10);
});
containerGeneral.appendChild(scanButton);
// Crear el contenedor para el checkbox de usuario
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.className = "pln-preset-btn"; // Clase para aplicar estilos comunes
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);
// Checkbox para recomendar categorías
const recommendCategoriesWrapper = document.createElement("div");
recommendCategoriesWrapper.style.marginTop = "10px";
recommendCategoriesWrapper.style.marginBottom = "5px";
recommendCategoriesWrapper.style.display = "flex";
recommendCategoriesWrapper.style.flexDirection = "column"; //Cambiar a columna para apilar checkboxes
recommendCategoriesWrapper.style.alignItems = "flex-start"; //Alinear ítems al inicio
recommendCategoriesWrapper.style.padding = "6px 8px"; // Añadir padding
recommendCategoriesWrapper.style.backgroundColor = "#e0f7fa"; // Fondo claro para destacar
recommendCategoriesWrapper.style.border = "1px solid #00bcd4"; // Borde azul
recommendCategoriesWrapper.style.borderRadius = "4px"; // Bordes redondeados
containerGeneral.appendChild(recommendCategoriesWrapper); //Añadir el wrapper aquí, antes de sus contenidos
// Contenedor para el checkbox "Recomendar categorías"
const recommendCategoryCheckboxRow = document.createElement("div"); //
recommendCategoryCheckboxRow.style.display = "flex"; //Fila para checkbox y etiqueta
recommendCategoryCheckboxRow.style.alignItems = "center"; //
recommendCategoryCheckboxRow.style.marginBottom = "5px"; //Margen inferior
// Crear el checkbox y la etiqueta
const recommendCategoriesCheckbox = document.createElement("input");
recommendCategoriesCheckbox.type = "checkbox";
recommendCategoriesCheckbox.id = "chk-recommend-categories";
recommendCategoriesCheckbox.style.marginRight = "8px";
const savedCategoryRecommendationState = localStorage.getItem("wme_pln_recommend_categories");
recommendCategoriesCheckbox.checked = (savedCategoryRecommendationState === "true");
const recommendCategoriesLabel = document.createElement("label");
recommendCategoriesLabel.htmlFor = "chk-recommend-categories";
recommendCategoriesLabel.style.fontSize = "14px";
recommendCategoriesLabel.style.cursor = "pointer";
recommendCategoriesLabel.style.fontWeight = "bold";
recommendCategoriesLabel.style.color = "#00796b";
recommendCategoriesLabel.style.display = "flex";
recommendCategoriesLabel.style.alignItems = "center";
const iconSpan = document.createElement("span");
iconSpan.innerHTML = "✨ ";
iconSpan.style.marginRight = "4px";
iconSpan.style.fontSize = "16px";
iconSpan.appendChild(document.createTextNode("Recomendar categorías"));
recommendCategoriesLabel.appendChild(iconSpan);
recommendCategoryCheckboxRow.appendChild(recommendCategoriesCheckbox); //
recommendCategoryCheckboxRow.appendChild(recommendCategoriesLabel); //
recommendCategoriesWrapper.appendChild(recommendCategoryCheckboxRow); //Añadir la fila al wrapper
recommendCategoriesCheckbox.addEventListener("change", () =>
{
localStorage.setItem("wme_pln_recommend_categories", recommendCategoriesCheckbox.checked ? "true" : "false");
});
// --- Contenedor para AGRUPAR las opciones de exclusión ---
const excludeContainer = document.createElement('div');
excludeContainer.style.marginTop = '8px'; // Espacio que lo separa de la opción de arriba
// --- Fila para el checkbox "Excluir lugares..." ---
const avoidMyEditsCheckboxRow = document.createElement("div");
avoidMyEditsCheckboxRow.style.display = "flex";
avoidMyEditsCheckboxRow.style.alignItems = "center";
//: Añadir un margen inferior para separar del checkbox de categorías
const avoidMyEditsCheckbox = document.createElement("input");
avoidMyEditsCheckbox.type = "checkbox";
avoidMyEditsCheckbox.id = "chk-avoid-my-edits";
avoidMyEditsCheckbox.style.marginRight = "8px";
const savedAvoidMyEditsState = localStorage.getItem("wme_pln_avoid_my_edits");
avoidMyEditsCheckbox.checked = (savedAvoidMyEditsState === "true");
avoidMyEditsCheckboxRow.appendChild(avoidMyEditsCheckbox);
//: Añadir un label con el texto de la opción
const avoidMyEditsLabel = document.createElement("label");
avoidMyEditsLabel.htmlFor = "chk-avoid-my-edits";
avoidMyEditsLabel.style.fontSize = "16px"; // Tamaño de fuente consistente
avoidMyEditsLabel.style.cursor = "pointer";
avoidMyEditsLabel.style.fontWeight = "bold";
avoidMyEditsLabel.style.color = "#00796b";
avoidMyEditsLabel.innerHTML = `Excluir lugares cuya última edición sea del Editor: <span style="color: #007bff; font-weight: normal;">Cargando...</span>`;
avoidMyEditsCheckboxRow.appendChild(avoidMyEditsLabel);
// --- Fila para el dropdown de fecha (sub-menú) ---
const dateFilterRow = document.createElement("div");
dateFilterRow.style.display = "flex";
dateFilterRow.style.alignItems = "center";
dateFilterRow.style.marginTop = "8px"; // Espacio entre el checkbox y esta fila
dateFilterRow.style.paddingLeft = "25px"; // Indentación para que parezca una sub-opción
dateFilterRow.style.gap = "8px";
//: Añadir un label para el dropdown
const dateFilterLabel = document.createElement("label");
dateFilterLabel.htmlFor = "dateFilterSelect";
dateFilterLabel.textContent = "Excluir solo ediciones de:";
dateFilterLabel.style.fontSize = "13px";
dateFilterLabel.style.fontWeight = "500";
dateFilterLabel.style.color = "#334";
dateFilterRow.appendChild(dateFilterLabel);
//: Crear el dropdown para seleccionar el filtro de fecha
const dateFilterSelect = document.createElement("select");
dateFilterSelect.id = "dateFilterSelect";
dateFilterSelect.style.padding = "5px 8px";
dateFilterSelect.style.border = "1px solid #b0c4de";
dateFilterSelect.style.borderRadius = "4px";
dateFilterSelect.style.backgroundColor = "#fff";
dateFilterSelect.style.flexGrow = "1";
dateFilterSelect.style.fontSize = "13px";
dateFilterSelect.style.cursor = "pointer";
// Añadir opciones al dropdown
const dateOptions = {
"all": "Elegir una opción",
"6_months": "Últimos 6 meses",
"3_months": "Últimos 3 meses",
"1_month": "Último mes",
"1_week": "Última Semana",
"1_day": "Último día"
};
// Añadir las opciones al dropdown
for (const [value, text] of Object.entries(dateOptions))
{
const option = document.createElement("option");
option.value = value;
option.textContent = text;
dateFilterSelect.appendChild(option);
}
// Cargar el valor guardado del localStorage
const savedDateFilter = localStorage.getItem("wme_pln_date_filter");
if (savedDateFilter)
{
dateFilterSelect.value = savedDateFilter;
}
dateFilterSelect.addEventListener("change", () =>
{
localStorage.setItem("wme_pln_date_filter", dateFilterSelect.value);
});
dateFilterRow.appendChild(dateFilterSelect);
// --- Añadir AMBAS filas al contenedor de exclusión ---
excludeContainer.appendChild(avoidMyEditsCheckboxRow);
excludeContainer.appendChild(dateFilterRow);
// --- Añadir el contenedor AGRUPADO al wrapper principal (el cuadro azul) ---
recommendCategoriesWrapper.appendChild(excludeContainer);
// --- Lógica para habilitar/deshabilitar el dropdown ---
const toggleDateFilterState = () =>
{
const isChecked = avoidMyEditsCheckbox.checked;
dateFilterSelect.disabled = !isChecked;
dateFilterRow.style.opacity = isChecked ? "1" : "0.5";
dateFilterRow.style.pointerEvents = isChecked ? "auto" : "none";
};
// --- Listener unificado para el checkbox ---
avoidMyEditsCheckbox.addEventListener("change", () =>
{
toggleDateFilterState(); // Actualiza la UI del dropdown
localStorage.setItem("wme_pln_avoid_my_edits", avoidMyEditsCheckbox.checked ? "true" : "false"); // Guarda el estado
});
// Llamada inicial para establecer el estado correcto al cargar
toggleDateFilterState();
// --- Contenedor para el checkbox de estadísticas ---
const statsContainer = document.createElement('div');
statsContainer.style.marginTop = '8px';
// Añadir un borde y fondo para destacar
const statsCheckboxRow = document.createElement("div");
statsCheckboxRow.style.display = "flex";
statsCheckboxRow.style.alignItems = "center";
// Añadir un margen inferior para separar del checkbox de exclusión
const statsCheckbox = document.createElement("input");
statsCheckbox.type = "checkbox";
statsCheckbox.id = "chk-enable-stats";
statsCheckbox.style.marginRight = "8px";
statsCheckbox.checked = localStorage.getItem(STATS_ENABLED_KEY) === 'true';
statsCheckboxRow.appendChild(statsCheckbox);
// Crear la etiqueta para el checkbox de estadísticas
const statsLabel = document.createElement("label");
statsLabel.htmlFor = "chk-enable-stats";
statsLabel.style.fontSize = "16px"; // Tamaño consistente
statsLabel.style.cursor = "pointer";
statsLabel.style.fontWeight = "bold";
statsLabel.style.color = "#00796b";
statsLabel.innerHTML = `📊 Habilitar panel de estadísticas`;
statsCheckboxRow.appendChild(statsLabel);
// Añadir un tooltip al checkbox de estadísticas
statsContainer.appendChild(statsCheckboxRow);
// Añadir el contenedor de estadísticas al wrapper principal (el cuadro azul)
recommendCategoriesWrapper.appendChild(statsContainer);
// Listener para el checkbox de estadísticas
statsCheckbox.addEventListener("change", () =>
{
localStorage.setItem(STATS_ENABLED_KEY, statsCheckbox.checked ? "true" : "false");
toggleStatsPanelVisibility();
});
//===========================Finaliza bloque de estadísticas
// Listener para guardar el estado del nuevo checkbox
avoidMyEditsCheckbox.addEventListener("change", () =>
{ //
localStorage.setItem("wme_pln_avoid_my_edits", avoidMyEditsCheckbox.checked ? "true" : "false"); //
});
// Barra de progreso y texto
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);
// Texto de progreso
const tabProgressText = document.createElement("div");
tabProgressText.style.fontSize = "13px";
tabProgressText.style.marginTop = "5px";
tabProgressText.id = "progressBarTextTab";
tabProgressText.textContent = "Progreso: 0% (0/0)";
containerGeneral.appendChild(tabProgressText);
// Div para mostrar el resultado del análisis
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
{
plnLog('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"])
createSpecialItemsManager(tabContents["Espe"]);
else
{
plnLog('error',"[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'.");
}
// --- Llamada A La Función Para Poblar La Nueva Pestaña "Diccionario"
if (tabContents["Dicc"])
{
createDictionaryManager(tabContents["Dicc"]);
}
else
{
plnLog('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
{
plnLog('error',"[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'.");
}
}
catch (error)
{
plnLog('error',"[WME PLN] Error creando la pestaña lateral:", error, error.stack);
}
} // Fin de createSidebarTab
//Permite crear un panel flotante para mostrar los resultados del escaneo
function createFloatingPanel(status = "processing", numInconsistents = 0)
{
if (!floatingPanelElement)
{
floatingPanelElement = document.createElement("div");
floatingPanelElement.id = "wme-place-inspector-panel";
floatingPanelElement.setAttribute("role", "dialog");
floatingPanelElement.setAttribute("aria-label", "NrmliZer: Panel de resultados");
floatingPanelElement.style.position = "fixed";
floatingPanelElement.style.zIndex = "10005"; // Z-INDEX DEL PANEL DE RESULTADOS
floatingPanelElement.style.background = "#fff";
floatingPanelElement.style.border = "1px solid #ccc";
floatingPanelElement.style.borderRadius = "8px";
floatingPanelElement.style.boxShadow = "0 5px 15px rgba(0,0,0,0.2)";
floatingPanelElement.style.padding = "10px";
floatingPanelElement.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
floatingPanelElement.style.display = 'none';
floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s"; // Agregado left y top a la transición
floatingPanelElement.style.overflow = "hidden";
// Variables para almacenar el estado del panel
floatingPanelElement._isMaximized = false;
floatingPanelElement._isMinimized = false;
floatingPanelElement._originalState = {};
floatingPanelElement._isDragging = false;
floatingPanelElement._currentStatus = status;
// Crear barra de título con controles
const titleBar = document.createElement("div");
titleBar.id = "wme-pln-titlebar";
titleBar.style.display = "flex";
titleBar.style.justifyContent = "space-between";
titleBar.style.alignItems = "center";
titleBar.style.marginBottom = "10px";
titleBar.style.userSelect = "none";
titleBar.style.cursor = "move";
titleBar.style.padding = "5px 0";
// Título del panel
const titleElement = document.createElement("h4");
titleElement.id = "wme-pln-panel-title";
titleElement.style.margin = "0";
titleElement.style.fontSize = "20px";
titleElement.style.color = "#333";
titleElement.style.fontWeight = "bold";
titleElement.style.flex = "1";
titleElement.style.textAlign = "center";
// Contenedor de controles estilo macOS
const controlsContainer = document.createElement("div");
controlsContainer.style.display = "flex";
controlsContainer.style.gap = "8px";
controlsContainer.style.alignItems = "center";
controlsContainer.style.position = "absolute";
controlsContainer.style.left = "15px";
controlsContainer.style.top = "15px";
// Función para crear botones estilo macOS
function createMacButton(color, action, tooltip) {
const btn = document.createElement("div");
btn.style.width = "12px";
btn.style.height = "12px";
btn.style.borderRadius = "50%";
btn.style.backgroundColor = color;
btn.style.cursor = "pointer";
btn.style.border = "1px solid rgba(0,0,0,0.1)";
btn.style.display = "flex";
btn.style.alignItems = "center";
btn.style.justifyContent = "center";
btn.style.fontSize = "8px";
btn.style.color = "rgba(0,0,0,0.6)";
btn.style.transition = "all 0.2s";
btn.title = tooltip;
// Efectos hover
btn.addEventListener("mouseenter", () => {
btn.style.transform = "scale(1.1)";
if (color === "#ff5f57") btn.textContent = "×";
else if (color === "#ffbd2e") btn.textContent = "−";
else if (color === "#28ca42") btn.textContent = action === "maximize" ? "⬜" : "🗗";
});
btn.addEventListener("mouseleave", () => {
btn.style.transform = "scale(1)";
btn.textContent = "";
});
btn.addEventListener("click", action);
return btn;
}
// Botón cerrar (rojo)
const closeBtn = createMacButton("#ff5f57", async () => {
if (floatingPanelElement._currentStatus === "processing")
{
const confirmCancel = await (window.plnUiConfirm ? window.plnUiConfirm("¿Detener la búsqueda en progreso?", { okText: "Detener", cancelText: "Continuar" }) : Promise.resolve(true));
if (confirmCancel !== true) return;
window.resetInspectorState?.();
}
if (floatingPanelElement) floatingPanelElement.style.display = 'none';
window.resetInspectorState?.();
}, "Cerrar panel");
// Botón minimizar (amarillo)
const minimizeBtn = createMacButton("#ffbd2e", () => {
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
if (!floatingPanelElement._isMinimized) {
// Guardar estado actual antes de minimizar
floatingPanelElement._originalState = {
width: floatingPanelElement.style.width,
height: floatingPanelElement.style.height,
top: floatingPanelElement.style.top,
left: floatingPanelElement.style.left,
transform: floatingPanelElement.style.transform,
outputHeight: outputDiv ? outputDiv.style.height : 'auto'
};
// Minimizar - mover a la parte superior
floatingPanelElement.style.top = "20px";
floatingPanelElement.style.left = "50%";
floatingPanelElement.style.transform = "translateX(-50%)";
floatingPanelElement.style.height = "50px";
floatingPanelElement.style.width = "300px";
if (outputDiv) outputDiv.style.display = "none";
floatingPanelElement._isMinimized = true;
updateButtonVisibility();
} else {
// Restaurar desde minimizado
const originalState = floatingPanelElement._originalState;
floatingPanelElement.style.width = originalState.width;
floatingPanelElement.style.height = originalState.height;
floatingPanelElement.style.top = originalState.top;
floatingPanelElement.style.left = originalState.left;
floatingPanelElement.style.transform = originalState.transform;
if (outputDiv) {
outputDiv.style.display = "block";
outputDiv.style.height = originalState.outputHeight;
}
floatingPanelElement._isMinimized = false;
updateButtonVisibility();
}
}, "Minimizar panel");
// Botón maximizar (verde)
// const maximizeBtn = createMacButton("#28ca42", () => {
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
// Función para actualizar visibilidad de botones
// Replace the updateButtonVisibility function in createFloatingPanel
function updateButtonVisibility()
{
const isProcessing = floatingPanelElement._currentStatus === "processing";
// Limpiar contenedor
controlsContainer.innerHTML = "";
if (isProcessing) {
// Solo botón cerrar durante la búsqueda
controlsContainer.appendChild(closeBtn);
} else if (floatingPanelElement._isMinimized) {
// Minimizado: cerrar y restaurar
controlsContainer.appendChild(closeBtn);
// Crear botón de restaurar si estamos minimizados
const restoreBtn = createMacButton("#28ca42", () => {
// Restaurar desde minimizado
const originalState = floatingPanelElement._originalState;
floatingPanelElement.style.width = originalState.width;
floatingPanelElement.style.height = originalState.height;
floatingPanelElement.style.top = originalState.top;
floatingPanelElement.style.left = originalState.left;
floatingPanelElement.style.transform = originalState.transform;
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
if (outputDiv) {
outputDiv.style.display = "block";
outputDiv.style.height = originalState.outputHeight;
}
floatingPanelElement._isMinimized = false;
updateButtonVisibility();
}, "Restaurar panel");
restoreBtn.textContent = "🗗";
controlsContainer.appendChild(restoreBtn);
} else {
// Normal: cerrar y minimizar
controlsContainer.appendChild(closeBtn);
controlsContainer.appendChild(minimizeBtn);
}
}// updateButtonVisibility
// Funcionalidad de arrastrar
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
titleBar.addEventListener("mousedown", (e) => {
if (e.target === titleBar || e.target === titleElement) {
isDragging = true;
const rect = floatingPanelElement.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
floatingPanelElement.style.transition = "none";
e.preventDefault();
}
});
document.addEventListener("mousemove", (e) => {
if (isDragging && !floatingPanelElement._isMaximized) {
const newLeft = e.clientX - dragOffset.x;
const newTop = e.clientY - dragOffset.y;
floatingPanelElement.style.left = `${newLeft}px`;
floatingPanelElement.style.top = `${newTop}px`;
floatingPanelElement.style.transform = "none";
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s";
}
});
// Agregar controles y título a la barra
titleBar.appendChild(controlsContainer);
titleBar.appendChild(titleElement);
// Agregar barra de título al panel
floatingPanelElement.appendChild(titleBar);
// Contenido del panel
const outputDivLocal = document.createElement("div");
outputDivLocal.id = "wme-place-inspector-output";
outputDivLocal.setAttribute("aria-live", "polite");
outputDivLocal.style.fontSize = "18px";
outputDivLocal.style.backgroundColor = "#fdfdfd";
outputDivLocal.style.overflowY = "auto";
outputDivLocal.style.flex = "1";
floatingPanelElement.appendChild(outputDivLocal);
// Función para actualizar botones (hacer accesible)
floatingPanelElement._updateButtonVisibility = updateButtonVisibility;
if (!document.getElementById('pln-spinner-style'))
{
const st = document.createElement('style');
st.id = 'pln-spinner-style';
st.textContent = '@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}';
document.head.appendChild(st);
}
document.body.appendChild(floatingPanelElement);
}
// Actualizar estado actual
floatingPanelElement._currentStatus = status;
const processingPanelDimensions = window.processingPanelDimensions || { width: "480px", height: "220px" };
const resultsPanelDimensions = window.resultsPanelDimensions || { width: "820px", height: "720px" };
// Referencias a elementos existentes
const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title");
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
// Limpiar contenido
if(outputDiv) outputDiv.innerHTML = "";
// Actualizar visibilidad de botones
if (floatingPanelElement._updateButtonVisibility) {
floatingPanelElement._updateButtonVisibility();
}
// Configurar según el estado
if (status === "processing")
{
// Solo actualizar si no está maximizado o minimizado
if (!floatingPanelElement._isMaximized && !floatingPanelElement._isMinimized) {
floatingPanelElement.style.width = processingPanelDimensions.width;
floatingPanelElement.style.height = processingPanelDimensions.height;
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "50%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
if(outputDiv && !floatingPanelElement._isMinimized) {
outputDiv.style.height = floatingPanelElement._isMaximized ? "calc(100vh - 100px)" : "150px";
outputDiv.style.display = "block";
}
if(titleElement) titleElement.textContent = "Buscando...";
if (outputDiv && !floatingPanelElement._isMinimized)
{
outputDiv.innerHTML = "<div style='display:flex; align-items:center; justify-content:center; height:100%;'><span class='loader-spinner' style='width:32px; height:32px; border:4px solid #ccc; border-top:4px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span></div>";
}
}
else
{ // status === "results"
// Solo actualizar si no está maximizado o minimizado
if (!floatingPanelElement._isMaximized && !floatingPanelElement._isMinimized) {
floatingPanelElement.style.width = resultsPanelDimensions.width;
floatingPanelElement.style.height = resultsPanelDimensions.height;
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "60%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
if(outputDiv && !floatingPanelElement._isMinimized) {
outputDiv.style.height = floatingPanelElement._isMaximized ? "calc(100vh - 100px)" : "660px";
outputDiv.style.display = "block";
}
if(titleElement) titleElement.textContent = "NrmliZer: Resultados";
// --- BOTÓN MOSTRAR/OCULTAR NORMALIZADOS ---
let showHidden = false;
let toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (!toggleBtn) {
toggleBtn = document.createElement('button');
toggleBtn.id = 'pln-toggle-hidden-btn';
toggleBtn.textContent = 'Mostrar normalizados';
toggleBtn.style.marginLeft = '12px';
toggleBtn.style.padding = '4px 10px';
toggleBtn.style.fontSize = '12px';
toggleBtn.style.border = '1px solid #bbb';
toggleBtn.style.borderRadius = '5px';
toggleBtn.style.background = '#f4f4f4';
toggleBtn.style.cursor = 'pointer';
toggleBtn.addEventListener('click', () => {
showHidden = !showHidden;
if (showHidden) {
// Mostrar lo oculto
const st = document.getElementById('pln-hide-style'); if (st) st.remove();
document.querySelectorAll('tr.pln-hidden-normalized')
.forEach(tr => tr.classList.remove('pln-hidden-normalized'));
toggleBtn.textContent = 'Ocultar normalizados';
} else {
// Volver a ocultar normalizados
if (!document.getElementById('pln-hide-style')) {
const st = document.createElement('style');
st.id = 'pln-hide-style';
st.textContent = `tr.pln-hidden-normalized{display:none !important;}`;
document.head.appendChild(st);
}
document.querySelectorAll('tr').forEach(tr => {
// Reaplicar la lógica de ocultar si corresponde
if (tr.dataset && tr.dataset.placeId) {
// Si la fila ya estaba normalizada, volver a ocultarla
// (esto depende de lógica de marcado, aquí solo se vuelve a aplicar la clase si no tiene cambios)
// Si quieres forzar el ocultamiento, puedes volver a llamar a processAll() si la tienes global
if (typeof window.__plnHideNormalizedRows === 'function') {
window.__plnHideNormalizedRows();
}
}
});
toggleBtn.textContent = 'Mostrar normalizados';
}
});
// Insertar el botón en la barra de título del panel
const tb = document.getElementById('wme-pln-titlebar');
if (tb) tb.appendChild(toggleBtn);
}
}
floatingPanelElement.style.display = 'flex';
floatingPanelElement.style.flexDirection = 'column';
}// Fin de createFloatingPanel
//Permite renderizar los lugares en el panel flotante
function renderPlacesInFloatingPanel(places)
{
// Limpiar la lista global de duplicados antes de llenarla de nuevo
window.placesForDuplicateCheckGlobal = window.placesForDuplicateCheckGlobal || [];
window.placesForDuplicateCheckGlobal.length = 0;
createFloatingPanel("processing"); // Mostrar panel en modo "procesando"
const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10); //Obtiene el número total de lugares a procesar
const lockRankEmojis = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣"]; // Definir los emojis de nivel de bloqueo
// Permite obtener el nombre de la categoría de un lugar, ya sea del modelo antiguo o del SDK
function getPlaceCategoryName(venueFromOldModel, venueSDKObject)
{ // Acepta ambos tipos de venue
let categoryId = null;
let categoryName = null;
// Intento 1: Usar el venueSDKObject si está disponible y tiene la info
if (venueSDKObject)
{
if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id)
{// Si venueSDKObject tiene mainCategory con ID
categoryId = venueSDKObject.mainCategory.id; // source = "SDK (mainCategory.id)";
//Limpiar comillas aquí
if (typeof categoryId === 'string') categoryId = categoryId.replace(/'/g, '');
if (venueSDKObject.mainCategory.name) // Si mainCategory tiene nombre
categoryName = venueSDKObject.mainCategory.name;// source = "SDK (mainCategory.name)";
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
else if (Array.isArray(venueSDKObject.categories) && venueSDKObject.categories.length > 0)
{// Si venueSDKObject tiene un array de categorías y al menos una categoría
const firstCategorySDK = venueSDKObject.categories[0]; // source = "SDK (categories[0])";
if (typeof firstCategorySDK === 'object' && firstCategorySDK.id)
{// Si la primera categoría es un objeto con ID
categoryId = firstCategorySDK.id;
// Limpiar comillas aquí
if (typeof categoryId === 'string') categoryId = categoryId.replace(/'/g, '');
if (firstCategorySDK.name) // Si la primera categoría tiene nombre
categoryName = firstCategorySDK.name;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
else if (typeof firstCategorySDK === 'string') // Si la primera categoría es una cadena (nombre de categoría)
{
categoryName = firstCategorySDK;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
}
else if (venueSDKObject.primaryCategoryID)
{
categoryId = venueSDKObject.primaryCategoryID;
if (typeof categoryName === 'string') categoryName = categoryName.replace(/'/g, '');
}
}
if (categoryName)
{// Si se obtuvo el nombre de categoría del SDK
return categoryName;
}
// Intento 2: Usar W.model si no se obtuvo del SDK
if (!categoryId && venueFromOldModel && venueFromOldModel.attributes && Array.isArray(venueFromOldModel.attributes.categories) && venueFromOldModel.attributes.categories.length > 0)
categoryId = venueFromOldModel.attributes.categories[0];
if (!categoryId)// Si no se pudo obtener el ID de categoría de ninguna fuente
return "Sin categoría";
let categoryObjWModel = null; // Intentar obtener el objeto de categoría del modelo Waze
if (typeof W !== 'undefined' && W.model)
{// Si Waze Map Editor está disponible
if (W.model.venueCategories && typeof W.model.venueCategories.getObjectById === "function") // Si venueCategories está disponible en W.model
categoryObjWModel = W.model.venueCategories.getObjectById(categoryId);
if (!categoryObjWModel && W.model.categories && typeof W.model.categories.getObjectById === "function") // Si no se encontró en venueCategories, intentar en categories
categoryObjWModel = W.model.categories.getObjectById(categoryId);
}
if (categoryObjWModel && categoryObjWModel.attributes && categoryObjWModel.attributes.name)
{// Si se encontró el objeto de categoría en W.model
let nameToReturn = categoryObjWModel.attributes.name;
// Limpiar comillas aquí
if (typeof nameToReturn === 'string') nameToReturn = nameToReturn.replace(/'/g, '');
return nameToReturn;
}
if (typeof categoryId === 'number' || (typeof categoryId === 'string' && categoryId.trim() !== ''))
{// Si no se pudo obtener el nombre de categoría de ninguna fuente, devolver el ID
return `${categoryId}`; // Devuelve el ID si no se encuentra el nombre.
}
return "Sin categoría";
}//getPlaceCategoryName
//Permite obtener el tipo de lugar (área o punto) y su icono
function getPlaceTypeInfo(venueSDKObject) // <--- AHORA RECIBE venueSDKObject
{
let isArea = false;
let icon = "⊙"; // Icono por defecto para punto
let title = "Punto"; // Título por defecto para punto
if (venueSDKObject && venueSDKObject.geometry && venueSDKObject.geometry.type)
{
const geometryType = venueSDKObject.geometry.type;
if (geometryType === 'Polygon' || geometryType === 'MultiPolygon')
{
isArea = true;
icon = "⭔"; // Icono para área
title = "Área"; // Título para área
}
// Para otros tipos como 'Point', 'LineString', etc., se mantienen los valores por defecto (Punto).
}
return { isArea, icon, title };
}// getPlaceTypeInfo
//Permite procesar un lugar y generar un objeto con sus detalles
function shouldForceSuggestionForReview(word)
{
if (typeof word !== 'string') // Si la palabra no es una cadena, no forzar sugerencia por esta regla
return false;
const lowerWord = word.toLowerCase(); // Convertir la palabra a minúsculas para evitar problemas de mayúsculas/minúsculas
const hasTilde = /[áéíóúÁÉÍÓÚ]/.test(word); // Verificar si la palabra tiene alguna tilde (incluyendo mayúsculas acentuadas)
if (!hasTilde) // Si no tiene tilde, no forzar sugerencia por esta regla
return false; // Si no hay tilde, no forzar sugerencia por esta regla
const problematicSubstrings = ['c', 's', 'x', 'cc', 'sc', 'cs', 'g', 'j', 'z','ñ']; // Lista de patrones de letras/combinaciones que, junto con una tilde, fuerzan la sugerencia (insensible a mayúsculas debido a lowerWord)
for (const sub of problematicSubstrings)
{// Verificar si la palabra contiene alguna de las letras/combinaciones problemáticas
if (lowerWord.includes(sub))
return true; // Tiene tilde y una de las letras/combinaciones problemáticas
}
return false; // Tiene tilde, pero no una de las letras/combinaciones problemáticas
}//shouldForceSuggestionForReview
// Procesa un lugar y genera un objeto con sus detalles
async function getPlaceCityInfo(venueFromOldModel, venueSDKObject)
{
let hasExplicitCity = false; // Indica si hay una ciudad explícita definida
let explicitCityName = null; // Nombre de la ciudad explícita, si se encuentra
let hasStreetInfo = false; // Indica si hay información de calle disponible
let cityAssociatedWithStreet = null; // Nombre de la ciudad asociada a la calle, si se encuentra
// 1. Check for EXPLICIT city SDK
if (venueSDKObject && venueSDKObject.address)
{
plnLog('sdk', "[DEBUG] venueSDKObject.address:", venueSDKObject.address);
if (venueSDKObject.address.city && typeof venueSDKObject.address.city.name === 'string' && venueSDKObject.address.city.name.trim() !== '') {
// Si hay una ciudad explícita en el SDK
explicitCityName = venueSDKObject.address.city.name.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "SDK (address.city.name)";
plnLog('sdk', "[DEBUG] Ciudad explícita encontrada en SDK (address.city.name):", explicitCityName);
} else if (typeof venueSDKObject.address.cityName === 'string' && venueSDKObject.address.cityName.trim() !== '') {
// Si hay una ciudad explícita en el SDK (cityName)
explicitCityName = venueSDKObject.address.cityName.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "SDK (address.cityName)";
plnLog('sdk', "[DEBUG] Ciudad explícita encontrada en SDK (address.cityName):", explicitCityName);
}
else
{
plnLog('sdk', "[DEBUG] No se encontró ciudad explícita en SDK.");
}
}//
if (!hasExplicitCity && venueFromOldModel && venueFromOldModel.attributes)
{
plnLog('sdk', "[DEBUG] venueFromOldModel.attributes:", venueFromOldModel.attributes);
const cityID = venueFromOldModel.attributes.cityID;
plnLog('city', "[DEBUG] cityID del modelo antiguo:", cityID);
if (cityID && typeof W !== 'undefined' && W.model && W.model.cities && W.model.cities.getObjectById)
{
plnLog('city', "[DEBUG] Intentando obtener el objeto de ciudad con cityID:", cityID);
const cityObject = W.model.cities.getObjectById(cityID); // Obtener el objeto de ciudad del modelo Waze
plnLog('city', "[DEBUG] cityObject obtenido:", cityObject);
if (cityObject && cityObject.attributes && typeof cityObject.attributes.name === 'string' && cityObject.attributes.name.trim() !== '')
{
// Si el objeto de ciudad tiene un nombre válido
explicitCityName = cityObject.attributes.name.trim(); // Nombre de la ciudad explícita
hasExplicitCity = true; // source = "W.model.cities (cityID)";
plnLog('city', "[DEBUG] Ciudad explícita encontrada en modelo antiguo (cityID):", explicitCityName);
}
else
{
plnLog('city', "[DEBUG] cityObject no tiene un nombre válido.");
}
}
else
{
plnLog('city', "[DEBUG] cityID no válido o W.model.cities.getObjectById no disponible.");
}
}
// 2. Check for STREET information (and any city derived from it) // SDK street check
if (venueSDKObject && venueSDKObject.address)
if ((venueSDKObject.address.street && typeof venueSDKObject.address.street.name === 'string' && venueSDKObject.address.street.name.trim() !== '') ||
(typeof venueSDKObject.address.streetName === 'string' && venueSDKObject.address.streetName.trim() !== ''))
hasStreetInfo = true; // source = "SDK (address.street.name or streetName)";
if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.streetID)
{// Old Model street check (if not found via SDK or to supplement)
hasStreetInfo = true; // Street ID exists in old model
const streetID = venueFromOldModel.attributes.streetID; // Obtener el streetID del modelo antiguo
if (typeof W !== 'undefined' && W.model && W.model.streets && W.model.streets.getObjectById)
{// Si hay un streetID en el modelo antiguo
const streetObject = W.model.streets.getObjectById(streetID); // Obtener el objeto de calle del modelo Waze
if (streetObject && streetObject.attributes && streetObject.attributes.cityID)
{// Si el objeto de calle tiene un cityID asociado
const cityIDFromStreet = streetObject.attributes.cityID;// Obtener el cityID de la calle
if (W.model.cities && W.model.cities.getObjectById)
{// Si W.model.cities está disponible y tiene el método getObjectById
const cityObjectFromStreet = W.model.cities.getObjectById(cityIDFromStreet);// Obtener el objeto de ciudad asociado a la calle
// Si el objeto de ciudad tiene un nombre válido
if (cityObjectFromStreet && cityObjectFromStreet.attributes && typeof cityObjectFromStreet.attributes.name === 'string' && cityObjectFromStreet.attributes.name.trim() !== '')
cityAssociatedWithStreet = cityObjectFromStreet.attributes.name.trim(); // Nombre de la ciudad asociada a la calle
}
}
}
}
// --- 3. Determine icon, title, and returned hasCity based on user's specified logic ---
let icon;
let title;
const returnedHasCityBoolean = hasExplicitCity; // To be returned, indicates if an *explicit* city is set.
const hasAnyAddressInfo = hasExplicitCity || hasStreetInfo; // Determina si hay alguna información de dirección (ciudad explícita o calle).
if (hasAnyAddressInfo)
{// Si hay información de dirección (ciudad explícita o calle)
if (hasExplicitCity)
{
// Tiene ciudad explícita
icon = "🏙️";
title = `Ciudad: ${explicitCityName}`;
}
else if (cityAssociatedWithStreet)
{
// No tiene ciudad explícita, pero la calle sí está asociada a ciudad
icon = "🏙️";
title = `Ciudad (por calle): ${cityAssociatedWithStreet}`;
}
else
{
// No hay ciudad explícita ni ciudad por calle
icon = "🚫";
title = "Sin ciudad asignada";
}
return {
icon: icon || "❓",
title: title || "Info no disponible",
hasCity: (hasExplicitCity || !!cityAssociatedWithStreet) // Ahora true si tiene ciudad por calle
};
}
else
{ // No tiene ni ciudad explícita ni información de calle
icon = "🚫";
title = "El campo dirección posee inconsistencias"; // Título para "no tiene ciudad ni calle"
}
return {
icon: icon || "❓", // Usar '?' si icon es undefined/null/empty
title: title || "Info no disponible", // Usar "Info no disponible" si title es undefined/null/empty
hasCity: returnedHasCityBoolean || false // Asegurarse de que sea un booleano
};
}//getPlaceCityInfo
//Renderizar barra de progreso en el TAB PRINCIPAL justo después del slice
const tabOutput = document.querySelector("#wme-normalization-tab-output");
if (tabOutput)
{// Si el tab de salida ya existe, limpiar su contenido
// 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";
// Crear el contenedor de la barra de progreso
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);
// Crear texto de progreso
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"); // Obtener la barra de progreso del tab
const progressBarTextTab = document.getElementById("progressBarTextTab"); // Obtener el texto de progreso del tab
if (progressBarInnerTab && progressBarTextTab)
{// Si ambos elementos existen, reiniciar su estado
progressBarInnerTab.style.width = "0%";
progressBarTextTab.textContent = `Progreso: 0% (0/${places.length})`; // Reiniciar el texto de progreso
}
// --- PANEL FLOTANTE: limpiar y preparar salida ---
const output = document.querySelector("#wme-place-inspector-output");//
if (!output)
{// Si el panel flotante no está disponible, mostrar un mensaje de error
plnLog('error',"[WME_PLN][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:13px; color:#555;'>Inicializando escaneo...</div></div></div>";
// Asegurar que el panel flotante tenga un alto mínimo
const processingStepLabel = document.getElementById("processingStep");
// Animación de puntos suspensivos
const dotsSpan = output.querySelector(".dots");
if (dotsSpan)
{// Si el span de puntos existe, iniciar la animación de puntos
const dotStates = ["", ".", "..", "..."];
let dotIndex = 0;
window.processingDotsInterval = setInterval(() => {dotIndex = (dotIndex + 1) % dotStates.length;
dotsSpan.textContent = dotStates[dotIndex];}, 500);
}
output.style.height = "calc(55vh - 40px)";
if (!places.length)
{// Si no hay places, mostrar mensaje y salir
output.appendChild(document.createTextNode("No hay places visibles para analizar."));
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)// Si ya existe un overlay de escaneo, removerlo
existingOverlay.remove();
return;
}
// Procesamiento incremental para evitar congelamiento
let inconsistents = []; // Array para almacenar inconsistencias encontradas
let index = 0; // Índice para iterar sobre los lugares
const scanBtn = document.getElementById("pln-start-scan-btn"); if (scanBtn)
{// Si el botón de escaneo existe, remover el ícono de ✔ previo si está presente
const existingCheck = scanBtn.querySelector("span");
if (existingCheck) // Si hay un span dentro del botón, removerlo
existingCheck.remove();
}
// --- Sugerencias por palabra global para toda la ejecución ---
let sugerenciasPorPalabra = {};
// Helper local: similitud usando índice por primera letra
function findSimilarWords(cleanedLower, dictIndex, threshold)
{
const firstChar = cleanedLower.charAt(0);
const bucket = (dictIndex && dictIndex[firstChar]) ? dictIndex[firstChar] : [];
const results = [];
for (const word of bucket)
{
const sim = (PLNCore?.utils?.calculateSimilarity)
? PLNCore.utils.calculateSimilarity(cleanedLower, word.toLowerCase())
: 0;
if (sim >= threshold) results.push({ word, similarity: sim });
}
results.sort((a,b)=>b.similarity-a.similarity);
return results.slice(0, 5);
}
// 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) : []);
// Función asíncrona para procesar el siguiente lugar
async function processNextPlace()
{
const maxInconsistentsToFind = parseInt(document.getElementById("maxPlacesInput")?.value || "30", 10);
if (inconsistents.length >= maxInconsistentsToFind)
{
finalizeRender(inconsistents, places.slice(0, index), sugerenciasPorPalabra);
return;
}
if (index >= places.length)
{// Si se han procesado todos los lugares, finalizar
finalizeRender(inconsistents, places, sugerenciasPorPalabra);
return;
}
const venueFromOldModel = places[index];
const currentVenueId = venueFromOldModel.getID();
// Salto temprano si el lugar es inválido o no tiene nombre
if (!venueFromOldModel || !venueFromOldModel.attributes || typeof (venueFromOldModel.attributes.name?.value || venueFromOldModel.attributes.name || '').trim() !== 'string' || (venueFromOldModel.attributes.name?.value || venueFromOldModel.attributes.name || '').trim() === '')
{
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
return;
}
// --- Inicialización de variables para este lugar ---
let shouldSkipThisPlace = false;
let skipReasonLog = "";
let venueSDK = null;
try
{
if (wmeSDK?.DataModel?.Venues?.getById)
{
venueSDK = await wmeSDK.DataModel.Venues.getById({ venueId: currentVenueId });
}
}
catch (sdkError)
{
plnLog('error',`[WME_PLN] Error al obtener venueSDK para ID ${currentVenueId}:`, sdkError);
}
const originalNameRaw = (venueSDK?.name || venueFromOldModel.attributes.name?.value || venueFromOldModel.attributes.name || '').trim();
const nameForProcessing = originalNameRaw; // PLNCore.normalize maneja limpieza
const normalizedName = PLNCore.normalize(nameForProcessing);
const suggestedName = normalizedName;
const originalWords = nameForProcessing.split(/\s+/).filter(word => word.length > 0);
let sugerenciasLugar = {};
const similarityThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "81") / 100;
originalWords.forEach((originalWord) =>
{
if (!originalWord) return;
const lowerOriginalWord = originalWord.toLowerCase();
const cleanedLowerNoDiacritics = (window.PLNCore?.utils?.removeDiacritics || ((s)=>s))(lowerOriginalWord);
let tildeCorrectionSuggested = false;
if (window.dictionaryWords?.size > 0)
{
const firstChar = lowerOriginalWord.charAt(0);
const candidatesForTildeCheck = window.dictionaryIndex[firstChar] || [];
for (const dictWord of candidatesForTildeCheck)
{
const lowerDictWord = dictWord.toLowerCase();
if (removeDiacritics(lowerDictWord) === cleanedLowerNoDiacritics && lowerDictWord !== lowerOriginalWord && !/[áéíóúÁÉÍÓÚüÜñÑ]/.test(lowerOriginalWord) && /[áéíóúÁÉÍÓÚüÜñÑ]/.test(lowerDictWord))
{
let suggestedTildeWord = PLNCore.normalize(dictWord); if (!sugerenciasLugar[originalWord]) sugerenciasLugar[originalWord] = [];
sugerenciasLugar[originalWord].push({ word: suggestedTildeWord, similarity: 0.999, fuente: 'dictionary_tilde' });
tildeCorrectionSuggested = true;
break;
}
}
}
if (!tildeCorrectionSuggested && window.dictionaryWords)
{
const similarDictionary = findSimilarWords(cleanedLowerNoDiacritics, window.dictionaryIndex, similarityThreshold);
if (similarDictionary.length > 0)
{
const finalSuggestions = similarDictionary.filter(d => d.word.toLowerCase() !== lowerOriginalWord);
if (finalSuggestions.length > 0)
{
if (!sugerenciasLugar[originalWord]) sugerenciasLugar[originalWord] = [];
finalSuggestions.forEach(dictSuggestion =>
{
if (!sugerenciasLugar[originalWord].some(s => s.word === PLNCore.normalize(dictSuggestion.word)))
{
sugerenciasLugar[originalWord].push({ ...dictSuggestion, fuente: 'dictionary' });
}
});
}
}
}
});
const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
// ===================================================================
// INICIO: LÓGICA DE DECISIÓN UNIFICADA Y CORREGIDA
// ===================================================================
const cleanedOriginalName = String(nameForProcessing || '').replace(/\s+/g, ' ').trim();
const cleanedSuggestedName = String(suggestedName || '').replace(/\s+/g, ' ').trim();
// Condición única y estricta: un lugar es una inconsistencia si su nombre cambia.
const isTrulyInconsistent = cleanedOriginalName !== cleanedSuggestedName;
if (!isTrulyInconsistent)
{
// Si los nombres son idénticos, este lugar no cuenta y pasamos al siguiente.
shouldSkipThisPlace = true;
}
else
{
// Aquí podrías añadir las otras condiciones de omisión si las necesitas.
// Por ahora, si es inconsistente, no lo saltamos.
shouldSkipThisPlace = false;
}
const isNameEffectivelyNormalized = (cleanedOriginalName === cleanedSuggestedName);
const avoidMyEdits = document.getElementById("chk-avoid-my-edits")?.checked ?? false;
const typeInfo = getPlaceTypeInfo(venueSDK);
const areaMeters = PLNCore.utils.calculateArea(venueSDK);
let wasEditedByMe = false;
if (currentGlobalUserInfo && currentGlobalUserInfo.id)
{
let lastEditorId = venueSDK?.modificationData?.updatedBy ?? venueFromOldModel.attributes.updatedBy;
if (lastEditorId)
{
let lastEditorName = W.model.users.getObjectById(lastEditorId)?.userName || "";
wasEditedByMe = (String(lastEditorId) === String(currentGlobalUserInfo.id)) || (lastEditorName && lastEditorName === currentGlobalUserInfo.name);
}
}
// Aplicar reglas de omisión en orden de prioridad
if (isNameEffectivelyNormalized && !tieneSugerencias)
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP NORMALIZED]`;
}
else if (excludedPlaces.has(currentVenueId))
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP EXCLUDED PLACE]`;
}
else if (avoidMyEdits && wasEditedByMe)
{
const dateFilterValue = document.getElementById("dateFilterSelect")?.value || "all";
const placeEditDate = (venueSDK?.modificationData?.updatedOn) ? new Date(venueSDK.modificationData.updatedOn) : null;
if (placeEditDate && typeof window.isDateWithinRange === 'function' && window.isDateWithinRange(placeEditDate, dateFilterValue))
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP MY OWN EDIT - In Range: ${dateFilterValue}]`;
}
}
else if (typeInfo.isArea && areaMeters === null)
{
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP AREA_CALC_FAILED]`;
}
// ===================================================================
// FIN: LÓGICA DE DECISIÓN
// ===================================================================
if (shouldSkipThisPlace)
{
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
return;
}
// --- Si no se omite, se recopila la información para mostrar ---
const processingStepLabel = document.getElementById("processingStep");
if (processingStepLabel)
{
processingStepLabel.textContent = "Registrando inconsistencias...";
}
const { lat: placeLat, lon: placeLon } = (window.getPlaceCoordinates ? window.getPlaceCoordinates(venueFromOldModel, venueSDK) : {lat:null, lon:null});
const shouldRecommendCategories = document.getElementById("chk-recommend-categories")?.checked ?? true;
let currentCategoryKey = getPlaceCategoryName(venueFromOldModel, venueSDK);
const categoryDetails = (window.getCategoryDetails ? window.getCategoryDetails(currentCategoryKey) : {});
let dynamicSuggestions = shouldRecommendCategories && typeof window.findCategoryForPlace === 'function' ? window.findCategoryForPlace(originalNameRaw) : [];
let lastEditorId = venueSDK?.modificationData?.updatedBy ?? venueFromOldModel.attributes.updatedBy;
let resolvedEditorName = W.model.users.getObjectById(lastEditorId)?.userName || `${lastEditorId}` || "Desconocido";
const cityInfo = await getPlaceCityInfo(venueFromOldModel, venueSDK);
let lockRank = venueSDK?.lockRank ?? venueFromOldModel.attributes.lockRank ?? 0;
const lockRankEmojis = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣"];
let lockRankEmoji = (lockRank >= 0 && lockRank <= 5) ? lockRankEmojis[lockRank + 1] : lockRankEmojis[0];
const hasOverlappingHours = checkForOverlappingHours(venueSDK); // Llama a la nueva función.
inconsistents.push({
lockRankEmoji,
id: currentVenueId,
original: originalNameRaw,
normalized: suggestedName,
editor: resolvedEditorName,
cityIcon: cityInfo.icon,
cityTitle: cityInfo.title,
hasCity: cityInfo.hasCity,
venueSDKForRender: venueSDK,
currentCategoryName: categoryDetails.description,
currentCategoryIcon: categoryDetails.icon,
currentCategoryTitle: categoryDetails.description,
currentCategoryKey: currentCategoryKey,
dynamicCategorySuggestions: dynamicSuggestions,
lat: placeLat,
lon: placeLon,
typeInfo: typeInfo,
areaMeters: areaMeters,
hasOverlappingHours: hasOverlappingHours
});
sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
}
plnLog('normalize', "[WME_PLN] Iniciando primer processNextPlace...");
try
{
setTimeout(() => { processNextPlace(); }, 10);
}
catch (error)
{
plnLog('error',"[WME_PLN][ERROR_CRITICAL] Fallo al iniciar processNextPlace:", error, error.stack);
enableScanControls();
const outputFallback = document.querySelector("#wme-place-inspector-output");
if (outputFallback)
{
outputFallback.innerHTML = `<div style='color:red; padding:10px;'><b>Error Crítico:</b> El script de normalización encontró un problema grave y no pudo continuar. Revise la consola para más detalles (F12).<br>Detalles: ${error.message}</div>`;
}
const scanBtn = document.querySelector("button[type='button']"); // Asumiendo que es el botón de Start Scan
if (scanBtn)
{
scanBtn.disabled = false;
scanBtn.textContent = "Start Scan... (Error Previo)";
}
if (window.processingDotsInterval)
{
clearInterval(window.processingDotsInterval);
}
}// processNextPlace
// Función para re-aplicar la lógica de palabras excluidas al texto normalizado
function reapplyExcludedWordsLogic(text, excludedWordsSet)
{
if (typeof text !== 'string' || !excludedWordsSet || excludedWordsSet.size === 0)
{
return text;
}
const wordsInText = text.split(/\s+/);
const processedWordsArray = wordsInText.map(word =>
{
if (word === "") return "";
const wordWithoutDiacriticsLower = removeDiacritics(word.toLowerCase());
// Encontrar la palabra excluida que coincida (insensible a may/min y diacríticos)
const matchingExcludedWord = Array.from(excludedWordsSet).find(
w_excluded => removeDiacritics(w_excluded.toLowerCase()) === wordWithoutDiacriticsLower);
if (matchingExcludedWord)
{
// Si coincide, DEVOLVER LA FORMA EXACTA DE LA LISTA DE EXCLUIDAS
return matchingExcludedWord;
}
// Si no, devolver la palabra como estaba (ya normalizada por pasos previos)
return word;
});
return processedWordsArray.join(' ');
}// reapplyExcludedWordsLogic
function excludePlace(row, placeId, placeName)
{
const confirmModal = document.createElement("div");
// ... (Estilos del modal, no cambian)
confirmModal.style.position = "fixed";
confirmModal.style.top = "50%";
confirmModal.style.left = "50%";
confirmModal.style.transform = "translate(-50%, -50%)";
confirmModal.style.background = "#fff";
confirmModal.style.border = "1px solid #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000";
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
confirmModal.innerHTML = `
<div style="font-size: 38px; margin-bottom: 10px;">🚫</div>
<div style="font-size: 20px; margin-bottom: 8px;"><b>¿Excluir "${placeName}"?</b></div>
<div style="font-size: 15px; color: #555; margin-bottom: 18px;">Este lugar no volverá a aparecer en futuras búsquedas del normalizador.</div>
`;
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
// ... (Estilos del botón cancelar)
cancelBtn.style.padding = "7px 18px";
cancelBtn.style.background = "#eee";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => confirmModal.remove());
const confirmExcludeBtn = document.createElement("button");
confirmExcludeBtn.textContent = "Excluir";
// ... (Estilos del botón confirmar)
confirmExcludeBtn.style.padding = "7px 18px";
confirmExcludeBtn.style.background = "#d9534f";
confirmExcludeBtn.style.color = "#fff";
confirmExcludeBtn.style.border = "none";
confirmExcludeBtn.style.borderRadius = "4px";
confirmExcludeBtn.style.cursor = "pointer";
confirmExcludeBtn.style.fontWeight = "bold";
confirmExcludeBtn.addEventListener("click", () => {
excludedPlaces.set(placeId, placeName);
saveExcludedPlacesToLocalStorage();
showTemporaryMessage("Lugar excluido de futuras búsquedas.", 3000, 'success');
if (row) {
// Reutilizamos la lógica del botón "Aplicar" para unificar el feedback visual
const actionButtons = row.querySelectorAll('td:last-child button');
actionButtons.forEach(btn => {
btn.disabled = true;
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.5';
});
const mainButton = row.querySelector('button[title="Aplicado"], button[title="Aplicar sugerencia"]');
if(mainButton){
mainButton.innerHTML = '🚫';
mainButton.style.backgroundColor = '#dc3545';
mainButton.style.color = 'white';
mainButton.style.opacity = '1';
mainButton.title = 'Excluido';
}
updateInconsistenciesCount(-1);
}
confirmModal.remove();
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmExcludeBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal);
}
async function handleAiRequestForRow(brainButton, row, placeId, originalName)
{
const suggestionTextarea = row.querySelector('.replacement-input');
const aiContainer = document.getElementById(`ai-suggestion-container-${placeId}`);
if (!aiContainer || !suggestionTextarea) return;
row.dataset.aiProcessing = 'true';
try {
brainButton.innerHTML = '🧠...';
brainButton.disabled = true;
aiContainer.innerHTML = '';
const loadingContainer = document.createElement('div');
loadingContainer.style.display = 'flex'; loadingContainer.style.alignItems = 'center'; loadingContainer.style.gap = '8px'; loadingContainer.style.color = '#555'; loadingContainer.style.fontSize = '12px';
const spinner = document.createElement('div');
spinner.style.width = '16px'; spinner.style.height = '16px'; spinner.style.border = '2px solid #ccc'; spinner.style.borderTop = '2px solid #00796b'; spinner.style.borderRadius = '50%'; spinner.style.animation = 'pln-spin 0.8s linear infinite';
const loadingText = document.createElement('span');
loadingText.textContent = 'Procesando...';
loadingContainer.appendChild(spinner); loadingContainer.appendChild(loadingText); aiContainer.appendChild(loadingContainer);
let nameForAI = originalName;
if (typeof aplicarReemplazosDefinidos === 'function' && typeof replacementWords === 'object')
{
nameForAI = aplicarReemplazosDefinidos(nameForAI, replacementWords);
}
if (typeof applySwapRules === 'function')
{
nameForAI = applySwapRules(nameForAI);
}
const categoryKeysForAI = window.dynamicCategoryRules.map(rule => rule.categoryKey).filter(Boolean);
const aiSuggestions = await PLNCore.ai.getSuggestions(nameForAI, categoryKeysForAI);
aiContainer.innerHTML = '';
if (!aiSuggestions || aiSuggestions.error) {
const errorText = aiSuggestions ? (aiSuggestions.error || aiSuggestions.details) : "Error desconocido.";
aiContainer.innerHTML = `<div style="color:red; font-size:12px; font-weight:bold;">❌ ${errorText}</div>`;
brainButton.disabled = false; brainButton.innerHTML = '🧠';
return;
}
let finalSuggestion = aiSuggestions.normalizedName;
if (typeof plnApplyExclusions === 'function') {
finalSuggestion = plnApplyExclusions(finalSuggestion);
}
const currentSuggestedName = suggestionTextarea.value;
if (finalSuggestion.trim().toLowerCase() === currentSuggestedName.trim().toLowerCase() && !aiSuggestions.suggestedCategory) {
brainButton.innerHTML = '✔️';
brainButton.disabled = true;
aiContainer.innerHTML = `<div style="font-weight:bold; color:green; margin: 8px;">✔ El nombre ya es correcto.</div>`;
} else {
brainButton.innerHTML = '🧠';
brainButton.disabled = true;
const suggestionBox = document.createElement('div');
suggestionBox.style.padding = '8px'; suggestionBox.style.borderRadius = '5px'; suggestionBox.style.border = `1px solid #00796b`; suggestionBox.style.background = '#f8f9fa';
const suggestionContent = document.createElement('div');
suggestionContent.style.fontWeight = 'bold'; suggestionContent.style.marginBottom = '8px'; suggestionContent.style.color = '#007bff';
suggestionContent.innerHTML = `<b>Nombre:</b> ${finalSuggestion}`;
suggestionBox.appendChild(suggestionContent);
const acceptButton = document.createElement('button');
acceptButton.innerHTML = '✔ Aceptar';
acceptButton.style.padding = '4px 8px'; acceptButton.style.fontSize = '11px'; acceptButton.style.background = '#28a745'; acceptButton.style.color = 'white'; acceptButton.style.border = 'none'; acceptButton.style.borderRadius = '4px'; acceptButton.style.cursor = 'pointer';
acceptButton.addEventListener('click', () => {
const venueObj = W.model.venues.getObjectById(placeId);
if (!venueObj) return;
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
const changes = { name: finalSuggestion };
const action = new UpdateObject(venueObj, changes);
W.model.actionManager.add(action);
recordNormalizationEvent();
// 1. Marcar la fila como procesada (se atenuará y se ocultará si está activado el filtro)
markRowAsProcessed(row, 'applied');
// 2. Actualizar el contador de inconsistencias
updateInconsistenciesCount(-1);
// 3. Simular clic en el botón principal para que se ponga verde (o copiar su lógica)
const mainApplyButton = row.querySelector('button[title="Aplicar sugerencia"]');
if (mainApplyButton)
{
mainApplyButton.disabled = true;
mainApplyButton.innerHTML = '✔️'; // Poner el check
mainApplyButton.style.backgroundColor = '#28a745'; // Color verde
mainApplyButton.style.color = 'white';
mainApplyButton.style.opacity = '1';
mainApplyButton.title = 'Aplicado';
}
}
catch (e)
{
plnToast("Error al aplicar cambios: " + e.message);
}
});
suggestionBox.appendChild(acceptButton);
aiContainer.appendChild(suggestionBox);
}
}
finally
{
delete row.dataset.aiProcessing;
}
}
//Función para finalizar renderizado una vez completado el análisis
function finalizeRender(inconsistents, placesArr, allSuggestions)
{
const resultsPanel = document.getElementById("wme-place-inspector-panel");
if (resultsPanel)
{
resultsPanel.dataset.totalScanned = placesArr.length;
}
// Filtrar inconsistencias reales (original !== normalized)
// en la función finalizeRender
const filteredInconsistents = inconsistents.filter(place => {
// Comparamos los strings directamente después de limpiar espacios.
// Esto asegura que "Hotel hersua boutique" y "Hotel Hersua Boutique" se consideren diferentes.
return (place.original || "").trim() !== (place.normalized || "").trim();
});
// Limpiar el mensaje de procesamiento y spinner al finalizar el análisis
//const typeInfo = venueSDK?.typeInfo || {};
enableScanControls();
// 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";
}
// Verificar si el botón de escaneo existe
const output = document.querySelector("#wme-place-inspector-output");
if (!output)
{
plnLog('error',"[WME_PLN]❌ No se pudo montar el panel flotante. Revisar estructura del DOM.");
plnToast("Hubo un problema al mostrar los resultados. Intenta recargar la página.");
return;
}
// Limpiar el mensaje de procesamiento y spinner
const undoRedoHandler = function()
{// Maneja el evento de deshacer/rehacer
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
reactivateAllActionButtons(); // No necesitamos setTimeout aquí si renderPlacesInFloatingPanel es síncrono.
});
}
else
{
plnLog('ui', "[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
};
// Objeto para almacenar referencias de listeners para desregistro
if (!window._wmePlnUndoRedoListeners)
{
window._wmePlnUndoRedoListeners = {};
}
// Desregistrar listeners previos si existen
if (window._wmePlnUndoRedoListeners.undo)
{
W.model.actionManager.events.unregister("afterundoaction", null, window._wmePlnUndoRedoListeners.undo);
}
if (window._wmePlnUndoRedoListeners.redo)
{
W.model.actionManager.events.unregister("afterredoaction", null, window._wmePlnUndoRedoListeners.redo);
}
// Registrar nuevos listeners
W.model.actionManager.events.register("afterundoaction", null, undoRedoHandler);
W.model.actionManager.events.register("afterredoaction", null, undoRedoHandler);
// Almacenar referencias para poder desregistrar en el futuro
window._wmePlnUndoRedoListeners.undo = undoRedoHandler;
window._wmePlnUndoRedoListeners.redo = undoRedoHandler;
// Esta llamada se hace ANTES de limpiar el output. El primer argumento es el estado, el segundo es el número de inconsistencias.
createFloatingPanel("results", filteredInconsistents.length);
// Si no hay inconsistencias reales, retornar antes del renderizado de la tabla
if (filteredInconsistents.length === 0) {
// Limpiar el mensaje de procesamiento y spinner
if (output)
{
output.innerHTML = `<div style='color:green; padding:10px;'>✔ Todos los lugares visibles están correctamente normalizados o excluidos.</div>`;
}
// Ocultar botón de alternar panel si existe
const toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (toggleBtn)
{
toggleBtn.style.display = 'none';
}
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay) existingOverlay.remove();
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})`;
}
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";
}
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 = document.createElement("span");
iconCheck.textContent = " ✔";
iconCheck.style.marginLeft = "8px";
iconCheck.style.color = "green";
scanBtn.appendChild(iconCheck);
}
return;
}
// Limitar a 30 resultados y mostrar advertencia si excede
const maxRenderLimit = 30;
const totalInconsistentsOriginal = filteredInconsistents.length; // Guardar el total original
let isLimited = false; // Declarar e inicializar isLimited
// Si hay más de 30 resultados, limitar a 30 y mostrar mensaje
let inconsistentsToRender = filteredInconsistents;
if (totalInconsistentsOriginal > maxRenderLimit)
{
inconsistentsToRender = filteredInconsistents.slice(0, maxRenderLimit);
isLimited = true; // Establecer isLimited a true si se aplica el límite
// Mostrar mensaje de advertencia si se aplica el límite
if (!sessionStorage.getItem("popupShown"))
{
const modalLimit = document.createElement("div"); // Renombrado a modalLimit para claridad
modalLimit.style.position = "fixed";
modalLimit.style.top = "50%";
modalLimit.style.left = "50%";
modalLimit.style.transform = "translate(-50%, -50%)";
modalLimit.style.background = "#fff";
modalLimit.style.border = "1px solid #ccc";
modalLimit.style.padding = "20px";
modalLimit.style.zIndex = "10007"; // <<<<<<< Z-INDEX AUMENTADO
modalLimit.style.width = "400px";
modalLimit.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)";
modalLimit.style.borderRadius = "8px";
modalLimit.style.fontFamily = "sans-serif";
// Fondo suave azul y mejor presentación
modalLimit.style.backgroundColor = "#f0f8ff";
modalLimit.style.border = "1px solid #aad";
modalLimit.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)";
// --- Insertar ícono visual de información arriba del mensaje ---
const iconInfo = document.createElement("div"); // Renombrado
iconInfo.innerHTML = "ℹ️";
iconInfo.style.fontSize = "24px";
iconInfo.style.marginBottom = "10px";
modalLimit.appendChild(iconInfo);
// Contenedor del mensaje
const message = document.createElement("p");
message.innerHTML = `Se encontraron <strong>${
totalInconsistentsOriginal}</strong> lugares con nombres no normalizados.<br><br>Solo se mostrarán los primeros <strong>${
maxRenderLimit}</strong>.<br><br>Una vez corrijas estos, presiona nuevamente <strong>'Start Scan...'</strong> para continuar con el análisis del resto.`;
message.style.marginBottom = "20px";
modalLimit.appendChild(message);
// Botón de aceptar
const acceptBtn = document.createElement("button");
acceptBtn.textContent = "Aceptar";
acceptBtn.style.padding = "6px 12px";
acceptBtn.style.cursor = "pointer";
acceptBtn.style.backgroundColor = "#007bff";
acceptBtn.style.color = "#fff";
acceptBtn.style.border = "none";
acceptBtn.style.borderRadius = "4px";
acceptBtn.addEventListener("click", () => {sessionStorage.setItem("popupShown", "true");
modalLimit.remove();
});
modalLimit.appendChild(acceptBtn);
document.body.appendChild(modalLimit); // Se añade al body, así que el z-index debería funcionar globalmente
}
}
// Llamar a la función para detectar y alertar nombres duplicados
detectAndAlertDuplicateNames(inconsistentsToRender);
// Crear un contenedor para los elementos fijos de la cabecera del panel de resultados
const fixedHeaderContainer = document.createElement("div");
fixedHeaderContainer.style.background = "#fff"; // Fondo para que no se vea el scroll debajo
fixedHeaderContainer.style.padding = "0 10px 8px 10px"; // Padding para espacio y que no esté pegado
fixedHeaderContainer.style.borderBottom = "1px solid #ccc"; // Un borde para separarlo de la tabla
fixedHeaderContainer.style.zIndex = "11"; // Asegurarse de que esté por encima de la tabla
// Añadir Estas Dos Líneas Clave Al FixedHeaderContainer
fixedHeaderContainer.style.position = "sticky"; // Hacer Que Este Contenedor Sea Sticky
fixedHeaderContainer.style.top = "0"; // Pegado A La Parte Superior Del Contenedor De Scroll
// =======================================================
// INICIO DEL BLOQUE CORREGIDO
// =======================================================
// 1. Contenedor Flex para el texto y el botón
const headerControlsContainer = document.createElement("div");
headerControlsContainer.style.display = "flex";
// LA SIGUIENTE LÍNEA ES EL CAMBIO PRINCIPAL:
headerControlsContainer.style.justifyContent = "flex-start"; // Alinea los elementos al inicio
headerControlsContainer.style.alignItems = "center";
headerControlsContainer.style.gap = "15px"; // Mantiene el espacio entre texto y botón
const resultsCounter = document.createElement("div");
resultsCounter.className = "results-counter-display";
resultsCounter.style.fontSize = "13px";
resultsCounter.style.color = "#555";
resultsCounter.style.textAlign = "left";
resultsCounter.dataset.currentCount = inconsistentsToRender.length;
resultsCounter.dataset.totalOriginal = totalInconsistentsOriginal;
resultsCounter.dataset.maxRenderLimit = maxRenderLimit;
if (totalInconsistentsOriginal > 0)
{
resultsCounter.innerHTML = `Inconsistencias encontradas: <b style="color: #ff0000;">${totalInconsistentsOriginal}</b> de <b>${placesArr.length}</b> analizados.`;
headerControlsContainer.appendChild(resultsCounter);
}
else
{
const outputDiv = document.querySelector("#wme-place-inspector-output");
if (outputDiv)
{
outputDiv.innerHTML = `<div style='color:green; padding:10px;'>✔ Todos los lugares visibles están correctamente normalizados o excluidos.</div>`;
}
}
// 2. Lógica del botón (sin cambios respecto a la corrección anterior)
// En la función donde creas el botón toggle
let toggleBtn = document.getElementById('pln-toggle-hidden-btn');
if (!toggleBtn)
{
toggleBtn = document.createElement("button");
toggleBtn.id = 'pln-toggle-hidden-btn';
toggleBtn.style.padding = "5px 10px";
toggleBtn.style.marginLeft = "15px";
toggleBtn.dataset.state = 'hidden'; // IMPORTANTE: Iniciar en 'hidden' para ocultar automáticamente
toggleBtn.addEventListener('click', function() {
const currentState = this.dataset.state;
if (currentState === 'shown')
{
// Cambiar a ocultos
this.textContent = "Mostrar procesados";
document.body.classList.add('pln-hide-normalized-rows');
this.dataset.state = 'hidden';
}
else
{
// Cambiar a visibles
this.textContent = "Ocultar procesados";
document.body.classList.remove('pln-hide-normalized-rows');
this.dataset.state = 'shown';
}
});
// Establecer texto inicial según el estado
toggleBtn.textContent = "Mostrar procesados";
}
// Sincronizar el texto del botón con su estado actual cada vez que se renderiza
if (toggleBtn.dataset.state === 'shown') {
toggleBtn.textContent = 'Ocultar Normalizados';
} else {
toggleBtn.textContent = 'Mostrar Normalizados';
}
if (totalInconsistentsOriginal > 0)
{
headerControlsContainer.appendChild(toggleBtn);
toggleBtn.style.display = 'inline-block'; // O simplemente ''
}
fixedHeaderContainer.appendChild(headerControlsContainer);
if (output)
{
output.style.display = 'flex';
output.style.flexDirection = 'column';
output.style.position = 'relative';
output.appendChild(fixedHeaderContainer);
}
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");
[
"N°",
"Perma",
"Tipo/Ciudad",
"LL",
"Editor",
"Nombre Actual",
"⚠️",
"Nombre Sugerido",
"Sugerencias<br>de reemplazo",
"Categoría",
"Categoría<br>Recomendada",
"Acción"
].forEach(header =>
{
const th = document.createElement("th");
th.innerHTML = header;
th.style.borderBottom = "1px solid #ccc";
th.style.padding = "4px";
th.style.textAlign = "center";
th.style.fontSize = "14px";
if (header === "N°")
{
th.style.width = "30px";
}
else if (header === "LL")
{
th.title = "Nivel de Bloqueo (Lock Level)";
th.style.width = "40px";
}
else if (header === "Perma" || header === "Tipo/Ciudad")
{
th.style.width = "65px";
}
else if (header === "⚠️")
{
th.title = "Alertas y advertencias";
th.style.width = "30px";
}
else if (header === "Categoría")
{
th.style.width = "130px";
}
else if (header === "Categoría<br>Recomendada" || header === "Sugerencias<br>de reemplazo")
{
th.style.width = "180px";
}
else if (header === "Editor")
{
th.style.width = "100px";
}
else if (header === "Acción")
{
th.style.width = "100px";
}
else if (header === "Nombre Actual" || header === "Nombre Sugerido")
{
th.style.width = "270px";
}
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
thead.style.position = "sticky";
thead.style.top = "0";
thead.style.background = "#f1f1f1";
thead.style.zIndex = "10";
headerRow.style.backgroundColor = "#003366";
headerRow.style.color = "#ffffff";
const tbody = document.createElement("tbody");
let activeAiPopover = null; // Variable global para controlar el popover activo
inconsistentsToRender.forEach(({ lockRankEmoji, id, original, normalized, editor, cityIcon, cityTitle, hasCity, currentCategoryName, currentCategoryIcon, currentCategoryTitle, currentCategoryKey, dynamicCategorySuggestions, venueSDKForRender, isDuplicate = false, duplicatePartners = [], typeInfo, areaMeters, hasOverlappingHours }, index) =>
{
const progressPercent = Math.floor(((index + 1) / inconsistentsToRender.length) * 100);
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");
row.querySelectorAll("td").forEach(td => td.style.verticalAlign = "top");
row.dataset.placeId = id;
const numberCell = document.createElement("td");
numberCell.textContent = index + 1;
numberCell.style.textAlign = "center";
numberCell.style.padding = "4px";
row.appendChild(numberCell);
const permalinkCell = document.createElement("td");
const link = document.createElement("a");
link.href = "#";
link.addEventListener("click", (e) =>
{
e.preventDefault();
const venueObj = W.model.venues.getObjectById(id);
const venueSDKForUse = venueSDKForRender;
let targetLat = null;
let targetLon = null;
if (venueSDKForUse && venueSDKForUse.geometry && Array.isArray(venueSDKForUse.geometry.coordinates) && venueSDKForUse.geometry.coordinates.length >= 2) {
targetLon = venueSDKForUse.geometry.coordinates[0];
targetLat = venueSDKForUse.geometry.coordinates[1];
}
if ((targetLat === null || targetLon === null) && venueObj && typeof venueObj.getOLGeometry === 'function') {
try {
const geometryOL = venueObj.getOLGeometry();
if (geometryOL && typeof geometryOL.getCentroid === 'function') {
const centroidOL = geometryOL.getCentroid();
if (typeof OpenLayers !== 'undefined' && OpenLayers.Projection) {
const transformedPoint = new OpenLayers.Geometry.Point(centroidOL.x, centroidOL.y).transform(
new OpenLayers.Projection("EPSG:3857"),
new OpenLayers.Projection("EPSG:4326")
);
targetLat = transformedPoint.y;
targetLon = transformedPoint.x;
} else {
targetLat = centroidOL.y;
targetLon = centroidOL.x;
}
}
} catch (e) {
plnLog('error',"[WME PLN] Error al obtener/transformar geometría OL para navegación:", e);
}
}
let navigated = false;
if (venueObj && W.selectionManager && typeof W.selectionManager.select === "function")
{
W.selectionManager.select(venueObj);
navigated = true;
}
else if (venueObj && W.selectionManager && typeof W.selectionManager.setSelectedModels === "function")
{
W.selectionManager.setSelectedModels([venueObj]);
navigated = true;
}
if (!navigated)
{
const confirmOpen = confirm(`El lugar "${original}" (ID: ${id}) no se pudo seleccionar o centrar directamente. ¿Deseas abrirlo en una nueva pestaña del editor?`);
if (confirmOpen)
{
const wmeUrl = `https://www.waze.com/editor?env=row&venueId=${id}`;
window.open(wmeUrl, '_blank');
}
else
{
showTemporaryMessage("El lugar podría estar fuera de vista o no cargado.", 4000, 'warning');
}
}
else
{
showTemporaryMessage("Presentando detalles del lugar...", 2000, 'info');
}
});
link.title = "Seleccionar lugar en el mapa";
link.textContent = "🔗";
permalinkCell.appendChild(link);
permalinkCell.style.padding = "4px";
permalinkCell.style.fontSize = "18px";
permalinkCell.style.textAlign = "center";
permalinkCell.style.width = "65px";
row.appendChild(permalinkCell);
const typeCityCell = document.createElement("td");
typeCityCell.style.padding = "4px";
typeCityCell.style.width = "65px";
typeCityCell.style.verticalAlign = "middle";
const cellContentWrapper = document.createElement("div");
cellContentWrapper.style.display = "flex";
cellContentWrapper.style.justifyContent = "space-around";
cellContentWrapper.style.alignItems = "center";
const typeContainer = document.createElement("div");
typeContainer.style.display = "flex";
typeContainer.style.flexDirection = "column";
typeContainer.style.alignItems = "center";
typeContainer.style.justifyContent = "center";
typeContainer.style.gap = "2px";
const typeIconSpan = document.createElement("span");
typeIconSpan.textContent = typeInfo.icon;
typeIconSpan.style.fontSize = "20px";
let tooltipText = `Tipo: ${typeInfo.title}`;
typeIconSpan.title = tooltipText;
typeContainer.appendChild(typeIconSpan);
if (typeInfo.isArea && areaMeters !== null && areaMeters !== undefined)
{
const areaSpan = document.createElement("span");
const areaFormatted = areaMeters.toLocaleString('es-ES', { maximumFractionDigits: 0 });
areaSpan.textContent = `${areaFormatted} m²`;
areaSpan.style.fontSize = "10px";
areaSpan.style.fontWeight = "bold";
areaSpan.style.textAlign = "center";
areaSpan.style.lineHeight = "1";
areaSpan.style.whiteSpace = "nowrap";
if (areaMeters < 400)
{
areaSpan.style.color = "red";
areaSpan.classList.add("area-blink");
}
else
{
areaSpan.style.color = "blue";
}
areaSpan.title = `Área: ${areaFormatted} m²`;
typeContainer.appendChild(areaSpan);
}
cellContentWrapper.appendChild(typeContainer);
const cityStatusIconSpan = document.createElement("span");
cityStatusIconSpan.className = 'city-status-icon';
cityStatusIconSpan.style.fontSize = "18px";
cityStatusIconSpan.style.cursor = "pointer";
if (hasCity)
{
cityStatusIconSpan.innerHTML = '✅';
cityStatusIconSpan.style.color = 'green';
cityStatusIconSpan.title = cityTitle;
}
else
{
cityStatusIconSpan.innerHTML = '🚩';
cityStatusIconSpan.style.color = 'red';
cityStatusIconSpan.title = cityTitle;
cityStatusIconSpan.addEventListener("click", async () =>
{
const coords = getPlaceCoordinates(W.model.venues.getObjectById(id), venueSDKForRender);
const placeLat = coords.lat;
const placeLon = coords.lon;
if (placeLat === null || placeLon === null)
{
plnToast("No se pudieron obtener las coordenadas del lugar.");
return;
}
const allCities = Object.values(W.model.cities.objects)
.filter(city =>
city &&
city.attributes &&
typeof city.attributes.name === 'string' &&
city.attributes.name.trim() !== ''
);
const citiesWithDistance = allCities.map(city =>
{
if (!city.attributes.geoJSONGeometry ||
!Array.isArray(city.attributes.geoJSONGeometry.coordinates) ||
city.attributes.geoJSONGeometry.coordinates.length < 2)
return null;
const cityLon = city.attributes.geoJSONGeometry.coordinates[0];
const cityLat = city.attributes.geoJSONGeometry.coordinates[1];
const distanceInMeters = calculateDistance(placeLat, placeLon, cityLat, cityLon);
const distanceInKm = distanceInMeters / 1000;
return {
name: city.attributes.name,
distance: distanceInKm,
cityId: city.getID()
};
}).filter(Boolean);
const closestCities = citiesWithDistance.sort((a, b) => a.distance - b.distance).slice(0, 5);
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 #aad";
modal.style.padding = "28px 32px 20px 32px";
modal.style.zIndex = "20000";
modal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
modal.style.fontFamily = "sans-serif";
modal.style.borderRadius = "10px";
modal.style.textAlign = "center";
modal.style.minWidth = "340px";
const iconElement = document.createElement("div");
iconElement.innerHTML = "🏙️";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
modal.appendChild(iconElement);
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>Asignar ciudad al lugar</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
modal.appendChild(messageTitle);
const listDiv = document.createElement("div");
listDiv.style.textAlign = "left";
listDiv.style.marginTop = "10px";
if (closestCities.length === 0)
{
const noCityLine = document.createElement("div");
noCityLine.textContent = "No se encontraron ciudades cercanas para mostrar.";
noCityLine.style.color = "#888";
listDiv.appendChild(noCityLine);
}
else
{
closestCities.forEach((city, idx) =>
{
const cityLine = document.createElement("div");
cityLine.style.marginBottom = "8px";
cityLine.style.display = "flex";
cityLine.style.alignItems = "center";
const radioInput = document.createElement("input");
radioInput.type = "radio";
radioInput.name = `city-selection-${id}`;
radioInput.value = city.cityId;
radioInput.id = `city-radio-${city.cityId}`;
radioInput.style.marginRight = "10px";
radioInput.style.marginTop = "0";
if (idx === 0) radioInput.checked = true;
const radioLabel = document.createElement("label");
radioLabel.htmlFor = `city-radio-${city.cityId}`;
radioLabel.style.cursor = "pointer";
radioLabel.innerHTML = `<b>${city.name}</b> <span style="color: #666; font-size: 11px;">(ID: ${city.cityId})</span> <span style="color: #007bff;">${city.distance.toFixed(1)} km</span>`;
cityLine.appendChild(radioInput);
cityLine.appendChild(radioLabel);
listDiv.appendChild(cityLine);
});
}
modal.appendChild(listDiv);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "flex-end";
buttonWrapper.style.gap = "12px";
buttonWrapper.style.marginTop = "20px";
const applyBtn = document.createElement("button");
applyBtn.textContent = "Aplicar Ciudad";
applyBtn.style.padding = "8px 16px";
applyBtn.style.background = "#28a745";
applyBtn.style.color = "#fff";
applyBtn.style.border = "none";
applyBtn.style.borderRadius = "4px";
applyBtn.style.cursor = "pointer";
applyBtn.style.fontWeight = "bold";
applyBtn.addEventListener('click', () => {
const selectedRadio = modal.querySelector(`input[name="city-selection-${id}"]:checked`);
if (!selectedRadio) {
plnToast("Por favor, selecciona una ciudad de la lista.");
return;
}
const selectedCityId = parseInt(selectedRadio.value, 10);
const selectedCityName = selectedRadio.parentElement.querySelector('label b').textContent;
const venueToUpdate = W.model.venues.getObjectById(id);
if (!venueToUpdate)
{
plnToast("Error: No se pudo encontrar el lugar para actualizar. Puede que ya no esté visible.");
modal.remove();
return;
}
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(venueToUpdate, { cityID: selectedCityId });
W.model.actionManager.add(action);
const row = document.querySelector(`tr[data-place-id="${id}"]`);
if (row)
{
row.dataset.addressChanged = 'true';
const iconToUpdate = row.querySelector('.city-status-icon');
if (iconToUpdate)
{
iconToUpdate.innerHTML = '✅';
iconToUpdate.style.color = 'green';
iconToUpdate.title = `Ciudad asignada: ${selectedCityName}`;
iconToUpdate.style.pointerEvents = 'none';
}
updateApplyButtonState(row, original);
}
modal.remove();
showTemporaryMessage("Ciudad asignada correctamente. No olvides Guardar los cambios.", 4000, 'success');
}
catch (e)
{
plnLog('error',"[WME PLN] Error al crear o ejecutar la acción de actualizar ciudad:", e);
plnToast("Ocurrió un error al intentar asignar la ciudad: " + e.message);
}
});
const closeBtn = document.createElement("button");
closeBtn.textContent = "Cerrar";
closeBtn.style.padding = "8px 16px";
closeBtn.style.background = "#888";
closeBtn.style.color = "#fff";
closeBtn.style.border = "none";
closeBtn.style.borderRadius = "4px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontWeight = "bold";
closeBtn.addEventListener("click", () => modal.remove());
buttonWrapper.appendChild(applyBtn);
buttonWrapper.appendChild(closeBtn);
modal.appendChild(buttonWrapper);
closeBtn.style.padding = "8px 16px";
closeBtn.style.background = "#888";
closeBtn.style.color = "#fff";
closeBtn.style.border = "none";
closeBtn.style.borderRadius = "4px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontWeight = "bold";
closeBtn.addEventListener("click", () => modal.remove());
buttonWrapper.appendChild(applyBtn);
buttonWrapper.appendChild(closeBtn);
modal.appendChild(buttonWrapper);
document.body.appendChild(modal);
});
}
cellContentWrapper.appendChild(cityStatusIconSpan);
typeCityCell.appendChild(cellContentWrapper);
row.appendChild(typeCityCell);
const lockCell = document.createElement("td");
lockCell.textContent = lockRankEmoji;
lockCell.style.textAlign = "center";
lockCell.style.padding = "4px";
lockCell.style.width = "40px";
lockCell.style.fontSize = "18px";
row.appendChild(lockCell);
const editorCell = document.createElement("td");
editorCell.textContent = editor || "Desconocido";
editorCell.title = "Último editor";
editorCell.style.padding = "4px";
editorCell.style.width = "140px";
editorCell.style.textAlign = "center";
row.appendChild(editorCell);
const originalCell = document.createElement("td");
const inputOriginal = document.createElement("textarea");
inputOriginal.rows = 3; inputOriginal.readOnly = true;
inputOriginal.style.whiteSpace = "pre-wrap";
const venueLive = W.model.venues.getObjectById(id);
const currentLiveName = venueLive?.attributes?.name?.value || venueLive?.attributes?.name || "";
inputOriginal.value = currentLiveName || original;
if (currentLiveName.trim().toLowerCase() !== normalized.trim().toLowerCase())
{
inputOriginal.style.border = "1px solid red";
inputOriginal.title = "Este nombre es distinto del original mostrado en el panel";
}
inputOriginal.disabled = true;
inputOriginal.style.width = "270px";
inputOriginal.style.backgroundColor = "#eee";
originalCell.style.padding = "4px";
originalCell.style.width = "270px";
originalCell.style.display = "flex";
originalCell.style.alignItems = "flex-start";
originalCell.style.verticalAlign = "middle";
inputOriginal.style.flex = "1";
inputOriginal.style.height = "100%";
inputOriginal.style.boxSizing = "border-box";
originalCell.appendChild(inputOriginal);
row.appendChild(originalCell);
const alertCell = document.createElement("td");
alertCell.style.width = "30px";
alertCell.style.textAlign = "center";
alertCell.style.verticalAlign = "middle";
alertCell.style.padding = "4px";
// Lógica para el icono de advertencia por duplicados
if (isDuplicate)
{
const warningIcon = document.createElement("span");
warningIcon.textContent = " ⚠️";
warningIcon.style.fontSize = "16px";
let tooltipText = `Nombre de lugar duplicado cercano.`;
if (duplicatePartners && duplicatePartners.length > 0)
{
const partnerDetails = duplicatePartners.map(p => `Línea ${p.line}: "${p.originalName}"`).join(", ");
tooltipText += ` Duplicado(s) con: ${partnerDetails}.`;
}
else
{
tooltipText += ` No se encontraron otros duplicados cercanos específicos.`;
}
warningIcon.title = tooltipText;
alertCell.appendChild(warningIcon);
}
// Lógica para el icono de advertencia por horarios que se cruzan
if (hasOverlappingHours)
{
const clockIcon = document.createElement("span");
clockIcon.textContent = " ⏰";
clockIcon.style.fontSize = "16px";
clockIcon.style.color = "red";
clockIcon.title = "¡Alerta! Este lugar tiene horarios que se cruzan.";
alertCell.appendChild(clockIcon);
}
row.appendChild(alertCell);
const suggestionCell = document.createElement("td");
suggestionCell.style.display = "flex";
suggestionCell.style.alignItems = "flex-start";
suggestionCell.style.justifyContent = "flex-start";
suggestionCell.style.padding = "4px";
suggestionCell.style.width = "270px";
const inputReplacement = document.createElement("textarea");
inputReplacement.className = 'replacement-input';
try
{
inputReplacement.value = normalized;
}
catch (_)
{
inputReplacement.value = normalized;
}
inputReplacement.style.width = "100%";
inputReplacement.style.height = "100%";
inputReplacement.style.boxSizing = "border-box";
inputReplacement.style.whiteSpace = "pre-wrap";
inputReplacement.rows = 3;
suggestionCell.appendChild(inputReplacement);
const brainButton = document.createElement('button');
brainButton.innerHTML = '🧠';
brainButton.className = 'pln-ai-brain-btn';
brainButton.title = 'Obtener sugerencia de la IA para este lugar';
brainButton.style.display = 'inline-block';
brainButton.style.marginLeft = '5px';
brainButton.style.padding = '5px';
brainButton.style.verticalAlign = 'top';
brainButton.style.background = '#f0f0f0';
brainButton.style.border = '1px solid #ccc';
brainButton.style.borderRadius = '4px';
brainButton.style.cursor = 'pointer';
suggestionCell.appendChild(brainButton); // Asegúrate que esta línea esté después de la creación del botón
brainButton.addEventListener('click', () => {
const currentRow = brainButton.closest('tr');
const originalName = currentRow.querySelector('td:nth-child(6) textarea').value;
const placeId = currentRow.dataset.placeId;
// Llamada a la función central que maneja la IA
handleAiRequestForRow(brainButton, currentRow, placeId, originalName);
});
function debounce(func, delay)
{
let timeout;
return function (...args)
{
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
const checkAndUpdateApplyButton = () =>
{
const nameIsDifferent = inputReplacement.value.trim() !== original.trim();
const categoryWasChanged = row.dataset.categoryChanged === 'true';
if (nameIsDifferent || categoryWasChanged)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
const successIcon = applyButtonWrapper.querySelector('span');
if (successIcon) successIcon.remove();
}
else
{
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
}
};
inputReplacement.addEventListener('input', debounce(checkAndUpdateApplyButton, 300));
let autoApplied = false;
if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded' && s.similarity === 1))
{
autoApplied = true;
}
if (autoApplied)
{
inputReplacement.style.backgroundColor = "#c8e6c9";
inputReplacement.title = "Reemplazo automático aplicado (palabra especial con 100% similitud)";
}
else if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded'))
{
inputReplacement.style.backgroundColor = "#fff3cd";
inputReplacement.title = "Contiene palabra especial reemplazada";
}
function debounce(func, delay)
{
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
inputReplacement.addEventListener('input', debounce(() =>
{
if (inputReplacement.value.trim() !== original)
{
applyButton.disabled = false;
applyButton.style.color = "";
}
else
{
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
}, 300));
inputOriginal.addEventListener('input', debounce(() =>
{
}, 300));
const suggestionListCell = document.createElement("td");
suggestionListCell.style.padding = "4px";
suggestionListCell.style.width = "180px";
const suggestionContainer = document.createElement('div');
const palabrasYaProcesadas = new Set();
const currentPlaceSuggestions = allSuggestions[id];
if (currentPlaceSuggestions)
{
Object.entries(currentPlaceSuggestions).forEach(([originalWordForThisPlace, suggestionsArray]) =>
{
if (Array.isArray(suggestionsArray))
{
suggestionsArray.forEach(s =>
{
let icono = '';
let textoSugerencia = '';
let colorFondo = '#f9f9f9';
let esSugerenciaValida = false;
let palabraAReemplazar = originalWordForThisPlace;
let palabraAInsertar = s.word;
switch (s.fuente)
{
case 'original_preserved':
esSugerenciaValida = true;
icono = '⚙️';
textoSugerencia = `¿"${originalWordForThisPlace}" x "${s.word}"?`;
colorFondo = '#f0f0f0';
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = s.word;
break;
case 'excluded':
if (s.similarity < 1 || (s.similarity === 1 && originalWordForThisPlace.toLowerCase() !== s.word.toLowerCase()))
{
esSugerenciaValida = true;
icono = '🏷️';
textoSugerencia = `¿"${originalWordForThisPlace}" x "${s.word}"? (sim. ${(s.similarity * 100).toFixed(0)}%)`;
colorFondo = '#f3f9ff';
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = s.word;
palabrasYaProcesadas.add(originalWordForThisPlace.toLowerCase());
}
break;
case 'dictionary':
esSugerenciaValida = true;
icono = '📘';
colorFondo = '#e6ffe6';
// Capitaliza la palabra sugerida desde el diccionario
const capitalizedDictionaryWord = s.word.charAt(0).toUpperCase() + s.word.slice(1);
textoSugerencia = `¿"${originalWordForThisPlace}" x "${capitalizedDictionaryWord}"? (sim. ${(s.similarity * 100).toFixed(0)}%)`;
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = capitalizedDictionaryWord;
break;
case 'dictionary_tilde':
esSugerenciaValida = true;
icono = '✍️';
colorFondo = '#ffe6e6';
// Capitaliza la palabra sugerida con corrección de tilde
const capitalizedTildeWord = s.word.charAt(0).toUpperCase() + s.word.slice(1);
textoSugerencia = `¿"${originalWordForThisPlace}" x "${capitalizedTildeWord}"? (Corregir Tilde)`;
palabraAReemplazar = originalWordForThisPlace;
palabraAInsertar = capitalizedTildeWord;
break;
}
if (esSugerenciaValida)
{
const suggestionDiv = document.createElement("div");
suggestionDiv.innerHTML = `${icono} ${textoSugerencia}`;
suggestionDiv.style.cursor = "pointer";
suggestionDiv.style.padding = "2px 4px";
suggestionDiv.style.margin = "2px 0";
suggestionDiv.style.border = "1px solid #ddd";
suggestionDiv.style.borderRadius = "3px";
suggestionDiv.style.backgroundColor = colorFondo;
suggestionDiv.addEventListener("click", () =>
{
const currentSuggestedValue = inputReplacement.value;
const searchRegex = new RegExp("\\b" + escapeRegExp(palabraAReemplazar) + "\\b", "gi");
const newSuggestedValue = currentSuggestedValue.replace(searchRegex, palabraAInsertar);
if (inputReplacement.value !== newSuggestedValue)
{
inputReplacement.value = newSuggestedValue;
}
checkAndUpdateApplyButton();
});
suggestionContainer.appendChild(suggestionDiv);
}
});
}
else
{
plnLog('warn',`[WME_PLN][DEBUG] suggestionsArray para "${originalWordForThisPlace}" no es un array o es undefined:`, suggestionsArray);
}
});
}
suggestionListCell.appendChild(suggestionContainer);
row.appendChild(suggestionCell);
row.appendChild(suggestionListCell);
const categoryCell = document.createElement("td");
categoryCell.style.padding = "4px";
categoryCell.style.width = "130px";
categoryCell.style.textAlign = "center";
const currentCategoryDiv = document.createElement("div");
currentCategoryDiv.style.display = "flex";
currentCategoryDiv.style.flexDirection = "column";
currentCategoryDiv.style.alignItems = "center";
currentCategoryDiv.style.gap = "2px";
const currentCategoryText = document.createElement("span");
currentCategoryText.textContent = currentCategoryTitle;
currentCategoryText.title = `Categoría Actual: ${currentCategoryTitle}`;
currentCategoryDiv.appendChild(currentCategoryText);
const currentCategoryIconDisplay = document.createElement("span");
currentCategoryIconDisplay.textContent = currentCategoryIcon;
currentCategoryIconDisplay.style.fontSize = "20px";
currentCategoryDiv.appendChild(currentCategoryIconDisplay);
categoryCell.appendChild(currentCategoryDiv);
row.appendChild(categoryCell);
const recommendedCategoryCell = document.createElement("td");
recommendedCategoryCell.style.padding = "4px";
recommendedCategoryCell.style.width = "180px";
recommendedCategoryCell.style.textAlign = "left";
const categoryDropdown = createRecommendedCategoryDropdown(
id,
currentCategoryKey,
dynamicCategorySuggestions
);
recommendedCategoryCell.appendChild(categoryDropdown);
// Contenedor para la sugerencia de la IA
const aiSuggestionContainer = document.createElement('div');
aiSuggestionContainer.id = `ai-suggestion-container-${id}`; // ID único por fila
aiSuggestionContainer.style.marginTop = '8px';
aiSuggestionContainer.style.minHeight = '30px'; // Espacio reservado
recommendedCategoryCell.appendChild(aiSuggestionContainer);
row.appendChild(recommendedCategoryCell);
const actionCell = document.createElement("td");
actionCell.style.padding = "4px";
actionCell.style.width = "120px";
const buttonGroup = document.createElement("div");
buttonGroup.style.display = "flex";
buttonGroup.style.flexDirection = "column";
buttonGroup.style.gap = "4px";
buttonGroup.style.alignItems = "flex-start";
const commonButtonStyle = {
width: "40px",
height: "30px",
minWidth: "40px",
minHeight: "30px",
padding: "4px",
border: "1px solid #ccc",
borderRadius: "4px",
backgroundColor: "#f0f0f0",
color: "#555",
cursor: "pointer",
fontSize: "18px",
display: "flex",
justifyContent: "center",
alignItems: "center",
boxSizing: "border-box"
};
const applyButton = document.createElement("button");
Object.assign(applyButton.style, commonButtonStyle);
applyButton.textContent = "✔";
applyButton.title = "Aplicar sugerencia";
applyButton.disabled = true;
applyButton.style.opacity = "0.5";
const applyButtonWrapper = document.createElement("div");
applyButtonWrapper.style.display = "flex";
applyButtonWrapper.style.alignItems = "center";
applyButtonWrapper.style.gap = "5px";
applyButtonWrapper.appendChild(applyButton);
buttonGroup.appendChild(applyButtonWrapper);
let deleteButton = document.createElement("button");
Object.assign(deleteButton.style, commonButtonStyle);
deleteButton.textContent = "🗑️";
deleteButton.title = "Eliminar lugar";
const deleteButtonWrapper = document.createElement("div");
Object.assign(deleteButtonWrapper.style, {
display: "flex",
alignItems: "center",
gap: "5px"
});
deleteButtonWrapper.appendChild(deleteButton);
buttonGroup.appendChild(deleteButtonWrapper);
const addToExclusionBtn = document.createElement("button");
Object.assign(addToExclusionBtn.style, commonButtonStyle);
addToExclusionBtn.textContent = "🏷️";
addToExclusionBtn.title = "Marcar palabra como especial (no se modifica)";
buttonGroup.appendChild(addToExclusionBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
const excludePlaceBtn = document.createElement("button");
Object.assign(excludePlaceBtn.style, commonButtonStyle);
excludePlaceBtn.textContent = "📵";
excludePlaceBtn.title = "Excluir este lugar (no aparecerá en futuras búsquedas)";
buttonGroup.appendChild(excludePlaceBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
// Comparamos el valor actual real con el normalizado y si son idénticos,
// añadimos la clase para ocultar la fila ANTES de que se renderice.
// Esto elimina la condición de carrera por completo.
const finalCurrentName = (currentLiveName || original).trim();
const finalNormalizedName = normalized.trim();
if (finalCurrentName === finalNormalizedName)
{
row.classList.add('pln-hidden-normalized');
}
applyButton.addEventListener("click", async () => {
const row = applyButton.closest('tr');
const venueObj = W.model.venues.getObjectById(id);
if (!venueObj) {
showTemporaryMessage("Error: No se pudo encontrar el lugar.", 4000, 'error');
return;
}
const newName = inputReplacement.value.trim();
const nameWasChanged = (newName !== (venueObj?.attributes?.name?.value || venueObj?.attributes?.name || ""));
const categoryWasChanged = row.dataset.categoryChanged === 'true';
if (!nameWasChanged && !categoryWasChanged) {
showTemporaryMessage("No hay cambios para aplicar.", 3000, 'info');
return;
}
try {
// Usamos el SDK de Waze para aplicar los cambios
if (nameWasChanged) {
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(venueObj, { name: newName });
W.model.actionManager.add(action);
}
// La categoría ya se aplicó al seleccionarla, solo necesitamos registrar el evento
showTemporaryMessage("Cambios aplicados. Presiona 'Guardar' en WME.", 3000, 'success');
recordNormalizationEvent(); // Para las estadísticas
updateInconsistenciesCount(-1); // Para el contador
// --- NUEVA LÓGICA VISUAL (NO OCULTA LA FILA) ---
// 1. Deshabilitar TODOS los botones de acción de esta fila
const actionButtons = row.querySelectorAll('td:last-child button');
actionButtons.forEach(btn => {
btn.disabled = true;
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.5';
});
// 2. Cambiar el botón "Aplicar" para mostrar que fue exitoso
applyButton.innerHTML = '✔️';
applyButton.style.backgroundColor = '#28a745';
applyButton.style.color = 'white';
applyButton.style.opacity = '1';
applyButton.title = 'Aplicado';
} catch (e) {
plnToast("Error al aplicar cambios: " + e.message);
plnLog('error',"[WME PLN] Error al aplicar cambios:", e);
}
});
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 #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000";
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
confirmModal.appendChild(iconElement);
const message = document.createElement("div");
const venue = W.model.venues.getObjectById(id);
const placeName = venue?.attributes?.name?.value || venue?.attributes?.name || "este lugar";
message.innerHTML = `<b>¿Eliminar "${placeName}"?</b>`;
message.style.fontSize = "20px";
message.style.marginBottom = "8px";
confirmModal.appendChild(message);
const nameDiv = document.createElement("div");
nameDiv.textContent = `"${placeName}"`;
nameDiv.style.fontSize = "15px";
nameDiv.style.color = "#007bff";
nameDiv.style.marginBottom = "18px";
confirmModal.appendChild(nameDiv);
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.padding = "7px 18px";
cancelBtn.style.background = "#eee";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => confirmModal.remove());
const confirmBtn = document.createElement("button");
confirmBtn.textContent = "Eliminar";
confirmBtn.style.padding = "7px 18px";
confirmBtn.style.background = "#d9534f";
confirmBtn.style.color = "#fff";
confirmBtn.style.border = "none";
confirmBtn.style.borderRadius = "4px";
confirmBtn.style.cursor = "pointer";
confirmBtn.style.fontWeight = "bold";
confirmBtn.addEventListener("click", () =>
{
const venue = W.model.venues.getObjectById(id);
if (!venue)
{
plnLog('error',"[WME_PLN]El lugar no está disponible o ya fue eliminado.");
confirmModal.remove();
return;
}
try
{
const DeleteObject = require("Waze/Action/DeleteObject");
const action = new DeleteObject(venue);
W.model.actionManager.add(action);
recordNormalizationEvent();
const row = deleteButton.closest('tr');
markRowAsProcessed(row, 'deleted');
updateInconsistenciesCount(-1);
deleteButton.disabled = true;
deleteButton.style.color = "#bbb";
deleteButton.style.opacity = "0.5";
applyButton.disabled = true;
applyButton.style.color = "#bbb";
applyButton.style.opacity = "0.5";
const successIcon = document.createElement("span");
successIcon.textContent = " 🗑️";
successIcon.style.marginLeft = "0";
successIcon.style.fontSize = "20px";
deleteButtonWrapper.appendChild(successIcon);
}
catch (e)
{
plnLog('error',"[WME_PLN] Error al eliminar lugar: " + e.message, e);
}
confirmModal.remove();
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal);
});
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 #aad";
modal.style.padding = "28px 32px 20px 32px";
modal.style.zIndex = "20000";
modal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
modal.style.fontFamily = "sans-serif";
modal.style.borderRadius = "10px";
modal.style.textAlign = "center";
modal.style.minWidth = "340px";
const title = document.createElement("h4");
title.textContent = "Agregar palabra a especiales";
modal.appendChild(title);
const instructions = document.createElement("p");
const list = document.createElement("ul");
list.style.listStyle = "none";
list.style.padding = "0";
words.forEach(w =>
{
if (w.trim() === '') return;
const lowerW = w.trim().toLowerCase();
if (!/[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9]/.test(lowerW) || /^[^a-zA-Z0-9]+$/.test(lowerW)) return;
const alreadyExists = Array.from(excludedWords).some(existing => existing.toLowerCase() === lowerW);
if (commonWords.includes(lowerW) || alreadyExists) return;
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = w;
checkbox.id = `cb-exc-${w.replace(/[^a-zA-Z0-9]/g, "")}`;
li.appendChild(checkbox);
const label = document.createElement("label");
label.htmlFor = checkbox.id;
label.appendChild(document.createTextNode(" " + w));
li.appendChild(label);
list.appendChild(li);
});
modal.appendChild(list);
const confirmBtn = document.createElement("button");
confirmBtn.textContent = "Añadir Seleccionadas";
confirmBtn.addEventListener("click", () =>
{
const checked = modal.querySelectorAll("input[type=checkbox]:checked");
let wordsActuallyAdded = false;
checked.forEach(c =>
{
if (!excludedWords.has(c.value))
{
excludedWords.add(c.value);
wordsActuallyAdded = true;
}
});
if (wordsActuallyAdded)
{
if (typeof renderExcludedWordsList === 'function')
{
const excludedListElement = document.getElementById("excludedWordsList");
if (excludedListElement)
{
renderExcludedWordsList(excludedListElement);
}
else
{
renderExcludedWordsList();
}
}
}
modal.remove();
if (wordsActuallyAdded)
{
saveExcludedWordsToLocalStorage();
showTemporaryMessage("Palabra(s) añadida(s) a especiales y guardada(s).", 3000, 'success');
}
else
{
showTemporaryMessage("No se seleccionaron palabras o ya estaban en la lista.", 3000, 'info');
}
});
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);
// Reemplaza el addEventListener del excludePlaceBtn con esto:
excludePlaceBtn.addEventListener("click", () => {
const placeName = original || `ID: ${id}`;
const row = excludePlaceBtn.closest('tr');
excludePlace(row, id, placeName);
});
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
//Descripción: Compara los nombres y añade la clase de ocultar durante
// la creación de la fila para eliminar la condición de carrera.
if ((original || "").trim() === (normalized || "").trim())
{
row.classList.add('pln-hidden-normalized');
}
row.style.borderBottom = "1px solid #ddd";
row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff";
row.querySelectorAll("td").forEach(td =>
{
td.style.verticalAlign = "top";
});
tbody.appendChild(row);
checkAndUpdateApplyButton();
setTimeout(() =>
{
const progress = Math.floor(((index + 1) / inconsistentsToRender.length) * 100);
const progressElem = document.getElementById("scanProgressText");
if (progressElem)
{
progressElem.textContent = `Analizando lugares: ${progress}% (${index + 1}/${inconsistentsToRender.length})`;
}
}, 0);
});
table.appendChild(tbody);
output.appendChild(table);
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
{
existingOverlay.remove();
}
// Forzar una re-evaluación final para asegurar que se oculten las filas ya normalizadas.
if (typeof window.__plnHideNormalizedRows === 'function')
{
setTimeout(() => window.__plnHideNormalizedRows(), 100); // Pequeño delay para asegurar que el DOM está listo.
}
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})`;
}
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, () =>
{
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons, 250);
});
}
else
{
plnLog('ui', "[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
});
W.model.actionManager.events.register("afterredoaction", null, () =>
{
if (floatingPanelElement && floatingPanelElement.style.display !== 'none')
{
waitForWazeAPI(() =>
{
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places);
setTimeout(reactivateAllActionButtons, 250);
});
}
else
{
plnLog('ui', "[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
}
});
}
}// renderPlacesInFloatingPanel
// Muestra un spinner de procesamiento con un mensaje opcional
function showProcessingSpinner(msg = "Procesando...")
{
const panel = document.querySelector('#user-panel-root');
if (!panel || document.getElementById('pln-spinner')) return;
const wrapper = document.createElement('div');
wrapper.id = 'pln-spinner';
wrapper.style.cssText = `
position:absolute;
top:35%;
left:50%;
transform:translate(-50%,-50%);
background:rgba(255,255,255,0.95);
padding:12px 20px;
border-radius:8px;
z-index:9999;
font-weight:bold;
box-shadow:0 2px 6px rgba(0,0,0,0.25);
`;
wrapper.innerHTML = `<div class="wz-icon wz-icon-spinner" style="margin-right:8px;animation:spin 1s linear infinite"></div>${msg}`;
panel.appendChild(wrapper);
}// showProcessingSpinner
// Oculta el spinner de procesamiento si está visible
function hideProcessingSpinner()
{
const el = document.getElementById('pln-spinner');
if (el) el.remove();
}// hideProcessingSpinner
// Muestra un mensaje tipo "toast" en la esquina inferior derecha
function plnToast(message, duration = 3000)
{
try
{
let container = document.getElementById('pln-toast-container');
if (!container)
{
container = document.createElement('div');
container.id = 'pln-toast-container';
container.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px;';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
background-color: #333;
color: white;
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
font-family: sans-serif;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
transform: translateY(20px);
`;
container.appendChild(toast);
// Animación de entrada
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
}, 10);
// Desaparición automática
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(20px)';
// Eliminar del DOM después de la animación
toast.addEventListener('transitionend', () => toast.remove());
}, duration);
}
catch (e)
{
// Fallback si algo sale mal
plnLog('error',"Error en plnToast:", e);
alert(message);
}
}// plnToast
// Notifica que un lugar ha sido procesado exitosamente
function plnNotifyProcessed(placeName)
{
if (!placeName) return;
const message = `'${placeName}' procesado.`;
plnToast(message, 2000);
plnLog('normalize', `✅ ${message}`);
}// plnNotifyProcessed
// Función para obtener información del editor. Se queda aquí porque es necesaria al inicio.
function getCurrentEditorViaWazeWrap()
{
if (WazeWrap && WazeWrap.User)
{
const user = WazeWrap.User.getActiveUser();
return {
id: user.id,
name: user.userName,
privilege: user.rank
};
}
return { id: null, name: 'Desconocido', privilege: 'N/A' };
}
// ================== UI ADAPTERS ==================
if (typeof window.plnUiConfirm !== 'function')
{
window.plnUiConfirm = function (message, opts = {})
{
return new Promise(resolve =>
{
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.background = 'rgba(0,0,0,0.35)';
overlay.style.zIndex = '10006';
const dialog = document.createElement('div');
dialog.role = 'dialog';
dialog.ariaLabel = 'Confirmación';
dialog.style.position = 'fixed';
dialog.style.top = '50%';
dialog.style.left = '50%';
dialog.style.transform = 'translate(-50%, -50%)';
dialog.style.background = '#fff';
dialog.style.padding = '14px 16px';
dialog.style.borderRadius = '6px';
dialog.style.boxShadow = '0 8px 24px rgba(0,0,0,.25)';
dialog.style.minWidth = '320px';
const msg = document.createElement('div');
msg.style.marginBottom = '10px';
msg.style.fontSize = '13px';
msg.textContent = message || '¿Confirmar acción?';
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = opts.cancelText || 'Cancelar';
cancel.onclick = () => { document.body.removeChild(overlay); resolve(false); };
const ok = document.createElement('button');
ok.type = 'button';
ok.textContent = opts.okText || 'Aceptar';
ok.style.background = '#d9534f';
ok.style.color = '#fff';
ok.style.border = '1px solid #c9302c';
ok.style.borderRadius = '4px';
ok.onclick = () => { document.body.removeChild(overlay); resolve(true); };
actions.appendChild(cancel);
actions.appendChild(ok);
dialog.appendChild(msg);
dialog.appendChild(actions);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
});
};
}