您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Módulo de UI para WME Place Normalizer. No funciona por sí solo.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @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); }); }; }