Normaliza nombres de lugares y gestiona categorías dinámicamente en WME.
当前为
// ==UserScript==
// @name WME Places Name Normalizer
// @namespace https://gf.qytechs.cn/en/users/mincho77
// @version 7.1.1
// @author Mincho77
// @description Normaliza nombres de lugares y gestiona categorías dinámicamente en WME.
// @license MIT
// @include https://beta.waze.com/*
// @include https://www.waze.com/editor*
// @include https://www.waze.com/*/editor*
// @exclude https://www.waze.com/user/editor*
// @grant GM_xmlhttpRequest
// @connect sheets.googleapis.com
// @run-at document-end
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// ==/UserScript==
(function ()
{
// Variables globales básicas
const SCRIPT_NAME = GM_info.script.name;
const VERSION = GM_info.script.version.toString();
// Variables globales para el diccionario de palabras excluidas
//Permite inicializar el diccionario de palabras intercambiadas
if (!window.swapWords)
{
const stored = localStorage.getItem("wme_swapWords");
window.swapWords = stored ? JSON.parse(stored) : [];
}
// Variables globales para el panel flotante
let floatingPanelElement = null;
let dynamicCategoriesLoaded = false;
const tempSelectedCategories = new Map(); // Mapa para placeId -> categoryKey seleccionada
const placesForDuplicateCheckGlobal = []; // Nueva variable global para almacenar datos de lugares para verificar duplicados
const processingPanelDimensions = { width: '400px', height: '200px' }; // Panel pequeño para procesamiento
const resultsPanelDimensions = { width: '1400px', height: '700px' }; // Panel grande para resultados
const commonWords = [//Palabras comunes en español que no deberían ser consideradas para normalización
'de', 'del', 'el', 'la', 'los', 'las', 'y', 'e',
'o', 'u', 'un', 'una', 'unos', 'unas', 'a', 'en',
'con', 'tras', 'por', 'al', 'lo'
];
const tabNames = [//Definir nombres de pestañas cortos antes de la generación de botones
{ label: "Gene", icon: "⚙️" },
{ label: "Espe", icon: "🏷️" },
{ label: "Dicc", icon: "📘" },
{ label: "Reemp", icon: "🔂" }
];
let wmeSDK = null; // Almacena la instancia del SDK de WME.
//Novedades de cada version del script esto permitirá una pantalla la primera vez que se abra el script
const myChangelog = {
[VERSION]: {
"Novedades": [
"Detección de duplicados cercanos: Alerta ahora sobre nombres duplicados a menos de 50m, mostrando el N° de registro para fácil revisión.",
"Campo 'Categoría Recomendada' optimizado: Inicia en blanco y permite búsqueda, con homogeneidad visual en botones.",
"Se eliminan todos los Emoticons de los nombres de los places: Se eliminan todos los emoticones de los nombres de los lugares, pues WME y el APP ya no los aceptan con colores",
"Limpieza mejorada: Mayor precisión en la eliminación de comillas simples en categorías y se solucionó el problema al aplicar cambios en nombres que solo involucraban emoticones.",
"Gestión de categorías desde Google Sheets: ¡Nuevas categorías y sus íconos ahora se cargan directamente desde un archivo de Google Sheets! Esto permite mantener la lista de categorías más actualizada y flexible.",
"Ya no se debe cargar el archivo XML de diccionario: Ahora el diccionario se carga directamente desde Google Sheets, eliminando la necesidad de cargar un archivo XML.",
"Diccionario también desde Google Sheets: El diccionario de palabras válidas también se carga ahora desde un archivo de Google Sheets, facilitando su actualización y personalización."
],
"Correcciones": [
"Ventanas emergentes (modales): Solucionado el problema por el cual la ventana de confirmación aparecía duplicada al eliminar lugares.",
"Rendimiento del script: Se corrigieron algunos problemas internos que causaban errores y mejoraron la estabilidad general del script."
]
}
};// myChangelog
// Función que construirá el HTML del changelog
function getChangelogHtml(versionData)
{
let html = '';
if (versionData["Novedades"] && versionData["Novedades"].length > 0)
{
html += `<h6>Novedades:</h6><ul style="margin-bottom: 10px; list-style-type: disc; margin-left: 20px;">`;
versionData["Novedades"].forEach(item => {
html += `<li>${item}</li>`;
});
html += `</ul>`;
}
if (versionData["Correcciones"] && versionData["Correcciones"].length > 0) {
html += `<h6>Correcciones:</h6><ul style="margin-bottom: 10px; list-style-type: disc; margin-left: 20px;">`;
versionData["Correcciones"].forEach(item => {
html += `<li>${item}</li>`;
});
html += `</ul>`;
}
return html;
}//getChangelogHtml
// Función para mostrar el changelog al actualizar el script
function showChangelogOnUpdate()
{
const LAST_SEEN_VERSION_KEY = `${SCRIPT_NAME}_last_seen_version`;
const lastSeenVersion = localStorage.getItem(LAST_SEEN_VERSION_KEY);
const currentScriptVersion = VERSION; // Variable global VERSION
// Obtener la versión actual del script desde GM_info
const versionData = myChangelog[currentScriptVersion];
// Verificar si hay datos de versión y si la versión actual es diferente a la última vista
if (versionData && currentScriptVersion !== lastSeenVersion)
{
const title = `${SCRIPT_NAME} v${currentScriptVersion}`;
const bodyHtml = getChangelogHtml(versionData); // Genera el HTML del cuerpo
// Crear el modal
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.backgroundColor = "#fff";
modal.style.border = "1px solid #ccc";
modal.style.borderRadius = "8px";
modal.style.boxShadow = "0 5px 15px rgba(0,0,0,0.3)";
modal.style.padding = "20px";
modal.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
modal.style.zIndex = "20000"; // Por encima de casi todo
modal.style.width = "450px";
modal.style.maxHeight = "80vh";
modal.style.overflowY = "auto";
// Estilos adicionales para el modal
const modalTitle = document.createElement("h3");
modalTitle.textContent = title;
modalTitle.style.marginTop = "0";
modalTitle.style.marginBottom = "15px";
modalTitle.style.textAlign = "center";
modalTitle.style.color = "#333";
// Crear el cuerpo del modal con el contenido del changelog
const modalBody = document.createElement("div");
modalBody.innerHTML = bodyHtml;
// Estilos para el cuerpo del modal
const closeButton = document.createElement("button");
closeButton.textContent = "Entendido";
closeButton.style.display = "block";
closeButton.style.margin = "20px auto 0 auto";
closeButton.style.padding = "10px 20px";
closeButton.style.backgroundColor = "#007bff";
closeButton.style.color = "#fff";
closeButton.style.border = "none";
closeButton.style.borderRadius = "5px";
closeButton.style.cursor = "pointer";
//
closeButton.addEventListener("click", () => {
modal.remove();
localStorage.setItem(LAST_SEEN_VERSION_KEY, currentScriptVersion); // Guarda la versión
});
// Añadir todo al modal y al body
modal.appendChild(modalTitle);
modal.appendChild(modalBody);
modal.appendChild(closeButton);
document.body.appendChild(modal);
}
else if (!versionData)
{//
// Si no hay datos de versión, no se hace nada
localStorage.setItem(LAST_SEEN_VERSION_KEY, currentScriptVersion);
}
}//showChangelogOnUpdate
//Permite inicializar el SDK de WME
function tryInitializeSDK(finalCallback)
{
let attempts = 0;
const maxAttempts = 60; // Intentos máximos (60 * 500ms = 30 segundos)
const intervalTime = 500;
let sdkAttemptInterval = null;
// Función interna para intentar obtener el SDK de WME
function attempt()
{
if (typeof getWmeSdk === 'function')
{
if (sdkAttemptInterval)
clearInterval(sdkAttemptInterval);
try
{
wmeSDK = getWmeSdk({scriptId : 'WMEPlacesNameInspector', scriptName : SCRIPT_NAME, });
if (wmeSDK)
console.log("[SDK INIT SUCCESS] SDK obtenido exitosamente:", wmeSDK);
else
console.warn("[SDK INIT WARNING] getWmeSdk() fue llamada pero devolvió null/undefined.");
}
catch (e)
{
console.error("[SDK INIT ERROR] Error al llamar a getWmeSdk():", e);
wmeSDK = null;
}
finalCallback();
return;
}
attempts++;
if (attempts >= maxAttempts)
{
if (sdkAttemptInterval) clearInterval(sdkAttemptInterval);
// console.error(`[SDK INIT FAILURE] No se pudo encontrar getWmeSdk() después de ${maxAttempts} intentos.`);
wmeSDK = null;
finalCallback();
}
}
sdkAttemptInterval = setInterval(attempt, intervalTime);
attempt();
}//tryInitializeSDK
// Esperar a que la API principal de Waze esté completamente cargada
async function waitForWazeAPI(callbackPrincipalDelScript)
{
let wAttempts = 0;
const wMaxAttempts = 40;
const wInterval = setInterval(async () => {
wAttempts++;
if (typeof W !== 'undefined' && W.map && W.loginManager && W.model && W.model.venues && W.userscripts && typeof W.userscripts.registerSidebarTab === 'function')
{
clearInterval(wInterval);
if (!dynamicCategoriesLoaded) // solo carga las categorías de Google Sheets si no se han cargado aún
{
try
{
await loadDynamicCategoriesFromSheet();
dynamicCategoriesLoaded = true; // <-- Marcar como cargado
}
catch (error)
{
console.error("No se pudieron cargar las categorías dinámicas:", error);
}
}
tryInitializeSDK(callbackPrincipalDelScript);
}
else if (wAttempts >= wMaxAttempts) // Si no se ha cargado la API de Waze después de 20 segundos
{
clearInterval(wInterval);
callbackPrincipalDelScript();
}
}, 500);
}//waitforWazeAPI
//+++++++++Funciones de Georeferenciación y Distancia++++++++++++++
// Función para calcular la distancia en metros entre dos puntos geográficos (latitud, longitud)
function calculateDistance(lat1, lon1, lat2, lon2)
{
const earthRadiusMeters = 6371e3; // Radio de la Tierra en metros
// Convertir latitudes y diferencias de longitudes de grados a radianes
const lat1Rad = lat1 * Math.PI / 180;// Convertir latitud 1 a radianes
const lat2Rad = lat2 * Math.PI / 180;// Convertir latitud 2 a radianes
const deltaLatRad = (lat2 - lat1) * Math.PI / 180;// Convertir diferencia de latitudes a radianes
const deltaLonRad = (lon2 - lon1) * Math.PI / 180;// Convertir diferencia de longitudes a radianes
// Fórmula de Haversine para calcular la distancia entre dos puntos en una esfera
// a = sin²(Δlat/2) + cos(lat1) * cos(lat2) * sin²(Δlon/2)
const a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);//
// c = 2 * atan2(√a, √(1−a))
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// Calcular la distancia final en metros
const distanceMeters = earthRadiusMeters * c;
return distanceMeters;
}//calculateDistance
//Función para obtener coordenadas de un lugar
function getPlaceCoordinates(venueOldModel, venueSDK)
{
let lat = null;
let lon = null;
const placeId = venueOldModel ? venueOldModel.getID() : 'N/A';
// Priorizar SDK si tiene 'geometry.coordinates' (lat/lon como array [lon, lat])
if (venueSDK && venueSDK.geometry && Array.isArray(venueSDK.geometry.coordinates) && venueSDK.geometry.coordinates.length === 2)
{
lon = venueSDK.geometry.coordinates[0];
lat = venueSDK.geometry.coordinates[1];
if (typeof lat === 'number' && typeof lon === 'number' && Math.abs(lat) <= 90 && Math.abs(lon) <= 180)
{
// console.log(`[WME_PLN_COORD] Coords from SDK (geometry.coordinates) for ID ${placeId}: ${lat}, ${lon}`);
return { lat, lon };
}
else
{
console.warn(`[WME_PLN_COORD] SDK geometry.coordinates tiene valores no geográficos o inválidos para ID ${placeId}. Coords: [${lon}, ${lat}]`);
lat = null;
lon = null;
}
}
// Si no se obtuvieron del SDK o eran inválidas, intentar con 'geometry.centroid' del SDK
if (venueSDK && venueSDK.geometry && venueSDK.geometry.centroid)
{
if (typeof venueSDK.geometry.centroid.lat === 'number' && typeof venueSDK.geometry.centroid.lon === 'number')
{
lat = venueSDK.geometry.centroid.lat;
lon = venueSDK.geometry.centroid.lon;
// console.log(`[WME_PLN_COORD] Coords from SDK (geometry.centroid) for ID ${placeId}: ${lat}, ${lon}`);
return { lat, lon };
}
else
{
console.warn(`[WME_PLN_COORD] SDK geometry.centroid lacks numerical lat/lon for ID ${placeId}. Centroid:`, venueSDK.geometry.centroid);
lat = null;
lon = null;
}
}
// Fallback a OpenLayers Geometry (modelo antiguo) si SDK no proporcionó o fue inválido
// Añadiendo conversión de coordenadas de proyección.
if ((lat === null || lon === null) && venueOldModel && typeof venueOldModel.getOLGeometry === 'function')
{
try
{
const geometry = venueOldModel.getOLGeometry();
if (geometry && typeof geometry.getCentroid === 'function')
{
const centroid = geometry.getCentroid();
if (centroid && typeof centroid.x === 'number' && typeof centroid.y === 'number')
{
// Convertir de la proyección de Waze (Spherical Mercator) a WGS84 (Lat/Lon)
// Waze usa EPSG:3857 (Spherical Mercator) para sus coordenadas de mapa interno.
// La función OL.Projection.transform es la clave aquí.
// OpenLayers.Projection debe estar disponible globalmente.
if (typeof OpenLayers !== 'undefined' && OpenLayers.Projection)
{
const mercatorPoint = new OpenLayers.Geometry.Point(centroid.x, centroid.y);
const wgs84Point = mercatorPoint.transform(
new OpenLayers.Projection("EPSG:3857"), // Fuente: Spherical Mercator
new OpenLayers.Projection("EPSG:4326") // Destino: WGS84 Lat/Lon
);
lat = wgs84Point.y;
lon = wgs84Point.x;
if (typeof lat === 'number' && typeof lon === 'number' && Math.abs(lat) <= 90 && Math.abs(lon) <= 180)
{
// console.log(`[WME_PLN_COORD] Coords from Old Model (Converted OLGeometry) for ID ${placeId}: ${lat}, ${lon}`);
return { lat, lon };
}
else
{
console.warn(`[WME_PLN_COORD] OLGeometry().getCentroid() (after conversion) resulted in invalid lat/lon for ID ${placeId}. Converted: ${lat}, ${lon}`);
lat = null;
lon = null;
}
}
else
{
console.warn(`[WME_PLN_COORD] OpenLayers.Projection no está disponible para la conversión de coordenadas para ID ${placeId}.`);
lat = centroid.y; // Si no hay conversión, se usan las originales (que fallarán la validación)
lon = centroid.x;
}
}
else
{
console.warn(`[WME_PLN_COORD] getOLGeometry().getCentroid() returned invalid/null centroid or missing x/y for ID ${placeId}. Centroid:`, centroid);
}
}
else
{
console.warn(`[WME_PLN_COORD] getOLGeometry() or getCentroid() method not available for ID ${placeId}. Geometry:`, geometry);
}
}
catch (e)
{
console.error(`[WME_PLN_COORD] Error calling getOLGeometry() or transforming for ID ${placeId}:`, e);
}
}
// Log final si las coordenadas siguen siendo inválidas o indefinidas.
if (lat === null || lon === null || isNaN(lat) || isNaN(lon))
{
console.warn(`[WME_PLN_COORD] No se pudieron obtener coordenadas válidas FINALES para el lugar ID: ${placeId}. Lat: ${lat}, Lon: ${lon}`);
return { lat: null, lon: null };
}
return { lat, lon };
}//getPlaceCoordinates
// Nueva función robusta para agregar datos para verificación de duplicados
function addPlaceDataForDuplicateCheck(venue, venueSDK, normalizedName)
{
// Usa una variable global para almacenar los datos de lugares para comparar duplicados
if (typeof window.duplicatePlacesData === "undefined") window.duplicatePlacesData = [];
const duplicatePlacesData = window.duplicatePlacesData;
let geometry = null;
if (venueSDK && venueSDK.geometry && venueSDK.geometry.coordinates)
{
const [lon, lat] = venueSDK.geometry.coordinates;
duplicatePlacesData.push({ name: normalizedName, lat, lon, venueId: venueSDK.id });
return;
}
else if (venue && typeof venue.getOLGeometry === 'function')
{
geometry = venue.getOLGeometry();
if (geometry)
{
const lonLat = geometry.getCoordinates();
const lon = lonLat[0];
const lat = lonLat[1];
duplicatePlacesData.push({ name: normalizedName, lat, lon, venueId: venue.getID() });
return;
}
}
console.warn("No se pudo obtener geometría válida para el lugar:", venue, venueSDK);
}
// Función para detectar nombres duplicados cercanos y generar alertas
function detectAndAlertDuplicateNames(allScannedPlacesData)
{
const DISTANCE_THRESHOLD_METERS = 50; // Umbral de distancia para considerar "cerca" (en metros)
const duplicatesGroupedForAlert = new Map(); // Almacenará {normalizedName: [{places}, {places}]}
// Paso 1: Agrupar por nombre NORMALIZADO y encontrar duplicados cercanos
allScannedPlacesData.forEach(p1 => {
if (p1.lat === null || p1.lon === null) return; // Saltar si no tiene coordenadas
// Buscar otros lugares con el mismo nombre normalizado
const nearbyMatches = allScannedPlacesData.filter(p2 => {
if (p2.id === p1.id || p2.lat === null || p2.lon === null || p1.normalized !== p2.normalized) {
return false;
}
const distance = calculateDistance(p1.lat, p1.lon, p2.lat, p2.lon);
return distance <= DISTANCE_THRESHOLD_METERS;
});
if (nearbyMatches.length > 0) {
// Si encontramos duplicados cercanos para p1, agruparlos
const groupKey = p1.normalized.toLowerCase();
if (!duplicatesGroupedForAlert.has(groupKey)) {
duplicatesGroupedForAlert.set(groupKey, new Set());
}
duplicatesGroupedForAlert.get(groupKey).add(p1); // Añadir p1
nearbyMatches.forEach(p => duplicatesGroupedForAlert.get(groupKey).add(p)); // Añadir todos sus duplicados
}
});
// Paso 2: Generar el mensaje de alerta final
if (duplicatesGroupedForAlert.size > 0)
{
let totalNearbyDuplicateGroups = 0; // Para contar la cantidad de "nombres" con duplicados
const duplicateEntriesHtml = []; // Para almacenar las líneas HTML de la alerta formateadas
duplicatesGroupedForAlert.forEach((placesSet, normalizedName) => {
const uniquePlacesInGroup = Array.from(placesSet); // Convertir Set a Array
if (uniquePlacesInGroup.length > 1) { // Solo si realmente hay más de un lugar en el grupo
totalNearbyDuplicateGroups++;
// Obtener los números de línea para cada lugar en este grupo
const lineNumbers = uniquePlacesInGroup.map(p => {
const originalPlaceInInconsistents = allScannedPlacesData.find(item => item.id === p.id);
return originalPlaceInInconsistents ? (allScannedPlacesData.indexOf(originalPlaceInInconsistents) + 1) : 'N/A';
}).filter(num => num !== 'N/A').sort((a, b) => a - b); // Asegurarse que son números y ordenarlos
// Marcar los lugares en `allScannedPlacesData` para el `⚠️` visual
uniquePlacesInGroup.forEach(p => {
const originalPlaceInInconsistents = allScannedPlacesData.find(item => item.id === p.id);
if (originalPlaceInInconsistents) {
originalPlaceInInconsistents.isDuplicate = true;
}
});
// Construir la línea para el modal
duplicateEntriesHtml.push(`
<div style="margin-bottom: 5px; font-size: 15px; text-align: left;">
<b>${totalNearbyDuplicateGroups}.</b> Nombre: <b>${normalizedName}</b>
<span style="font-weight: bold; color: #007bff;">[Registro${lineNumbers.join("],[Registro")}]</span>
</div>
`);
}
});
// Solo mostrar la alerta si realmente hay grupos de más de 1 duplicado cercano
if (duplicateEntriesHtml.length > 0) {
// Crear el modal
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"; // Z-INDEX ALTO para asegurar que esté encima
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 = "400px";
modal.style.maxWidth = "600px";
modal.style.maxHeight = "80vh"; // Para scroll si hay muchos duplicados
modal.style.overflowY = "auto"; // Para scroll si hay muchos duplicados
// Ícono visual
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️"; // Signo de advertencia
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
modal.appendChild(iconElement);
// Mensaje principal
const messageTitle = document.createElement("div");
messageTitle.innerHTML = `<b>¡Atención! Se encontraron ${duplicateEntriesHtml.length} nombres duplicados.</b>`;
messageTitle.style.fontSize = "20px";
messageTitle.style.marginBottom = "8px";
modal.appendChild(messageTitle);
const messageExplanation = document.createElement("div");
messageExplanation.textContent = `Los siguientes grupos de lugares se encuentran a menos de ${DISTANCE_THRESHOLD_METERS}m uno del otro. El algoritmo asume que son el mismo lugar, por favor revisa los registros indicados en el panel flotante:`;
messageExplanation.style.fontSize = "15px";
messageExplanation.style.color = "#555";
messageExplanation.style.marginBottom = "18px";
messageExplanation.style.textAlign = "left"; // Alinear texto explicativo a la izquierda
modal.appendChild(messageExplanation);
// Lista de duplicados
const duplicatesListDiv = document.createElement("div");
duplicatesListDiv.style.textAlign = "left"; // Alinear la lista a la izquierda
duplicatesListDiv.style.paddingLeft = "10px"; // Pequeño padding para los números
duplicatesListDiv.innerHTML = duplicateEntriesHtml.join('');
modal.appendChild(duplicatesListDiv);
// Botón OK
const buttonWrapper = document.createElement("div");
buttonWrapper.style.display = "flex";
buttonWrapper.style.justifyContent = "center";
buttonWrapper.style.gap = "18px";
buttonWrapper.style.marginTop = "20px"; // Espacio superior
const okBtn = document.createElement("button");
okBtn.textContent = "OK";
okBtn.style.padding = "7px 18px";
okBtn.style.background = "#007bff";
okBtn.style.color = "#fff";
okBtn.style.border = "none";
okBtn.style.borderRadius = "4px";
okBtn.style.cursor = "pointer";
okBtn.style.fontWeight = "bold";
okBtn.addEventListener("click", () => modal.remove()); // Cierra el modal
buttonWrapper.appendChild(okBtn);
modal.appendChild(buttonWrapper);
document.body.appendChild(modal); // Añadir el modal al body
}
}
}
//+++++++++FIN Funciones de Georeferenciación y Distancia++++++++++++++
// Permite crear un panel flotante en WME
function updateScanProgressBar(currentIndex, totalPlaces)
{
if (totalPlaces === 0) // Si no hay lugares, no actualiza la barra de progreso
return;
let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100); // Calcular el porcentaje de progreso
progressPercent = Math.min(progressPercent, 100);
const progressBarInnerTab = document.getElementById("progressBarInnerTab"); // Actualizar la barra de progreso
const progressBarTextTab = document.getElementById("progressBarTextTab"); // Actualizar el texto de la barra de progreso
if (progressBarInnerTab && progressBarTextTab) // Asegurarse de que los elementos existen antes de intentar actualizarlos
{
progressBarInnerTab.style.width = `${progressPercent}%`; // Actualizar el ancho de la barra de progreso
const currentDisplay = Math.min(currentIndex + 1, totalPlaces); // Mostrar el número actual de lugares procesados
progressBarTextTab.textContent = `Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`; // Actualizar el texto de la barra de progreso
}
}//updateScanProgressBar
// Permite crear un panel flotante en WME
function escapeRegExp(string)
{
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}//escapeRegExp
// Función para cargar palabras del diccionario desde Google Sheets (Hoja "Dictionary")
async function loadDictionaryWordsFromSheet()
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = "Dictionary!A2:B";
// Asegurarse de que window.dictionaryWords y window.dictionaryIndex estén inicializados
if (!window.dictionaryWords) window.dictionaryWords = new Set(); // Usar Set para evitar duplicados
if (!window.dictionaryIndex) window.dictionaryIndex = {}; // Índice para palabras por primera letra
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`; // URL para acceder a la hoja de Google Sheets
return new Promise((resolve) => { // Promise para manejar la carga asíncrona
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY")
{
console.warn('[WME PLN] SPREADSHEET_ID o API_KEY no configurados para el diccionario. Se omitirá la carga desde Google Sheets.');
resolve();
return;
}
console.log('[WME PLN] Cargando palabras del diccionario desde Google Sheets...');
GM_xmlhttpRequest(
{
method: "GET",
url: url,
onload: function (response)
{
if (response.status >= 200 && response.status < 300)
{
const data = JSON.parse(response.responseText);
let newWordsAdded = 0;
if (data.values)
{
data.values.forEach(row => { // Procesar cada fila de la hoja
const word = (row[0] || '').trim(); // Columna A es WORD
if (word && !window.dictionaryWords.has(word.toLowerCase()))// Verificar si la palabra ya está en el Set
{
window.dictionaryWords.add(word.toLowerCase()); // Añadir a Set en minúsculas
const firstChar = word.charAt(0).toLowerCase();
if (!window.dictionaryIndex[firstChar])
window.dictionaryIndex[firstChar] = [];
// Asegurarse de que el índice existe para la primera letra
window.dictionaryIndex[firstChar].push(word.toLowerCase()); // Añadir al índice
newWordsAdded++;
}
});
//console.log(`[WME PLN] ¡Éxito! Diccionario cargado desde Google Sheets. ${newWordsAdded} palabras nuevas añadidas.`);
try// Guardar el diccionario actualizado en localStorage
{
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando diccionario actualizado en localStorage:", e);
}
}
else
console.warn('[WME PLN] No se encontraron valores en la hoja "Dictionary".');
}
else
{
const error = JSON.parse(response.responseText);
console.error(`[WME PLN] Error al cargar la hoja "Dictionary" de Google: ${error.error.message}.`);
}
resolve();
},
onerror: function ()
{
console.error('Error de red al intentar conectar con Google Sheets para el diccionario.');
resolve();
}
});
});
}//loadDictionaryWordsFromSheet
//Función Para Cargar Categorías Desde Google Sheets
async function loadDynamicCategoriesFromSheet()
{
const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = "Categories!A2:E";
window.dynamicCategoryRules = []; // Definimos la variable global para guardar las reglas
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
return new Promise((resolve) => { // Verificamos si la variable global ya está definida
if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") // Medida de seguridad: si no has puesto tus datos, no intenta la conexión.
{
console.warn('[WME PLN] No se ha configurado SPREADSHEET_ID o API_KEY. Se omitirá la carga de categorías dinámicas.');
resolve(); // Permite que el script continúe sin las reglas.
return;
}
//console.log('[WME PLN] Cargando reglas de categoría dinámicas desde Google Sheets...');
GM_xmlhttpRequest({method: "GET", url: url, onload: function (response)
{
if (response.status >= 200 && response.status < 300) // Verifica si la respuesta fue exitosa
{
const data = JSON.parse(response.responseText);
if (data.values) // Verifica si hay valores en la hoja
{
window.dynamicCategoryRules = data.values.map(row => { // Procesa los datos y los guarda en la variable global
const keyword = (row[0] || '').toLowerCase().trim();
const keywords = keyword.split(';').map(k => k.trim()).filter(k => k.length > 0);
const regexParts = keywords.map(k => `\\b${escapeRegExp(k)}\\b`);
const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
return {
keyword: keyword, // Mantener original si es necesario
categoryKey: row[1] || '',
icon: row[2] || '⚪',
desc_es: row[3] || 'Sin descripción',
desc_en: row[4] || 'No description',
compiledRegex: combinedRegex // Guarda la regex pre-compilada
};
});
window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length); // Una vez cargadas, ordena las reglas UNA SOLA VEZ
console.log('[WME PLN] ¡Éxito! Reglas de categoría dinámicas cargadas y ordenadas:', window.dynamicCategoryRules);
}
else
{
console.warn('[WME PLN] No se encontraron valores en la hoja de categorías.');
}
}
else
{
const error = JSON.parse(response.responseText);
alert(`Error al cargar la hoja de Google: ${error.error.message}.`);
}
resolve();
}, onerror: function ()
{
alert('Error de red al intentar conectar con Google Sheets.');
resolve();
}
});// GM_xmlhttpRequest
});
}//loadDynamicCategoriesFromSheet
// Función para encontrar la categoría de un lugar basado en su nombre
function findCategoryForPlace(placeName)
{
if (!placeName || typeof placeName !== 'string' || !window.dynamicCategoryRules || window.dynamicCategoryRules.length === 0) // Si el nombre del lugar es inválido o no hay reglas de categoría cargadas, devuelve un array vacío de sugerencias.
return [];
const lowerCasePlaceName = placeName.toLowerCase();// Convertir el nombre del lugar a minúsculas para comparaciones insensibles a mayúsculas
const allMatchingRules = []; // Este array almacenará todas las reglas de categoría que coincidan.
const placeWords = lowerCasePlaceName.split(/\s+/).filter(w => w.length > 0); // Descomponer el nombre del lugar en palabras
const SIMILARITY_THRESHOLD_FOR_KEYWORDS = 0.95; // Puedes ajustar este umbral (ej. 0.90 para 90% de similitud)
// PASO 0: Normalizar el nombre del lugar eliminando diacríticos y caracteres especiales
for (const rule of window.dynamicCategoryRules)
{
if (!rule.compiledRegex) continue; // Si la regla no tiene una expresión regular compilada (lo cual no debería pasar si se cargó correctamente), salta a la siguiente regla.
// **PASO 1: Búsqueda por Regex Exacta
if (rule.compiledRegex.test(lowerCasePlaceName))
{
if (!allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey)) {
allMatchingRules.push(rule);
}
// Si Ya Añadimos La Regla Por Regex Exacta, Pasar A La Siguiente Regla Para Ahorrar Cálculos De Similitud
continue;
}
// **PASO 2: Búsqueda por Similitud para CADA palabra del lugar vs CADA palabra clave de la regla**
const ruleKeywords = rule.keyword.split(';').map(k => k.trim().toLowerCase()).filter(k => k.length > 0); // Descomponer la 'keyword' de la regla en sus palabras individuales (si usa ';')
let foundSimilarityForThisRule = false; // Bandera para saber si ya encontramos una buena similitud para esta regla, para no seguir buscando más palabras clave de la regla.
for (const pWord of placeWords) // Cada palabra del nombre del lugar
{ // Cada palabra del nombre del lugar
if (foundSimilarityForThisRule) break; // Si ya encontramos una buena similitud para esta regla, pasamos a la siguiente.
for (const rKeyword of ruleKeywords)
{ // Cada palabra clave de la regla
// Asegurarse de que rKeyword no sea una expresión regular, sino la palabra literal para Levenshtein
const similarity = calculateSimilarity(pWord, rKeyword); // Calcular la similitud entre la palabra del lugar y la palabra clave de la regla
if (similarity >= SIMILARITY_THRESHOLD_FOR_KEYWORDS && !allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey)) // Si la similitud es alta y aún no hemos añadido esta categoría
{
allMatchingRules.push(rule);
foundSimilarityForThisRule = true; // Marcamos que ya la encontramos para esta regla
break; // Salimos del bucle de rKeyword y pWord
}
}
}
}
//console.log(`[WME PLN DEBUG] findCategoryForPlace para "${placeName}" devolvió: `, allMatchingRules);
return allMatchingRules;
}//findCategoryForPlace
// Permite obtener el icono de una categoría
function getWazeLanguage()
{
// 1. Intento principal con el SDK (método recomendado)
if (wmeSDK && typeof wmeSDK.getWazeLocale === 'function')
{
const locale = wmeSDK.getWazeLocale(); // ej: 'es-419'
if (locale)
return locale.split('-')[0].toLowerCase(); // -> 'es'
}
// 2. Fallback al objeto global 'W' si el SDK falla
if (typeof W !== 'undefined' && W.locale)
return W.locale.split('-')[0].toLowerCase();
// 3. Último recurso si nada funciona
return 'es';
}//getWazeLanguage
//Permite obtener el icono y descripción de una categoría
function getCategoryDetails(categoryKey)
{
const lang = getWazeLanguage();
// 1. Intento con la hoja de Google (window.dynamicCategoryRules)
if (window.dynamicCategoryRules && window.dynamicCategoryRules.length > 0)
{
const rule = window.dynamicCategoryRules.find(r => r.categoryKey.toUpperCase() === categoryKey.toUpperCase());
if (rule)
{
const description = (lang === 'es' && rule.desc_es) ? rule.desc_es : rule.desc_en;
return { icon: rule.icon, description: description };
}
}
// 2. Fallback a la lista interna del script si no se encontró en la hoja
const hardcodedInfo = getCategoryIcon(categoryKey); // Llama a la función original
if (hardcodedInfo && hardcodedInfo.icon !== '⚪' && hardcodedInfo.icon !== '❓')
{
// La función original devuelve un título "Español / English", lo separamos.
const descriptions = hardcodedInfo.title.split(' / ');
const description = (lang === 'es' && descriptions[0]) ? descriptions[0] : descriptions[1] || descriptions[0];
return { icon: hardcodedInfo.icon, description: description };
}
// 3. Si no se encuentra en ninguna parte, devolver un valor por defecto.
const defaultDescription = lang === 'es' ? `Categoría no encontrada (${categoryKey})` : `Category not found (${categoryKey})`;
return { icon: '⚪', description: defaultDescription };
}//getCategoryDetails
// Función para eliminar diacríticos de una cadena
function removeDiacritics(str)
{
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}//removeDiacritics
// Función para validar una palabra excluida
function isValidExcludedWord(newWord)
{
if (!newWord) // Si la palabra está vacía, no es válida
return { valid : false, msg : "La palabra no puede estar vacía." };
const lowerNewWord = newWord.toLowerCase(); // Convertir a minúsculas para comparaciones insensibles a mayúsculas
if (newWord.length === 1) // No permitir palabras de un solo caracter
return { valid: false, msg: "No se permite agregar palabras de un solo caracter." };
if (/[-']/.test(newWord)) // Permitir palabras con "-" o "'" sin separarlas
return { valid: true };
if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord)) // No permitir caracteres especiales solos
return { valid : false, msg : "No se permite agregar solo caracteres especiales." };
if (window.dictionaryWords && Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord)) // No permitir si ya está en el diccionario (ignorando mayúsculas)
return { valid : false, msg :"La palabra ya existe en el diccionario. No se puede agregar a especiales." };
if (commonWords.includes(lowerNewWord)) // No permitir palabras comunes
return { valid : false, msg : "Esa palabra es muy común y no debe agregarse a la lista." };
if (excludedWords && Array.from(excludedWords).some(w => w.toLowerCase() === lowerNewWord)) // No permitir duplicados en excluidas
return { valid : false, msg : "La palabra ya está en la lista (ignorando mayúsculas)." };
return { valid : true };
}//isValidExcludeWord
// La función removeEmoticons con una regex más segura o un paso extraremoveEmoticons solo para emojis (sin afectar números)
function removeEmoticons(text)
{
if (!text || typeof text !== 'string') // Si el texto es nulo o no es una cadena, retorna vacío
return '';
const specificEmojiAndSymbolRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\udff5]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udf00-\udfff]|\ud83d[\udc00-\udfff]|\ud83e[\udc00-\udfff]|[\u2600-\u26FF]\ufe0f?)/gu; // Regex para eliminar emojis y símbolos específicos
let cleanedText = text.replace(specificEmojiAndSymbolRegex, ''); // Elimina emojis y símbolos específicos
cleanedText = cleanedText.replace(/\uD83D\uDD0B/g, ''); // Unicode para 🔋 (U+1F50B)
return cleanedText.trim().replace(/\s{2,}/g, ' ');
}// removeEmoticons
// Modify aplicarReemplazosGenerales
function aplicarReemplazosGenerales(name)
{
if (typeof window.skipGeneralReplacements === "boolean" && window.skipGeneralReplacements)
return name;
// Paso 1: Eliminar emoticones al inicio de los reemplazos generales.
name = removeEmoticons(name);
const reglas = [
// Nueva regla: reemplazar | por espacio, guion y espacio
{ buscar: /\|/g, reemplazar: " - " },
// Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor
{ buscar: /\s*\/\s*/g, reemplazar: " / " },
// Corrección: Para buscar [P] o [p] literalmente
{ buscar: /\[[Pp]\]/g, reemplazar: "" },
// Solo añade espacios alrededor del guion si no hay un carácter de letra/número directamente adyacente.
// Esto significa que "i-Store" se mantendrá, pero "Palabra - Otra" seguirá siendo "Palabra - Otra".
{ buscar: /(?<=[^a-zA-Z0-9])-(?=[^a-zA-Z0-9])/g, reemplazar: " - " }, // Reemplaza - solo si no está entre letras/números
{ buscar: /(?<=[a-zA-Z0-9])-(?=[^a-zA-Z0-9])/g, reemplazar: "-" }, // Mantiene si la primera parte es letra/número y no hay espacio después
{ buscar: /(?<=[^a-zA-Z0-9])-(?=[a-zA-Z0-9])/g, reemplazar: "-" }, // Mantiene si la segunda parte es letra/número y no hay espacio antes
];
reglas.forEach(regla => { // Itera sobre cada regla de reemplazo
if (regla.buscar.source === '\\|') // Si la regla es para el carácter '|', usa replaceAll
name = name.replace(regla.buscar, regla.reemplazar);
else
name = name.replace(regla.buscar, regla.reemplazar);
});
name = name.replace(/\s{2,}/g, ' ').trim(); // Asegura el recorte final y espacios únicos
return name;
}// aplicarReemplazosGenerales
//Nueva función auxiliar para capitalizar cada palabra en una frase
function capitalizeEachWord(phrase)
{
if (!phrase || typeof phrase !== 'string') return "";
return phrase.split(/\s+/) // Dividir por uno o más espacios
.map(word => {
if (word.length === 0) return "";
// Aplicar lógica similar a normalizePlaceName para cada palabra
// Asegurarse de que no sea un número romano o palabra especial que deba ir en mayúsculas
const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
if (romanRegexStrict.test(word))
return word.toUpperCase();
// Puedes añadir aquí otras reglas específicas que apliques a cada palabra
// Por ejemplo, mantener acrónimos en mayúsculas si es lo que deseas.
if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && (word.includes('.') || /^[A-ZÁÉÍÓÚÑ]+$/.test(word)))
return word; // Mantener como está si es un acrónimo/abreviatura
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(' '); // Unir de nuevo con un solo espacio
}// capitalizeEachWord
//Permite aplicar reglas especiales de capitalización y puntuación a un nombre
function aplicarReglasEspecialesNombre(newName)
{
newName = newName.replace(/([A-Za-z])'([A-Za-z])/g, (match, before, after) => `${before}'${after.toLowerCase()}`); // Asegura que las letras después de un apóstrofe estén en minúsculas
newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); // Asegura que las letras después de un guion estén en mayúsculas
newName = newName.replace(/\.\s+([a-z])/g, (match, letter) => `. ${letter.toUpperCase()}`); // Asegura que las letras después de un punto estén en mayúsculas
//Capitalizar Después De Paréntesis De Apertura
newName = newName.replace(/(\(\s*)([a-z])/g, (match, P1, P2) => {
// P1 es el paréntesis de apertura y cualquier espacio después (ej. "( " o "(")
// P2 es la primera letra minúscula después de eso.
return P1 + P2.toUpperCase();
}
);
newName = newName.replace(/\s([a-zA-Z])$/, (match, letter) => ` ${letter.toUpperCase()}`); // Asegura que la última letra de una cadena esté en mayúsculas si es una letra sola al final
//console.log("[DEBUG aplicarReglasEspecialesNombre] Before hyphen capitalization:", newName);
newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); // Asegura que las letras después de un guion estén en mayúsculas
//console.log("[DEBUG aplicarReglasEspecialesNombre] After hyphen capitalization:", newName);
return newName;
}//aplicarReglasEspecialesNombre
// Permite normalizar un nombre de lugar
function processPlaceName(originalName)
{
let processedName = originalName.trim(); // Eliminar espacios al inicio y al final del nombre original
// Paso 1: Normalizar palabras
const words = processedName.split(/\s+/).map(word => normalizePlaceName(word));
processedName = words.join(" ");// Unir las palabras normalizadas con un espacio
// Paso 2: Aplicar reglas especiales (paréntesis, comillas, etc.)
processedName = aplicarReglasEspecialesNombre(processedName);
// Paso 3: Post-procesamiento de comillas y paréntesis
processedName = postProcessQuotesAndParentheses(processedName);
// Paso 4: Aplicar reemplazos definidos por el usuario (AL FINAL)
if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0)
processedName = aplicarReemplazosDefinidos(processedName, replacementWords);
return processedName.trim();
}//processPlaceName
//Permite aplicar reemplazos definidos por el usuario a un texto
function aplicarReemplazosDefinidos(text, replacementRules)
{
let newText = text;
if (typeof replacementRules !== 'object' || replacementRules === null || Object.keys(replacementRules).length === 0) // Verificar si replacementRules es un objeto y tiene claves
return newText; // No hay reglas o no es un objeto, devolver el texto original
// Ordenar las claves de reemplazo por longitud descendente para manejar correctamente
// los casos donde una clave puede ser subcadena de otra (ej. "D1 Super" y "D1").
const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);
for (const fromKey of sortedFromKeys)
{// Asegurarse de que fromKey es una cadena
const toValue = replacementRules[fromKey];
const escapedFromKey = escapeRegExp(fromKey);
// Regex mejorada para definir "palabra completa" usando propiedades Unicode.
// Grupo 1: Captura el delimitador previo (un no-palabra o inicio de cadena).
// Grupo 2: Captura la clave de reemplazo que coincidió.
// Grupo 3: Captura el delimitador posterior o fin de cadena.
const regex = new RegExp(`(^|[^\\p{L}\\p{N}_])(${escapedFromKey})($|(?=[^\\p{L}\\p{N}_]))`, 'giu'); // 'g' para global, 'i' para insensible a mayúsculas, 'u' para Unicode, 'u' para Unicode
newText = newText.replace(regex, (
match, // La subcadena coincidente completa
delimitadorPrevio, // Contenido del grupo de captura 1
matchedKey, // Contenido del grupo de captura 2
_delimitadorPosteriorOculto, // Contenido del grupo de captura 3 (no usado directamente en la lógica de abajo, pero necesario para alinear parámetros)
offsetOfMatchInCurrentText, // El desplazamiento de la subcadena coincidente
stringBeingProcessed // La cadena completa que se está examinando
) => {
if (typeof stringBeingProcessed !== 'string')
{// Asegurarse de que stringBeingProcessed es una cadena
//console.error("[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'stringBeingProcessed' (la cadena original en el reemplazo) no es una cadena.","Tipo:", typeof stringBeingProcessed, "Valor:", stringBeingProcessed, "Regla actual (fromKey):", fromKey, "Texto parcial procesado:", newText);
return match;
}
if (typeof offsetOfMatchInCurrentText !== 'number')
{ // Asegurarse de que offsetOfMatchInCurrentText es un número
//console.error("[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'offsetOfMatchInCurrentText' no es un número.", "Tipo:", typeof offsetOfMatchInCurrentText, "Valor:", offsetOfMatchInCurrentText);
return match;
}
const fromKeyLower = fromKey.toLowerCase(); // Convertir la clave de reemplazo a minúsculas para comparaciones insensibles a mayúsculas
const toValueLower = toValue.toLowerCase(); // Convertir el valor de reemplazo a minúsculas para comparaciones insensibles a mayúsculas
const indexOfFromInTo = toValueLower.indexOf(fromKeyLower); // Buscar la posición de fromKeyLower dentro de toValueLower para verificar si ya existe en el texto original
if (indexOfFromInTo !== -1)
{// Si fromKeyLower está presente en toValueLower, verificar si ya existe en el texto original
const actualMatchedKeyOffset = offsetOfMatchInCurrentText + (delimitadorPrevio ? delimitadorPrevio.length : 0); // El offset real de matchedKey dentro de stringBeingProcessed
const potentialExistingToStart = actualMatchedKeyOffset - indexOfFromInTo; // Calcular el índice de inicio potencial del substring toValueLower en stringBeingProcessed
if (potentialExistingToStart >= 0 && (potentialExistingToStart + toValue.length) <= stringBeingProcessed.length)
{// Verificar que el índice de inicio y fin del substring están dentro de los límites de la cadena original
const substringInOriginal = stringBeingProcessed.substring(potentialExistingToStart, potentialExistingToStart + toValue.length);
if (substringInOriginal.toLowerCase() === toValueLower) // Si el substring en la cadena original coincide con toValueLower, significa que ya existe
return match;
}
}
// Evitar Duplicación De Palabras En Los Bordes
const palabrasDelToValue = toValue.trim().split(/\s+/).filter(p => p.length >0); // Filtrar palabras vacías
if (palabrasDelToValue.length > 0)
{// Si hay palabras en toValue, proceder a verificar duplicados
const primeraPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[0].toLowerCase());
const ultimaPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[palabrasDelToValue.length - 1].toLowerCase());
// Palabra ANTES del inicio del 'match' (delimitadorPrevio + matchedKey)
// El offsetOfMatchInCurrentText es el inicio de 'match', no de 'delimitadorPrevio' si son diferentes.
// Necesitamos el texto ANTES de delimitadorPrevio. El offset real del inicio del match completo es offsetOfMatchInCurrentText.
const textoAntesDelMatch = stringBeingProcessed.substring(0, offsetOfMatchInCurrentText); // El texto antes del match completo
const palabrasEnTextoAntes = textoAntesDelMatch.trim().split(/\s+/).filter(p => p.length > 0); // Filtrar palabras vacías
const palabraAnteriorLimpia = palabrasEnTextoAntes.length > 0 ? removeDiacritics(palabrasEnTextoAntes[palabrasEnTextoAntes.length - 1].toLowerCase()) : ""; // La última palabra antes del match completo, si existe
// Palabra DESPUÉS del final del 'match' (delimitadorPrevio + matchedKey)
const textoDespuesDelMatch = stringBeingProcessed.substring(offsetOfMatchInCurrentText + match.length); // El texto después del match completo
const palabrasEnTextoDespues = textoDespuesDelMatch.trim().split(/\s+/).filter(p => p.length > 0); // Filtrar palabras vacías
const palabraSiguienteLimpia = palabrasEnTextoDespues.length > 0 ? removeDiacritics(palabrasEnTextoDespues[0].toLowerCase()) : ""; // La primera palabra después del match completo, si existe
if (palabraAnteriorLimpia && primeraPalabraToValueLimpia && palabraAnteriorLimpia === primeraPalabraToValueLimpia)
{// Delimitador posterior oculto: lo que sigue inmediatamente al matchedKey
if (delimitadorPrevio.trim() === "" || delimitadorPrevio.match(/^\s+$/)) // Solo prevenir si el delimitador previo es solo espacio o vacío, indicando adyacencia real de palabras.
return match;
}
if (palabraSiguienteLimpia && ultimaPalabraToValueLimpia && ultimaPalabraToValueLimpia === palabraSiguienteLimpia)
{//Evitar duplicación de la última palabra del toValue
// El delimitadorPosteriorOculto nos dice qué sigue inmediatamente al matchedKey.
// Si este delimitador es solo espacio o vacío, y luego viene la palabra duplicada.
if (_delimitadorPosteriorOculto.trim() === "" || _delimitadorPosteriorOculto.match(/^\s+$/))
return match;
}
}
return delimitadorPrevio + toValue;
});
}
return newText;
}//aplicarReemplazosDefinidos
//Permite crear un panel flotante en WME
function getVisiblePlaces()
{
if (typeof W === 'undefined' || !W.map || !W.model || !W.model.venues)
{// Si Waze Map Editor no está completamente cargado, retornar un array vacío
console.warn('Waze Map Editor no está completamente cargado.');
return [];
}
// Obtener los lugares visibles en el mapa
const venues = W.model.venues.objects;
const visiblePlaces = Object.values(venues).filter(venue => { // Filtrar los lugares que están visibles en el mapa
const olGeometry = venue.getOLGeometry?.();// Obtener la geometría del lugar
const bounds = olGeometry?.getBounds?.(); // Obtener los límites del lugar
return bounds && W.map.getExtent().intersectsBounds(bounds);
});
return visiblePlaces;
}// getVisiblePlaces
//Permite renderizar los lugares en el panel flotante
function renderPlacesInFloatingPanel(places)
{
// Limpiar la lista global de duplicados antes de llenarla de nuevo
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
if (places.length > maxPlacesToScan) // Limitar el número de lugares a escanear
places = places.slice(0, maxPlacesToScan); // Limitar el número de places a escanear
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
// console.log(`[CATEGORÍA] Usando nombre de categoría de ${source}: ${categoryName} ${categoryId ? `(ID: ${categoryId})` : ''}`); // Comentario de depuración eliminado
return categoryName;
}
// Intento 2: Usar W.model si no se obtuvo del SDK
if (!categoryId && venueFromOldModel && venueFromOldModel.attributes && Array.isArray(venueFromOldModel.attributes.categories) && venueFromOldModel.attributes.categories.length > 0)
categoryId = venueFromOldModel.attributes.categories[0];
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
// console.log(`[CATEGORÍA] Usando nombre de categoría de W.model.categories (para ID ${categoryId} de ${source}): ${categoryObjWModel.attributes.name}`); // Comentario de depuración eliminado
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
// console.log(`[CATEGORÍA] No se pudo resolver el nombre para ID de categoría ${categoryId} (obtenido de ${source}). Devolviendo ID.`); // Comentario de depuración eliminado
return `${categoryId}`; // Devuelve el ID si no se encuentra el nombre.
}
return "Sin categoría";
}//getPlaceCategoryName
//Permite obtener el tipo de lugar (área o punto) y su icono
function getPlaceTypeInfo(venue)
{
const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null; // Obtener la geometría del lugar
const isArea = geometry?.CLASS_NAME?.endsWith("Polygon"); // Verificar si la geometría es un polígono (área) o no (punto)
return {isArea, icon : isArea ? "⭔" : "⊙", title : isArea ? "Área" : "Punto"};
}// 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)
{
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)";
}
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)";
}
}
if (!hasExplicitCity && venueFromOldModel && venueFromOldModel.attributes)
{// Old Model (if no explicit city from SDK)
const cityID = venueFromOldModel.attributes.cityID;
if (cityID && typeof W !== 'undefined' && W.model && W.model.cities && W.model.cities.getObjectById)
{// Si hay un cityID en el modelo antiguo
const cityObject = W.model.cities.getObjectById(cityID); // Obtener el objeto de ciudad del modelo Waze
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)";
}
}
}
// 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).
//console.log(`[WME PLN DEBUG CityInfo] Calculated flags: hasExplicitCity=${hasExplicitCity} (Name: ${explicitCityName}), hasStreetInfo=${hasStreetInfo}, cityAssociatedWithStreet=${cityAssociatedWithStreet}`);
//console.log(`[WME PLN DEBUG CityInfo] Calculated: hasAnyAddressInfo=${hasAnyAddressInfo}`);
if (hasAnyAddressInfo)
{// Si hay información de dirección (ciudad explícita o calle)
if (hasExplicitCity)
{// Tiene ciudad explícita
icon = "🏙️"; // Icono para ciudad asignada
title = `Ciudad: ${explicitCityName}`;
}
else
{ // No tiene ciudad explícita, pero sí información de calle
if (cityAssociatedWithStreet)
{ // Tiene calle y ciudad asociada a la calle
icon = "🇻🇦";
title = `Tiene ciudad asociada a la calle: ${cityAssociatedWithStreet}`;
}
else
{
icon = "🚫";
title = "Tiene calle, sin ciudad explícita";
}
}
}
else
{ // No tiene ni ciudad explícita ni información de calle
icon = "🚫";
title = "Faltan datos de Ciudad o Calle"; // Título para "no tiene ciudad ni calle"
}
// console.log(`[CITY INFO] Place ID ${venueFromOldModel ? venueFromOldModel.getID() : 'N/A'}: Icon=${icon}, Title='${title}', HasExplicitCity=${hasExplicitCity}, HasStreet=${hasStreetInfo}, CityViaStreet='${cityAssociatedWithStreet}', ReturnedHasCity=${returnedHasCityBoolean}`);
return {
icon: icon || "❓", // 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
console.error("❌ Panel flotante no está disponible");
return;
}
output.innerHTML = ""; // Limpia completamente el contenido del panel flotante
output.innerHTML = "<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:13px; color:#555;'>Inicializando escaneo...</div></div></div>";
// 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.querySelector("button[type='button']"); // Remover ícono de ✔ previo si existe
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 = {};
// 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 para actualizar la barra de progreso
async function processNextPlace()
{
const currentPlaceForLog = places[index]; // Obtener el lugar actual para log
const currentVenueIdForLog = currentPlaceForLog ? currentPlaceForLog.getID() : 'ID Desconocido'; // Log para el ID del lugar actual
let cityInfo = { // Inicializamos cityInfo con un objeto por defecto
icon: "❓", // Icono por defecto (ej. un signo de interrogación)
title: "Información de ciudad no disponible", // Título por defecto
hasCity: false // Valor booleano por defecto
};
// --- Obtener venueSDK lo antes posible para el nombre más confiable ---
let venueSDK = null;
//Declarar e inicializar resolvedEditorName y lastEditorIdForComparison aquí
let resolvedEditorName = "Desconocido"; // Valor por defecto
let lastEditorIdForComparison = null; // Valor por defecto
if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById) // Verificar si wmeSDK y sus métodos están disponibles
try
{
venueSDK = await wmeSDK.DataModel.Venues.getById({ venueId: currentVenueIdForLog });
}
catch (sdkError)
{
console.error(`[WME_PLN_TRACE] Error al obtener venueSDK para ID ${currentVenueIdForLog}:`, sdkError);
}
// --- Determinar el nombre original más completo (priorizando SDK) ---
let originalNameRaw; // Declaramos aquí, se asigna en el if/else
if (venueSDK && venueSDK.name) // Si venueSDK está disponible y tiene un nombre
originalNameRaw = venueSDK.name; // Si el SDK tiene nombre, úsalo.
else // Si no hay SDK o no tiene nombre, usar el modelo antiguo Fallback al nombre del modelo antiguo
originalNameRaw = currentPlaceForLog && currentPlaceForLog.attributes ? (currentPlaceForLog.attributes.name?.value || currentPlaceForLog.attributes.name || '') : '';
originalNameRaw = originalNameRaw.trim(); // Trim lo antes posible en la versión más "cruda".
let originalNameFull = removeEmoticons(originalNameRaw); // AHORA sí, aplica removeEmoticons UNA SOLA VEZ al nombre "crudo"
//console.log(`[DEBUG - INICIO] originalNameRaw (obtenido de Waze/SDK y trimmeado): "${originalNameRaw}"`);
//console.log(`[DEBUG - INICIO] originalNameFull (después de removeEmoticons - la única aplicación): "${originalNameFull}"`);
// 1. Leer estados de checkboxes y configuraciones iniciales
// console.log(`[WME_PLN_TRACE] Leyendo configuraciones...`);
const useFullPipeline = true; // Siempre usar el pipeline completo para este flujo
const applyGeneralReplacements = useFullPipeline || (document.getElementById("chk-general-replacements")?.checked ?? true); // Aplicar reemplazos generales por defecto
const checkExcludedWords = useFullPipeline || (document.getElementById("chk-check-excluded")?.checked ?? false); // Verificar palabras excluidas por defecto
const checkDictionaryWords = true;// Siempre verificar palabras del diccionario para este flujo
const restoreCommas = document.getElementById("chk-restore-commas")?.checked ?? false;// Restaurar comas por defecto
const similarityThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "81") / 100;// Umbral de similitud por defecto (convertido a porcentaje)
//console.log(`[WME_PLN_TRACE] Configuraciones leídas.`);
// 2. Condición de salida principal (todos los lugares procesados)
if (index >= places.length)
{
// console.log("[WME_PLN_TRACE] Todos los lugares procesados. Finalizando render...");
finalizeRender(inconsistents, places, sugerenciasPorPalabra);
return;
}
const venueFromOldModel = places[index]; // Obtener el lugar actual del array de lugares
const currentVenueNameObj = venueFromOldModel?.attributes?.name; // Obtener el objeto de nombre del lugar actual
const nameValue = typeof currentVenueNameObj === 'object' && currentVenueNameObj !== null && typeof currentVenueNameObj.value === 'string' ? currentVenueNameObj.value.trim() !== ''
? currentVenueNameObj.value : undefined : typeof currentVenueNameObj === 'string' && currentVenueNameObj.trim() !== '' ? currentVenueNameObj : undefined;
if (!places[index] || typeof places[index] !== 'object')
{// 2.1 --- Verificar si el lugar es válido y tiene un ID ---
// console.warn(`[WME_PLN_TRACE] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]);
updateScanProgressBar(index, places.length);
index++;
// console.log(`[WME_PLN_TRACE] Saltando al siguiente place (lugar inválido). Próximo índice: ${index}`);
setTimeout(() => processNextPlace(), 0);
return;
}
// console.log(`[WME_PLN_TRACE] Venue Old Model obtenido: ID ${venueFromOldModel.getID()}`);
// 3. Salto temprano si el venue es inválido o no tiene nombre
if (!venueFromOldModel || typeof venueFromOldModel !== 'object' || !venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '')
{
//console.warn(`[WME_PLN_TRACE] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
updateScanProgressBar(index, places.length);// Actualizar barra de progreso antes de saltar al siguiente lugar
index++;
// console.log(`[WME_PLN_TRACE] Saltando al siguiente place (sin nombre/inválido). Próximo índice: ${index}`);
setTimeout(() => processNextPlace(), 0);
return;
}
const originalName = originalNameFull; // Usa la variable ya limpia de emoticones
const normalizedName = processPlaceName(originalName);// Normalizar el nombre del lugar usando la función definida anteriormente
// 3.1 --- Verificar si el nombre original es igual al normalizado ---
const { lat: placeLat, lon: placeLon } = getPlaceCoordinates(venueFromOldModel, venueSDK);// Obtener las coordenadas del lugar
// Continuar con el flujo usando el nombre normalizado
//console.log(`Nombre original: "${originalName}", Nombre normalizado: "${normalizedName}"`);
// 3.1 --- Verificar si el nombre original es igual al normalizado ---
const currentVenueId = venueFromOldModel.getID();
// console.log(`[WME_PLN_TRACE] Nombre original: "${originalName}", ID: ${currentVenueId}`);
// 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (USANDO venueSDK si está disponible) ---
//console.log(`[WME_PLN_TRACE] Obteniendo información del editor...`);
let lastEditorInfoForLog = "Editor: Desconocido";
// console.log(`[WME_PLN_TRACE] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`);
if (venueSDK && venueSDK.modificationData)
{// Si venueSDK está disponible y tiene modificationData
const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
//console.log(`[WME_PLN_TRACE] Info editor desde SDK:`, updatedByDataFromSDK);
if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '')
{// Si updatedByDataFromSDK es una cadena no vacía
lastEditorInfoForLog = `Editor (SDK): ${updatedByDataFromSDK}`;// Log para el editor desde SDK
resolvedEditorName = updatedByDataFromSDK; // Nombre del editor resuelto desde SDK
}
else if (typeof updatedByDataFromSDK === 'number')
{// Si updatedByDataFromSDK es un número
lastEditorInfoForLog = `Editor (SDK): ID ${updatedByDataFromSDK}`; // Log para el editor desde SDK (ID numérico)
resolvedEditorName = `ID ${updatedByDataFromSDK}`;// Nombre del editor resuelto desde SDK (ID numérico)
lastEditorIdForComparison = updatedByDataFromSDK;// ID del editor para comparación posterior
if (typeof W !== 'undefined' && W.model && W.model.users)
{// Si Waze Map Editor está disponible y tiene el modelo de usuarios
const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
if (userObjectW && userObjectW.userName)
{// Si el objeto de usuario tiene un nombre de usuario válido
lastEditorInfoForLog = `Editor (SDK ID ${updatedByDataFromSDK} -> W.model): ${userObjectW.userName}`;
resolvedEditorName = userObjectW.userName;
}
else if (userObjectW) // Si el objeto de usuario existe pero no tiene userName
lastEditorInfoForLog = `Editor (SDK ID ${updatedByDataFromSDK} -> W.model): ID ${updatedByDataFromSDK} (sin userName en W.model)`;
}
}
else if (updatedByDataFromSDK === null)
{// Si updatedByDataFromSDK es null
lastEditorInfoForLog = "Editor (SDK): N/D (updatedBy es null)";
resolvedEditorName = "N/D";
}
else
{// Si updatedByDataFromSDK es de un tipo inesperado
lastEditorInfoForLog = `Editor (SDK): Valor inesperado para updatedBy ('${updatedByDataFromSDK}')`;
resolvedEditorName = "Inesperado (SDK)";
}
}
else
{// Si no hay venueSDK o no tiene modificationData, usar el modelo antiguo como fallback
// console.log(`[WME_PLN_TRACE] Fallback a W.model para info de editor.`);
const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy; // Obtener el ID del editor desde el modelo antiguo
if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined)
{// Si oldModelUpdatedBy es un ID válido (no null ni undefined)
lastEditorIdForComparison = oldModelUpdatedBy; // Guardar el ID del editor para comparación posterior
resolvedEditorName = `ID ${oldModelUpdatedBy}`;// Nombre del editor resuelto desde el modelo antiguo
let usernameFromOldModel = `ID ${oldModelUpdatedBy}`;// Nombre de usuario del editor desde el modelo antiguo
if (typeof W !== 'undefined' && W.model && W.model.users)
{// Si Waze Map Editor está disponible y tiene el modelo de usuarios
const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);// Obtener el objeto de usuario por ID
if (userObjectW && userObjectW.userName)
{// Si el objeto de usuario tiene un nombre de usuario válido
usernameFromOldModel = userObjectW.userName;
resolvedEditorName = userObjectW.userName;
}
else if (userObjectW) // Si el objeto de usuario existe pero no tiene userName
usernameFromOldModel = `ID ${oldModelUpdatedBy} (sin userName)`;
}
lastEditorInfoForLog = `Editor (W.model Fallback): ${usernameFromOldModel}`; // Log para el editor desde W.model (fallback)
}
else
{// Si oldModelUpdatedBy es null o undefined
lastEditorInfoForLog = "Editor (W.model Fallback): N/D";
resolvedEditorName = "N/D";
}
}
// console.log(`[WME_PLN_TRACE] Info editor final: ${lastEditorInfoForLog}, Editado por mi: ${wasEditedByMe}`);
// 5. PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA
//console.log(`[WME_PLN_TRACE] Iniciando procesamiento palabra por palabra del nombre...`);
let nombreSugeridoParcial = [];
let sugerenciasLugar = {};
const originalWords = originalName.split(/\s+/); // Dividir el nombre original en palabras usando espacios como delimitador
const processingStepLabel = document.getElementById("processingStep"); // Obtener el elemento del DOM para mostrar el progreso
if (index === 0)// Si es el primer lugar, mostrar mensaje de inicio
sugerenciasPorPalabra = {};
const newRomanBaseRegexString = "((XC|XL|L?X{0,3})(IX|IV|V?I{0,3})?|(IX|IV|V?I{0,3}))"; // Regex base para números romanos
const romanRegexStrict = new RegExp(`^${newRomanBaseRegexString}$`); // Sin 'i' para prueba con toUpperCase()
const romanRegexStrictInsensitive = new RegExp(`^${newRomanBaseRegexString}$`, 'i'); // Con 'i' para isPotentiallyRomanNumeral
originalWords.forEach((P, idx_word) => { // Verificar si se debe usar el diccionario
// console.log(`[WME_PLN_TRACE_WORD] Procesando palabra #${idx_word + 1}: "${P}"`);
const endsWithComma = restoreCommas && P.endsWith(","); // Verificar si la palabra termina con coma y si se debe restaurar
const baseWord = endsWithComma ? P.slice(0, -1) : P; // Eliminar la coma al final si corresponde
const cleaned = baseWord.trim(); // Limpiar espacios en blanco al inicio y al final de la palabra
//console.log(`[DEBUG WORD] Procesando "${P}". Cleaned: "${cleaned}"`);
if (cleaned === "") // Si cleaned es una cadena vacía, no procesar más
{
nombreSugeridoParcial.push(cleaned);
//console.log(`[WME_PLN_TRACE_WORD] Palabra vacía, continuando.`);
return;
}
let isExcluded = false; // Variable para verificar si la palabra está en la lista de excluidas
let matchingExcludedWord = null; // Variable para almacenar la palabra excluida coincidente, si se encuentra
if (checkExcludedWords)
{// Si se debe verificar las palabras excluidas
matchingExcludedWord = excludedArray.find(w_excluded => removeDiacritics(w_excluded.toLowerCase()) === removeDiacritics(cleaned.toLowerCase())); // Buscar coincidencia en la lista de palabras excluidas
isExcluded = !!matchingExcludedWord;// Verificar si la palabra está en la lista de excluidas
// console.log(`[WME_PLN_TRACE_WORD] Excluida: ${isExcluded}${isExcluded ? ` (coincide con: "${matchingExcludedWord}")` : ''}`);
}
let tempReplaced;
const isCommon = commonWords.includes(cleaned.toLowerCase()); // Verificar si la palabra es común
const isPotentiallyRomanNumeral = romanRegexStrictInsensitive.test(cleaned); //
//console.log(`[WME_PLN_TRACE_WORD] Común: ${isCommon}, Potencial Romano: ${isPotentiallyRomanNumeral}`);
if (isExcluded) // Verificar si la palabra es un número romano potencial
{
tempReplaced = matchingExcludedWord;
if (romanRegexStrictInsensitive.test(tempReplaced)) // Si la palabra excluida es un número romano potencial
tempReplaced = tempReplaced.toUpperCase();
//console.log(`[DEBUG WORD] Es excluida. tempReplaced: "${tempReplaced}"`);
}
else// Si la palabra no está en la lista de excluidas
{
let dictionaryFormToUse = null;
let foundInDictionary = false;
if (checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") // Verificar si la palabra es un número romano potencial
{
const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase()); // Convertir cleaned a minúsculas y eliminar diacríticos
const cleanedHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(cleaned); // Verificar si cleaned tiene diacríticos
for (const diccWord of window.dictionaryWords)
{// Verificar si la palabra es un número romano potencial
if (removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics)
{// Si la palabra del diccionario coincide con cleaned (sin diacríticos y en minúsculas)
foundInDictionary = true;// Marcar como encontrada en el diccionario
const diccWordHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(diccWord);// Verificar si la palabra del diccionario tiene diacríticos
if (cleanedHasDiacritics && !diccWordHasDiacritics) // Verificar si la palabra del diccionario es un número romano potencial
dictionaryFormToUse = cleaned;
else
{
if (isPotentiallyRomanNumeral) // Si cleaned es un número romano potencial
if (diccWord === diccWord.toUpperCase() && romanRegexStrict.test(diccWord))
dictionaryFormToUse = diccWord;
else // Si cleaned no es un número romano potencial
dictionaryFormToUse = diccWord;
}
break;
}
}
//console.log(`[WME_PLN_TRACE_WORD] Encontrada en diccionario: ${foundInDictionary}${dictionaryFormToUse ? ` (usando forma: "${dictionaryFormToUse}")` : ''}`);
}
if (dictionaryFormToUse !== null)
{// Si se encontró una forma en el diccionario
tempReplaced = dictionaryFormToUse;
//console.log(`[DEBUG WORD] En diccionario. tempReplaced: "${tempReplaced}"`);
}
else
{// Si no se encontró en el diccionario, aplicar normalización
tempReplaced = normalizePlaceName(cleaned);
//console.log(`[DEBUG WORD] Normalizada por normalizePlaceName. tempReplaced: "${tempReplaced}"`);
}
// Esta lógica capitaliza según si es romano, primera palabra, común, etc. Necesitamos asegurarnos que "Mi" y "Di" no se conviertan a "MI"/"DI" .
if (tempReplaced.toUpperCase() === "MI" || tempReplaced.toUpperCase() === "DI" || tempReplaced.toUpperCase() === "SI")
tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase();
else if (isPotentiallyRomanNumeral)
{ // No es "MI" ni "DI", pero sí un romano potencial
const upperVersion = tempReplaced.toUpperCase();
if (romanRegexStrict.test(upperVersion)) // Verificar si es un romano estricto
tempReplaced = upperVersion;
else // No pasó la prueba estricta de romano, capitalizar normalmente
tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase();
}
else if (idx_word === 0) // No es "MI", "DI", ni romano, y es primera palabra
tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
else
{ // No es "MI", "DI", ni romano, y no es primera palabra
if (isCommon)// Si es una palabra común, convertir a minúsculas
tempReplaced = tempReplaced.toLowerCase();
else
tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
}
//console.log(`[DEBUG WORD] Después de capitalización final. tempReplaced: "${tempReplaced}"`);
}
//console.log(`[WME_PLN_TRACE_WORD] Palabra temporalmente reemplazada a: "${tempReplaced}"`);
// Generación de Sugerencias Clickeables
const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase()); // Convertir cleaned a minúsculas y eliminar diacríticos
const tempReplacedLowerNoDiacritics = removeDiacritics(tempReplaced.toLowerCase()); // Convertir tempReplaced a minúsculas y eliminar diacríticos
if (cleaned !== tempReplaced && (!commonWords.includes(cleaned.toLowerCase()) || cleaned.toLowerCase() !== tempReplaced.toLowerCase()) && cleanedLowerNoDiacritics !== tempReplacedLowerNoDiacritics)
{// Verificar si cleaned es igual a tempReplaced (sin diacríticos)
if (!sugerenciasLugar[baseWord])// Si no hay sugerencias para la palabra base
sugerenciasLugar[baseWord] = [];
if (!sugerenciasLugar[baseWord].some(s => s.word === cleaned && s.fuente === 'original_preserved'))
sugerenciasLugar[baseWord].push({ word: cleaned, similarity: 0.99, fuente: 'original_preserved' });
}
if (!isExcluded && checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function")
{// Verificar si la palabra es un número romano potencial
const similarDictionary = findSimilarWords(cleaned, window.dictionaryIndex, similarityThreshold); // Buscar palabras similares en el diccionario
if (similarDictionary.length > 0)
{// Filtrar palabras similares que no son idénticas
if (!sugerenciasLugar[baseWord]) sugerenciasLugar[baseWord] = [];
similarDictionary.forEach(dictSuggestion => {
// Evitar sugerir la palabra misma o la ya normalizada si son idénticas
if (dictSuggestion.word.toLowerCase() !== cleaned.toLowerCase() && dictSuggestion.word.toLowerCase() !== tempReplaced.toLowerCase())
{
if (!sugerenciasLugar[baseWord].some(s => s.word === dictSuggestion.word && s.fuente === 'dictionary'))
sugerenciasLugar[baseWord].push({ ...dictSuggestion, fuente: 'dictionary' });
}
});
}
}
if (checkExcludedWords)
{// Verificar si la palabra es un número romano potencial
const similarExcluded = findSimilarWords(cleaned, excludedWordsMap, similarityThreshold).filter(s => s.similarity < 1); // Buscar palabras similares en la lista de excluidas, excluyendo coincidencias exactas
if (similarExcluded.length > 0)
{// Filtrar palabras similares que no son idénticas
if (!sugerenciasLugar[baseWord])
sugerenciasLugar[baseWord] = [];
similarExcluded.forEach(excludedSuggestion => {
if (!sugerenciasLugar[baseWord].some(s => s.word === excludedSuggestion.word && s.fuente === 'excluded'))
{
sugerenciasLugar[baseWord].push({...excludedSuggestion, fuente: 'excluded' });
//console.log(`[WME_PLN_TRACE_WORD] Añadida sugerencia 'excluded': "${excludedSuggestion.word}" para "${baseWord}"`);
}
});
}
}
if (endsWithComma && !tempReplaced.endsWith(",")) // Verificar si la palabra es un número romano potencial
tempReplaced += ",";
nombreSugeridoParcial.push(tempReplaced); // Agregar la palabra procesada al array parcial
});
// ---- FIN PROCESAMIENTO PALABRA POR PALABRA ----
//console.log(`[DEBUG FINAL] Nombre parcial antes de unirse: [${nombreSugeridoParcial.map(w => `"${w}"`).join(', ')}]`);
// 7. --- COMPILACIÓN DE suggestedName ---
const joinedSuggested = nombreSugeridoParcial.join(' '); // Unir las palabras procesadas en un solo string
let processedName = joinedSuggested; // El nombre procesado después de unir las palabras
if (applyGeneralReplacements)
{// Si el nombre es igual al original, no aplicar más transformaciones
//console.log(`[DEBUG FINAL] Antes de aplicarReemplazosGenerales: "${processedName}"`);
processedName = aplicarReemplazosGenerales(processedName);
//console.log(`[DEBUG FINAL] Después de aplicarReemplazosGenerales: "${processedName}"`);
}
// Aplicar reglas especiales al nombre procesado
processedName = aplicarReglasEspecialesNombre(processedName);
//console.log(`[DEBUG FINAL] Después de aplicarReglasEspecialesNombre: "${processedName}"`);
// Post-procesamiento de comillas y paréntesis
processedName = postProcessQuotesAndParentheses(processedName);
// Reemplazos definidos por el usuario
if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0)
{
// Aplicar reemplazos definidos por el usuario
//console.log(`[DEBUG FINAL] Antes de aplicarReemplazosDefinidos: "${processedName}"`);
processedName = aplicarReemplazosDefinidos(processedName, replacementWords);
//console.log(`[DEBUG FINAL] Después de aplicarReemplazosDefinidos: "${processedName}"`);
}
// Aplicar movimiento de palabras al inicio (SWAP) ---
processedName = applyWordsToStartMovement(processedName);
// console.log(`[WME_PLN_TRACE] Después de movimiento de palabras al inicio (SWAP): "${processedName}"`);
let suggestedName = processedName.replace(/\s{2,}/g, ' ').trim();
// console.log(`[WME_PLN_TRACE] Nombre sugerido después de trim/espacios múltiples: "${suggestedName}"`);
// 6.1 --- QUITAR PUNTO FINAL SI EXISTE ---
if (suggestedName.endsWith('.'))
{
suggestedName = suggestedName.slice(0, -1);
// console.log(`[WME_PLN_TRACE] Nombre sugerido después de quitar punto final: "${suggestedName}"`);
}
// 6.2 --- QUITAR ESPACIOS MÚLTIPLES ---
//console.log(`[WME_PLN_TRACE] Evaluando lógica de salto...`);
const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
let shouldSkipThisPlace = false;
let skipReasonLog = "";
// Nueva condición: Si el nombre original (con emoticones) es diferente del sugerido (sin emoticones), NO SALTAR.
if (originalNameRaw !== suggestedName)// Comparar el original con lo sugerido.
shouldSkipThisPlace = false; // No saltar si hubo algún cambio, incluyendo eliminación de emoticones.
else
{
let tempOriginalNormalized = aplicarReemplazosGenerales(originalName.trim());
tempOriginalNormalized = aplicarReglasEspecialesNombre(tempOriginalNormalized);
tempOriginalNormalized = postProcessQuotesAndParentheses(tempOriginalNormalized);
if (tempOriginalNormalized.endsWith('.'))
{
tempOriginalNormalized = tempOriginalNormalized.slice(0, -1);
}
tempOriginalNormalized = tempOriginalNormalized.replace(/\s{2,}/g, ' ').trim();
// REEMPLAZO DE BLOQUE DE CONDICIÓN SKIP NORMALIZED
const nombre = tempOriginalNormalized;
const normalizadoFinal = suggestedName;
const nombreClean = nombre.trim();
const normalizadoClean = normalizadoFinal.trim();
if (nombreClean === normalizadoClean)
{
//console.log("[DEBUG COMPARACIÓN] Detectados como iguales sin cambios:", nombreClean, "===", normalizadoClean);
shouldSkipThisPlace = true;
skipReasonLog = `[SKIP NORMALIZED]`;
// No return porque estamos en un bloque, así que solo marcamos el skip
}
}
// --- Salto temprano si se determinó omitir el lugar ---
if (shouldSkipThisPlace)
{
/*if (skipReasonLog)
console.log(`[WME_PLN_TRACE] ${skipReasonLog} Descartado "${originalName}"`);*/
const updateFrequency = 5; // Actualiza cada 5 lugares la barra de progreso
if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length)
{
updateScanProgressBar(index, places.length);
}
index++;
setTimeout(() => processNextPlace(), 0); // Continúa con el siguiente lugar
return;
}
//console.log(`[WME_PLN_TRACE] Decisión de salto: ${shouldSkipThisPlace} (${skipReasonLog})`);
// ---- FIN LÓGICA DE SALTO ---
// 8. Registrar o no en la lista de inconsistentes
//console.log(`[WME_PLN_TRACE] Registrando lugar con inconsistencias...`);
// *** Si Llegamos Aquí, El Lugar No Se Salta Y Necesitamos Su Info Completa Para La Tabla ***
if (processingStepLabel)
{
processingStepLabel.textContent = "Registrando lugar(es) con inconsistencias...";
}
// Lógica de Categorías (solo para lugares no saltados)
const shouldRecommendCategories = document.getElementById("chk-recommend-categories")?.checked ?? true;
let currentCategoryKey;
let currentCategoryIcon;
let currentCategoryTitle;
let currentCategoryName;
let dynamicSuggestions;
try
{
const lang = getWazeLanguage();
currentCategoryKey = getPlaceCategoryName(venueFromOldModel, venueSDK);
const categoryDetails = getCategoryDetails(currentCategoryKey);
currentCategoryIcon = categoryDetails.icon;
currentCategoryTitle = categoryDetails.description;
currentCategoryName = categoryDetails.description;
if (shouldRecommendCategories)
dynamicSuggestions = findCategoryForPlace(originalName);
else
dynamicSuggestions = [];
}
catch (e)
{
console.error("[WME PLN] Error procesando las categorías:", e);
currentCategoryName = "Error";
currentCategoryIcon = "❓";
currentCategoryTitle = "Error al obtener categoría";
dynamicSuggestions = [];
currentCategoryKey = "UNKNOWN";
}
// --- Fin de la Lógica de Categorías ---
lastEditorIdForComparison = null; // Re-inicializar para este bloque
if (venueSDK && venueSDK.modificationData)
{
const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '')
resolvedEditorName = updatedByDataFromSDK;
else if (typeof updatedByDataFromSDK === 'number')
{
lastEditorIdForComparison = updatedByDataFromSDK;
resolvedEditorName = `ID ${updatedByDataFromSDK}`;
if (W && W.model && W.model.users) {
const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
if (userObjectW && userObjectW.userName)
resolvedEditorName = userObjectW.userName;
}
}
}
else
{ // Fallback a W.model
const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined)
{
lastEditorIdForComparison = oldModelUpdatedBy;
resolvedEditorName = `ID ${oldModelUpdatedBy}`;
if (W && W.model && W.model.users)
{
const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);
if (userObjectW && userObjectW.userName)
resolvedEditorName = userObjectW.userName;
}
}
}
// Obtener información de la ciudad (solo para lugares no saltados)
try
{
cityInfo = await getPlaceCityInfo(venueFromOldModel, venueSDK);
}
catch (e)
{
console.error(`[WME_PLN_TRACE] Error al obtener información de la ciudad para el venue ID ${currentVenueId}:`, e);
}
// === FIN OBTENCIÓN DE DATOS ADICIONALES ===
// 8. Agregar a inconsistentes
// Identificar el nivel de bloqueo del lugar
let lockRank = 0; // Valor por defecto
if (venueSDK && venueSDK.lockRank !== undefined && venueSDK.lockRank !== null)
lockRank = venueSDK.lockRank;
else if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.lockRank !== undefined && venueFromOldModel.attributes.lockRank !== null)
lockRank = venueFromOldModel.attributes.lockRank;
//console.log(`[DEBUG LOCK] Place ID: ${currentVenueId}, Raw LockRank: ${lockRank}`);
let lockRankEmoji;
// Lógica corregida: 1 al 6 muestra su respectivo emoji; 0 (desbloqueado) o cualquier otro valor muestra 0️⃣
if (lockRank >= 0 && lockRank <= 5)
lockRankEmoji = lockRankEmojis[lockRank+1]; // Usa el emoji para el nivel exacto (1 al 6)
else
lockRankEmoji = lockRankEmojis[0]; // Para 0 (desbloqueado), Auto (si no fue 1-6), o cualquier otro caso
// --- NUEVO CONSOLE.LOG DESPUÉS DE ASIGNACIÓN ---
//console.log(`[DEBUG LOCK] Assigned LockRankEmoji: ${lockRankEmoji}`);
// --- FIN NUEVO CONSOLE.LOG ---
// Asegurar que `resolvedEditorName` esté declarada con `let` para evitar ReferenceError.
//let resolvedEditorName;
// Agregar a la lista de inconsistencias
inconsistents.push({
lockRankEmoji: lockRankEmoji,
id: currentVenueId,
original: originalName,
normalized: suggestedName,
editor: resolvedEditorName, // Usamos el nombre del editor resuelto
cityIcon: cityInfo.icon,
cityTitle: cityInfo.title,
hasCity: cityInfo.hasCity,
venueSDKForRender: venueSDK,
currentCategoryName: currentCategoryName,
currentCategoryIcon: currentCategoryIcon,
currentCategoryTitle: currentCategoryTitle,
currentCategoryKey: currentCategoryKey,
dynamicCategorySuggestions: dynamicSuggestions,
// MODIFICADO: Asegurarse de incluir lat y lon obtenidos de getPlaceCoordinates
lat: placeLat,
lon: placeLon
});
// 9. Agregar datos del lugar para la verificación de duplicados
sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;// Guardar sugerencias por palabra para este lugar
// 10. Finalizar procesamiento del 'place' actual y pasar al siguiente
const updateFrequency = 5;
if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length)
updateScanProgressBar(index, places.length);
index++;
setTimeout(() => processNextPlace(), 0);
}
// console.log("[WME_PLN_TRACE] Iniciando primer processNextPlace...");
try
{
setTimeout(() => { processNextPlace(); }, 10);
}
catch (error)
{
console.error("[WME_PLN_TRACE][ERROR_CRITICAL] Fallo al iniciar processNextPlace:", error, error.stack);
const outputFallback = document.querySelector("#wme-place-inspector-output");
if (outputFallback) {
outputFallback.innerHTML = `<div style='color:red; padding:10px;'><b>Error Crítico:</b> El script de normalización encontró un problema grave y no pudo continuar. Revise la consola para más detalles (F12).<br>Detalles: ${error.message}</div>`;
}
const scanBtn = document.querySelector("button[type='button']"); // Asumiendo que es el botón de Start Scan
if(scanBtn) {
scanBtn.disabled = false;
scanBtn.textContent = "Start Scan... (Error Previo)";
}
if (window.processingDotsInterval) {
clearInterval(window.processingDotsInterval);
}
}// processNextPlace
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(' ');
}// ---- FIN DE LA FUNCIÓN reapplyExcludedWordsLogic ----
//Función para finalizar renderizado una vez completado el análisis
function finalizeRender(inconsistents, placesArr, allSuggestions)
{ // Limpiar el mensaje de procesamiento y spinner al finalizar el análisis
// Detener animación de puntos suspensivos si existe
if (window.processingDotsInterval)
{
clearInterval(window.processingDotsInterval);
window.processingDotsInterval = null;
}
// Refuerza el restablecimiento del botón de escaneo al entrar
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
scanBtn.textContent = "Start Scan...";
scanBtn.disabled = false;
scanBtn.style.opacity = "1";
scanBtn.style.cursor = "pointer";
}
// Verificar si el botón de escaneo existe
const output = document.querySelector("#wme-place-inspector-output");
if (!output)
{
console.error("❌ No se pudo montar el panel flotante. Revisar estructura del DOM.");
alert("Hubo un problema al mostrar los resultados. Intenta recargar la página.");
return;
}
// 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
{
console.log("[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", inconsistents.length);
// Limpiar el mensaje de procesamiento y spinner
if (output)
{
// Mostrar el panel flotante al terminar el procesamiento se usa para mostrar los resultados y llamados al console.log
}
// Limitar a 30 resultados y mostrar advertencia si excede
const maxRenderLimit = 30;
const totalInconsistentsOriginal = inconsistents.length; // Guardar el total original
let isLimited = false; // Declarar e inicializar isLimited
// Si hay más de 30 resultados, limitar a 30 y mostrar mensaje
if (totalInconsistentsOriginal > maxRenderLimit)
{
inconsistents = inconsistents.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(inconsistents);
// Mostrar contador de registros
const resultsCounter = document.createElement("div");
resultsCounter.style.fontSize = "13px";
resultsCounter.style.color = "#555"; // Color base para el texto normal
resultsCounter.style.marginBottom = "8px";
resultsCounter.style.textAlign = "left";
// Mostrar el número total de inconsistencias encontradas
if (totalInconsistentsOriginal > 0)
{
if (isLimited)
{
resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando las primeras <span style="color: #ff0000;"><b>${inconsistents.length}</b></span> (límite de ${maxRenderLimit} aplicado).`;
}
else
{
resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando <span style="color: #ff0000;"><b>${inconsistents.length}</b></span>.`;
}
}
else
{
// No se añaden resultados a la tabla si no hay inconsistencias,
// pero el mensaje de "Todos los nombres... están correctamente normalizados" se manejará más abajo.
}
if (output && totalInconsistentsOriginal > 0) // Solo añadir si se encontraron inconsistencias originalmente
output.appendChild(resultsCounter);
// Si no hay inconsistencias, mostrar mensaje y salir (progreso visible)
if (inconsistents.length === 0) // Esto ahora significa que o no había nada, o se limitó a 0 (aunque es improbable con el límite de 30)
{
// Si totalInconsistentsOriginal también es 0, entonces realmente no había nada.
if (totalInconsistentsOriginal === 0)
{
output.appendChild(document.createTextNode("Todos los nombres de lugares visibles están correctamente normalizados."));
// Mensaje visual de análisis finalizado sin inconsistencias
const checkIcon = document.createElement("div");
checkIcon.innerHTML = "✔ Análisis finalizado sin inconsistencias.";
checkIcon.style.marginTop = "10px";
checkIcon.style.fontSize = "14px";
checkIcon.style.color = "green";
output.appendChild(checkIcon);
// Mensaje visual adicional solicitado
const successMsg = document.createElement("div");
successMsg.textContent = "Todos los nombres están correctamente normalizados.";
successMsg.style.marginTop = "10px";
successMsg.style.fontSize = "14px";
successMsg.style.color = "green";
successMsg.style.fontWeight = "bold";
output.appendChild(successMsg);
}
// Con inconsistents.length === 0 PERO totalInconsistentsOriginal > 0,
// significa que el límite fue tan bajo que no se muestra nada, lo cual no debería pasar con un límite de 30
// a menos que el total original fuera menor que 30 y luego se filtraran todos por alguna razón.
// En este caso, el contador ya habrá mostrado el mensaje adecuado.
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
existingOverlay.remove();
// Actualizar barra de progreso 100%
const progressBarInnerTab =
document.getElementById("progressBarInnerTab");
const progressBarTextTab =
document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent =
`Progreso: 100% (${placesArr.length}/${placesArr.length})`;
}
// Mensaje adicional en el tab principal (pestaña)
const outputTab =
document.getElementById("wme-normalization-tab-output");
if (outputTab)
{
outputTab.innerHTML =
`✔ Todos los nombres están normalizados. Se analizaron ${
placesArr.length} lugares.`;
outputTab.style.color = "green";
outputTab.style.fontWeight = "bold";
}
// Restaurar el texto y estado del botón de escaneo
const scanBtn = document.querySelector("button[type='button']");
if (scanBtn)
{
scanBtn.textContent = "Start Scan...";
scanBtn.disabled = false;
scanBtn.style.opacity = "1";
scanBtn.style.cursor = "pointer";
// Agregar check verde al lado del botón al finalizar sin
// errores
const iconCheck = document.createElement("span");
iconCheck.textContent = " ✔";
iconCheck.style.marginLeft = "8px";
iconCheck.style.color = "green";
scanBtn.appendChild(iconCheck);
}
return;
}
//Permite renderizar la tabla de resultados
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.fontSize = "12px";
// Añadir clase para estilo de tabla
const thead = document.createElement("thead");
// Añadir cabecera de la tabla
const headerRow = document.createElement("tr");
[
"N°",
"Perma",
"Tipo/Ciudad",
"LL",
"Editor",
"Nombre Actual",
"⚠️", // ("Alerta", "Advertencia")
"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"; // Un ancho pequeño
} 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") { // <-- Ajustar ancho si es necesario
th.style.width = "100px"; // Reducido de 120px a 100px
} else if (header === "Acción") { // <-- Ajustar ancho si es necesario
th.style.width = "100px"; // Reducido de 120px a 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"; // z-index de la cabecera de la tabla
headerRow.style.backgroundColor = "#003366";
headerRow.style.color = "#ffffff";
thead.appendChild(headerRow);
table.appendChild(thead);
// Añadir el cuerpo de la tabla
const tbody = document.createElement("tbody");
// En el render de cada fila:
inconsistents.forEach(({ lockRankEmoji, id, original, normalized, editor, cityIcon, cityTitle, hasCity, currentCategoryName, currentCategoryIcon, currentCategoryTitle, currentCategoryKey, dynamicCategorySuggestions, venueSDKForRender, isDuplicate=false, duplicatePartners= [] }, index) => {
// Añadir un console.log para depurar el estado de isDuplicate y duplicatePartners
//console.log(`[WME_PLN_DEBUG_FINAL_RENDER] Procesando lugar ID: ${id}, isDuplicate: ${isDuplicate}, duplicatePartners:`, duplicatePartners);
// Actualizar barra de progreso visual EN EL TAB PRINCIPAL
const progressPercent = Math.floor(((index + 1) / inconsistents.length) * 100);
// Actualiza barra de progreso en el tab principal
const progressBarInnerTab = document.getElementById("progressBarInnerTab");
const progressBarTextTab = document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = `${progressPercent}%`;
progressBarTextTab.textContent = `Progreso: ${
progressPercent}% (${index + 1}/${inconsistents.length})`;
}
const row = document.createElement("tr");
row.querySelectorAll("td").forEach(td => td.style.verticalAlign = "top");
row.dataset.placeId = id; //Añadir data-place-id a la fila para fácil referencia
//Celda para el número de línea (N°)
const numberCell = document.createElement("td");
numberCell.textContent = index + 1; // +1 porque el índice es base 0
numberCell.style.textAlign = "center";
numberCell.style.padding = "4px";
row.appendChild(numberCell);
// Columna de enlace permanente (Perma)
const permalinkCell = document.createElement("td");
const link = document.createElement("a");
link.href = "#";
// Reemplazado onclick por addEventListener para mejor compatibilidad y centrado de mapa
link.addEventListener("click", (e) => {
e.preventDefault();
const venueObj = W.model.venues.getObjectById(id);
const venueSDKObj = venueSDKForRender || null;
// Centrar mapa
const geometry = venueObj.getGeometry();
if (geometry && geometry.getCentroid)
{
const center = geometry.getCentroid();
W.map.setCenter(center, null, false, 0);
}
// Seleccionar lugar
if (W.selectionManager && typeof W.selectionManager.select === "function")
{
W.selectionManager.select(venueObj);
}
else if (W.selectionManager && typeof W.selectionManager.setSelectedModels === "function")
{
W.selectionManager.setSelectedModels([venueObj]);
}
});
link.title = "Abrir en panel lateral";
link.textContent = "🔗";
permalinkCell.appendChild(link);
permalinkCell.style.padding = "4px";
permalinkCell.style.fontSize = "18px"; // Tamaño del ícono
permalinkCell.style.textAlign = "center"; // Centrar el ícono
permalinkCell.style.width = "65px";
row.appendChild(permalinkCell);
// Combinada: Tipo / Ciudad
const typeCityCell = document.createElement("td");
// Obtener el objeto del venue por ID
const venueObject = W.model.venues.getObjectById(id);
const typeInfo = getPlaceTypeInfo(venueObject); // Obtenemos el ícono de Tipo (Punto/Área)
// Lógica condicional :
if (hasCity)
{
// Si SÍ tiene ciudad, solo muestra el ícono de Tipo.
typeCityCell.textContent = typeInfo.icon;
typeCityCell.title = `Tipo: ${typeInfo.title} | Ciudad: ${cityTitle}`;
}
else
{
// Si NO tiene ciudad, muestra "Tipo / Ciudad".
typeCityCell.innerHTML = `${typeInfo.icon} / <span style="color:red;">${cityIcon}</span>`;
typeCityCell.title = `Tipo: ${typeInfo.title} | ${cityTitle}`;
}
typeCityCell.style.textAlign = "center";
typeCityCell.style.fontSize = "20px";
row.appendChild(typeCityCell);
//Columna Bloqueo (LL)
const lockCell = document.createElement("td");
lockCell.textContent = lockRankEmoji; // Usa el emoji obtenido
lockCell.style.textAlign = "center"; //
lockCell.style.padding = "4px"; //
lockCell.style.width = "40px"; // Mismo ancho que en el thead
lockCell.style.fontSize = "18px";
row.appendChild(lockCell); //
// Columna Editor (username)
const editorCell = document.createElement("td");
editorCell.textContent = editor || "Desconocido"; // Use the stored editor name
editorCell.title = "Último editor";
editorCell.style.padding = "4px";
editorCell.style.width = "140px";
editorCell.style.textAlign = "center";
row.appendChild(editorCell);
const originalCell = document.createElement("td");
const inputOriginal = document.createElement("input");
inputOriginal.type = "text";
const venueLive = W.model.venues.getObjectById(id);
const currentLiveName = venueLive?.attributes?.name?.value || venueLive?.attributes?.name || "";
inputOriginal.value = currentLiveName;
// Si el nombre actual es distinto del sugerido, resalta en rojo.
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"; // Asegura el ancho del input
inputOriginal.style.backgroundColor = "#eee";
// convierte la celda en flex y centra verticalmente
originalCell.style.padding = "4px";
originalCell.style.width = "270px"; // Asegura el ancho de la celda
originalCell.style.display = "flex";
originalCell.style.alignItems = "flex-start";
originalCell.style.verticalAlign = "middle";
// haces que el input llene el alto
inputOriginal.style.flex = "1";
inputOriginal.style.height = "100%";
inputOriginal.style.boxSizing = "border-box";
originalCell.appendChild(inputOriginal); // Añade el input al originalCell
row.appendChild(originalCell); // Añade la celda "Nombre Actual" a la fila
// Alertas
const alertCell = document.createElement("td");
alertCell.style.width = "30px"; // Ancho de la columna de alerta
alertCell.style.textAlign = "center";
alertCell.style.verticalAlign = "middle"; // Centra el contenido verticalmente
alertCell.style.padding = "4px";
if (isDuplicate)
{ // Si el lugar es un duplicado, añade el icono de advertencia
const warningIcon = document.createElement("span");
warningIcon.textContent = " ⚠️"; // El símbolo de advertencia
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); // Añade el icono a la nueva celda de alerta
}
// Añadir más lógica para otras alertas :
// else if (otraCondicionDeAlerta) {
// const otroIcono = document.createElement("span");
// otroIcono.textContent = " 🚩";
// otroIcono.title = "Otra alerta";
// alertCell.appendChild(otroIcono);
// }
row.appendChild(alertCell); // Añade la nueva celda de alerta a la fila
// --- CONTINÚA CON EL CÓDIGO EXISTENTE PARA suggestionCell ---
const suggestionCell = document.createElement("td");
// : Asegurar estilos flex para alineación vertical
suggestionCell.style.display = "flex";
suggestionCell.style.alignItems = "flex-start";
suggestionCell.style.justifyContent = "flex-start";
suggestionCell.style.padding = "4px";
// Según la cabecera: "Nombre Sugerido" tiene width "270px".
suggestionCell.style.width = "270px"; // Mantener este ancho para la celda
// --- Renderizar input principal de sugerencia ---
// 1) Declaración de la celda
// 2) Declaración del input
const inputReplacement = document.createElement("input");
// 3) TODO el código que toque inputReplacement (estilos, atributos, listeners, lógica de enable/disable, etc.)
inputReplacement.type = "text";
inputReplacement.value = normalized;
inputReplacement.style.width = "100%";
inputReplacement.addEventListener('input', debounce(() => {
if (inputReplacement.value.trim() !== original) {
applyButton.disabled = false;
applyButton.style.color = "";
} else {
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
}, 300));
// 4) Finalmente, appendes
suggestionCell.appendChild(inputReplacement);
inputReplacement.style.height = "100%";
inputReplacement.style.boxSizing = "border-box";
// Las siguientes dos líneas están duplicadas y pueden ser eliminadas si ya se establecieron arriba para suggestionCell
// suggestionCell.style.padding = "4px"; // Si ya está arriba, eliminar
// suggestionCell.style.width = "270px"; // Si ya está arriba, eliminar
// --- INICIO: Lógica de Pistas Visuales (Colores de fondo) ---
let autoApplied = false;
if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded' && s.similarity === 1)) {
autoApplied = true;
}
if (autoApplied) {
inputReplacement.style.backgroundColor = "#c8e6c9"; // verde claro
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"; // amarillo claro
inputReplacement.title = "Contiene palabra especial reemplazada";
}
// --- FIN: Lógica de Pistas Visuales ---
// --- Función debounce
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
// --- Activar/desactivar el botón Aplicar
inputReplacement.addEventListener('input', debounce(() => {
if (inputReplacement.value.trim() !== original)
{
applyButton.disabled = false;
applyButton.style.color = "";
}
else
{
applyButton.disabled = true;
applyButton.style.color = "#bbb";
}
}, 300));
// --- Listener para inputOriginal
inputOriginal.addEventListener('input', debounce(() => {
// Opcional: alguna lógica si se desea manejar cambios en inputOriginal
}, 300));
// --- Lógica Unificada Para Renderizar Todas Las Sugerencias ---
// ¡Esta es la ÚNICA declaración necesaria para suggestionListCell!
const suggestionListCell = document.createElement("td");
suggestionListCell.style.padding = "4px";
suggestionListCell.style.width = "180px";
const suggestionContainer = document.createElement('div');
const palabrasYaProcesadas = new Set();
// Asegurarse de que allSuggestions[id] existe
const currentPlaceSuggestions = allSuggestions[id];
// Asegurarse de que currentPlaceSuggestions es un objeto
if (currentPlaceSuggestions) {
// Aquí usamos `suggestionsArray` para evitar el conflicto con `suggestions` que podría venir de un nombre de variable anterior
Object.entries(currentPlaceSuggestions).forEach(([originalWordForThisPlace, suggestionsArray]) => {
// Verificar si 'suggestionsArray' es realmente un array antes de intentar 'forEach'.
if (Array.isArray(suggestionsArray)) {
suggestionsArray.forEach(s => { // Aquí ya NO deberías tener error de "not defined"
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':
if (palabrasYaProcesadas.has(originalWordForThisPlace.toLowerCase())) break;
const normOriginal = normalizePlaceName(originalWordForThisPlace);
const normSugerida = normalizePlaceName(s.word);
if (normOriginal.toLowerCase() !== normSugerida.toLowerCase()) {
esSugerenciaValida = true;
icono = '📘';
textoSugerencia = `¿"${normOriginal}" x "${normSugerida}"? (sim. ${(s.similarity * 100).toFixed(0)}%)`;
palabraAReemplazar = normOriginal;
palabraAInsertar = normSugerida;
}
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;
}
inputReplacement.dispatchEvent(new Event('input'));
});
suggestionContainer.appendChild(suggestionDiv);
}
});
} else {
console.warn(`[WME PLN DEBUG] suggestionsArray para "${originalWordForThisPlace}" no es un array o es undefined:`, suggestionsArray);
}//
});
}
suggestionListCell.appendChild(suggestionContainer);
// Se añaden las celdas a la fila
row.appendChild(suggestionCell);
row.appendChild(suggestionListCell);
//---------------------------------------------------------------------------------------------------------------
// Columna Categoría (nombre y luego ícono abajo)
const categoryCell = document.createElement("td");
categoryCell.style.padding = "4px";
categoryCell.style.width = "130px";
categoryCell.style.textAlign = "center"; // Centra el contenido en la celda
// Columna Categoría (nombre y luego ícono abajo)
const currentCategoryDiv = document.createElement("div"); // Contenedor para el nombre y el ícono
currentCategoryDiv.style.display = "flex";
currentCategoryDiv.style.flexDirection = "column"; // Elementos apilados verticalmente
currentCategoryDiv.style.alignItems = "center"; // Centrar horizontalmente
currentCategoryDiv.style.gap = "2px"; // Pequeño espacio entre el nombre y el ícono
// Crear el texto y el ícono de la categoría actual
const currentCategoryText = document.createElement("span");
currentCategoryText.textContent = currentCategoryTitle;
currentCategoryText.title = `Categoría Actual: ${currentCategoryTitle}`; // Tooltip para la categoría actual
currentCategoryDiv.appendChild(currentCategoryText);
// Crear el ícono de la categoría actual
const currentCategoryIconDisplay = document.createElement("span"); // Contenedor para el ícono de la categoría actual
currentCategoryIconDisplay.textContent = currentCategoryIcon; // Usar el ícono de la categoría actual
currentCategoryIconDisplay.style.fontSize = "20px"; // Tamaño del ícono
currentCategoryDiv.appendChild(currentCategoryIconDisplay);// Añadir el ícono al contenedor
// Añadir el contenedor de categoría al cell
categoryCell.appendChild(currentCategoryDiv);
row.appendChild(categoryCell);
// Columna Categoría Recomendada (Ahora con un dropdown de búsqueda)
const recommendedCategoryCell = document.createElement("td"); // Línea existente
recommendedCategoryCell.style.padding = "4px"; // : Línea existente
recommendedCategoryCell.style.width = "180px"; // : Línea existente
recommendedCategoryCell.style.textAlign = "left"; // Alinear a la izquierda para el dropdown
// Crear y adjuntar el nuevo dropdown de categoría con búsqueda
const categoryDropdown = createRecommendedCategoryDropdown( // Nueva línea
id, // ID del lugar para actualizar la categoría // Nueva línea
currentCategoryKey, // La categoría actual del lugar // Nueva línea
dynamicCategorySuggestions // Las sugerencias dinámicas // Nueva línea
); // Nueva línea
recommendedCategoryCell.appendChild(categoryDropdown); // : Nueva línea
row.appendChild(recommendedCategoryCell); // : Línea existente
//---------------------------------------------------------------------------------------------------------------
// --- Columna Acción ---
const actionCell = document.createElement("td");
actionCell.style.padding = "4px";
actionCell.style.width = "120px";
// Crear botones de acción
const buttonGroup = document.createElement("div"); // Contenedor principal de los botones de acción
buttonGroup.style.display = "flex";
buttonGroup.style.flexDirection = "column"; // VERTICAL: Apilar los elementos (wrappers de botones)
buttonGroup.style.gap = "4px"; // Espacio entre los grupos de botones
buttonGroup.style.alignItems = "flex-start"; // Alinear los grupos de botones a la izquierda
// Estilos Comunes Para Todos Los Botones De Acción Para Homogeneidad
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"
};
// 1. Botón de aplicar sugerencia (EL BOTÓN GRIS CON EL CHULITO)
const applyButton = document.createElement("button");
Object.assign(applyButton.style, commonButtonStyle);
applyButton.textContent = "✔"; // Texto o icono para el botón de aplicar
applyButton.title = "Aplicar sugerencia";
// Contenedor para el botón Aplicar y el chulito VERDE de confirmación
const applyButtonWrapper = document.createElement("div");
applyButtonWrapper.style.display = "flex"; // Horizontal: Botón y chulito uno al lado del otro
applyButtonWrapper.style.alignItems = "center";
applyButtonWrapper.style.gap = "5px";
applyButtonWrapper.appendChild(applyButton);
buttonGroup.appendChild(applyButtonWrapper); // Añadir el wrapper al grupo principal de botones
// 2. Botón de eliminar lugar (EL BOTÓN GRIS CON EL BOTE DE BASURA)
let deleteButton = document.createElement("button");
Object.assign(deleteButton.style, commonButtonStyle);
deleteButton.textContent = "🗑️";
deleteButton.title = "Eliminar lugar";
// Contenedor para el botón Eliminar y el icono de bote de basura de confirmación
const deleteButtonWrapper = document.createElement("div");
Object.assign(deleteButtonWrapper.style, { // Asegurar estilos para el wrapper del deleteButton también
display: "flex",
alignItems: "center",
gap: "5px"
});
deleteButtonWrapper.appendChild(deleteButton);
buttonGroup.appendChild(deleteButtonWrapper); // Añadir el wrapper al grupo principal de botones
// 3. Botón para añadir a la lista de exclusión (EL BOTÓN GRIS CON LA ETIQUETA)
const addToExclusionBtn = document.createElement("button");
Object.assign(addToExclusionBtn.style, commonButtonStyle); // Aplicar estilos comunes
addToExclusionBtn.textContent = "🏷️";
addToExclusionBtn.title = "Marcar palabra como especial (no se modifica)";
buttonGroup.appendChild(addToExclusionBtn); // Este botón se añade directamente al buttonGroup (no necesita un wrapper flex si siempre va solo)
actionCell.appendChild(buttonGroup); // Añadir el grupo de botones a la celda de acción
row.appendChild(actionCell);
// --- LISTENERS ---
// Relacionar botones para deshabilitarse mutuamente
//applyButton.relatedDelete = deleteButton;
//deleteButton.relatedApply = applyButton;
// Listener para el botón "APLICAR"
applyButton.addEventListener("click", async () => {
console.log(inputReplacement.value);
const venueObj = W.model.venues.getObjectById(id); // Correctly defined here
if (!venueObj)
{
console.error("Error: El lugar no está disponible o ya fue eliminado.");
return;
}
const newName = inputReplacement.value.trim();
// Verificar si el nuevo nombre es válido
const currentLiveNameInWaze = venueObj?.attributes?.name?.value || venueObj?.attributes?.name || ""; //CAMBIO
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
let combinedUpdates = {};
//Compara newName con el nombre actual en Waze, que aún podría tener emoticones
if (newName !== currentLiveNameInWaze)
combinedUpdates.name = newName;
if (Object.keys(combinedUpdates).length > 0)
{
const combinedAction = new UpdateObject(venueObj, combinedUpdates); // Changed 'venue' to 'venueObj'
W.model.actionManager.add(combinedAction);
// Deshabilitar botón y mostrar éxito
applyButton.disabled = true;
applyButton.style.color = "#bbb";
applyButton.style.opacity = "0.5";
if (applyButton.relatedDelete)
{
applyButton.relatedDelete.disabled = true;
applyButton.relatedDelete.style.color = "#bbb";
applyButton.relatedDelete.style.opacity = "0.5";
}
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.marginLeft = "0";
successIcon.style.fontSize = "20px";
applyButtonWrapper.appendChild(successIcon); // Añadir al applyButtonWrapper
// Actualizar el campo del dropdown de categoría para reflejar la categoría REAL del place.
setTimeout(() => {
const latestVenueState = W.model.venues.getObjectById(id);
if (latestVenueState)
{
const latestCategoryKey = getPlaceCategoryName(latestVenueState, venueSDKForRender); // Asegúrate de que esta línea esté presente para usar latestCategoryKey si lo necesitas.
const categoryDropdownInput = recommendedCategoryCell.querySelector('input'); // ¡Obtener la referencia DENTRO del setTimeout!
if (categoryDropdownInput)
{ // Añadir esta comprobación para mayor robustez
categoryDropdownInput.value = ""; // Reiniciar a vacío para que muestre el placeholder
const existingDropdownCheck = categoryDropdownInput.parentElement.querySelector('.category-update-check');
if (existingDropdownCheck) existingDropdownCheck.remove();
}
}
}, 100); // Pequeño retraso para la sincronización del modelo de Waze.
}
else
{
console.log("No hay cambios en el nombre para aplicar.");
}
}
catch (e)
{
alert("Error al actualizar: " + e.message);
console.error("Error al actualizar lugar:", e);
}
});
// Listener para el botón de "ELIMINAR"
deleteButton.addEventListener("click", () => {
// Modal bonito de confirmación
const confirmModal = document.createElement("div");
confirmModal.style.position = "fixed";
confirmModal.style.top = "50%";
confirmModal.style.left = "50%";
confirmModal.style.transform = "translate(-50%, -50%)";
confirmModal.style.background = "#fff";
confirmModal.style.border = "1px solid #aad";
confirmModal.style.padding = "28px 32px 20px 32px";
confirmModal.style.zIndex = "20000"; // Z-INDEX AUMENTADO
confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
confirmModal.style.fontFamily = "sans-serif";
confirmModal.style.borderRadius = "10px";
confirmModal.style.textAlign = "center";
confirmModal.style.minWidth = "340px";
// Ícono visual
const iconElement = document.createElement("div");
iconElement.innerHTML = "⚠️";
iconElement.style.fontSize = "38px";
iconElement.style.marginBottom = "10px";
confirmModal.appendChild(iconElement);
// Mensaje principal
const message = document.createElement("div");
const venue = W.model.venues.getObjectById(id);
// Asegurarse de que placeName siempre tenga un valor para mostrar en el modal
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)
{
console.error("El lugar no está disponible o ya fue eliminado.");
confirmModal.remove(); // Asegurarse de cerrar el modal
return;
}
try
{
const DeleteObject = require("Waze/Action/DeleteObject");
const action = new DeleteObject(venue);
W.model.actionManager.add(action); // Ejecutar la acción de eliminación en Waze
// Deshabilitar el botón de eliminar actual
deleteButton.disabled = true;
deleteButton.style.color = "#bbb";
deleteButton.style.opacity = "0.5";
// Deshabilitar el botón de aplicar de esta fila
applyButton.disabled = true;
applyButton.style.color = "#bbb";
applyButton.style.opacity = "0.5";
// Añadir el icono de éxito visual al lado del botón de eliminar
const successIcon = document.createElement("span");
successIcon.textContent = " 🗑️";
successIcon.style.marginLeft = "0";
successIcon.style.fontSize = "20px";
deleteButtonWrapper.appendChild(successIcon);
}
catch (e)
{
console.error("Error al eliminar lugar: " + e.message, e);
}
confirmModal.remove(); // Cerrar el modal después de la acción (éxito o fallo)
});
buttonWrapper.appendChild(cancelBtn);
buttonWrapper.appendChild(confirmBtn);
confirmModal.appendChild(buttonWrapper);
document.body.appendChild(confirmModal); // Añadir el modal al body para que sea visible
});
// Listener para el botón de añadir a exclusión
addToExclusionBtn.addEventListener("click", () => {
const words = original.split(/\s+/);
const modal = document.createElement("div");
// ... (resto del código del modal de exclusión) ...
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%)";
modal.style.background = "#fff";
modal.style.border = "1px solid #ccc";
modal.style.padding = "10px";
modal.style.zIndex = "20000";
modal.style.maxWidth = "300px";
const title = document.createElement("h4");
title.textContent = "Agregar palabra a especiales";
modal.appendChild(title);
const list = document.createElement("ul");
list.style.listStyle = "none";
list.style.padding = "0";
words.forEach(w => {
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;
}
});
// Actualizar el mapa de palabras excluidas
if (wordsActuallyAdded) {
if (typeof renderExcludedWordsList === 'function') {
const excludedListElement = document.getElementById("excludedWordsList");
if (excludedListElement) {
renderExcludedWordsList(excludedListElement);
} else {
renderExcludedWordsList();
}
}
}
modal.remove();
});
modal.appendChild(confirmBtn);
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancelar";
cancelBtn.style.marginLeft = "8px";
cancelBtn.addEventListener("click", () => modal.remove());
modal.appendChild(cancelBtn);
document.body.appendChild(modal);
});
buttonGroup.appendChild(addToExclusionBtn);
// buttonGroup.appendChild(addToDictionaryBtn);
actionCell.appendChild(buttonGroup);
row.appendChild(actionCell);
//-----------------------------------------------------------------------------------------------------------------------------------
// Añadir borde inferior visible entre cada lugar
row.style.borderBottom = "1px solid #ddd";
row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff";
row.querySelectorAll("td").forEach(td => {
td.style.verticalAlign = "top";
});
tbody.appendChild(row);
// Actualizar progreso al final del ciclo usando setTimeout para
// liberar el hilo visual
setTimeout(() => {
const progress =
Math.floor(((index + 1) / inconsistents.length) * 100);
const progressElem =
document.getElementById("scanProgressText");
if (progressElem)
{
progressElem.textContent = `Analizando lugares: ${
progress}% (${index + 1}/${inconsistents.length})`;
}
}, 0);
});
table.appendChild(tbody);// Añadir el tbody a la tabla
output.appendChild(table);// Añadir la tabla al contenedor de salida
// Log de cierre
// console.log("✔ Panel finalizado y tabla renderizada.");
// Quitar overlay spinner justo antes de mostrar la tabla
const existingOverlay = document.getElementById("scanSpinnerOverlay");
if (existingOverlay)
{
existingOverlay.remove();
}
// Al finalizar, actualizar el texto final en el tab principal (progreso
// 100%)
const progressBarInnerTab =
document.getElementById("progressBarInnerTab");
const progressBarTextTab =
document.getElementById("progressBarTextTab");
if (progressBarInnerTab && progressBarTextTab)
{
progressBarInnerTab.style.width = "100%";
progressBarTextTab.textContent =
`Progreso: 100% (${inconsistents.length}/${placesArr.length})`;
}
// Función para reactivar todos los botones de acción en el panel
// flotante
function reactivateAllActionButtons()
{
document.querySelectorAll("#wme-place-inspector-output button")
.forEach(btn => {
btn.disabled = false;
btn.style.color = "";
btn.style.opacity = "";
});
}
W.model.actionManager.events.register("afterundoaction", null, () => {
// Verificar si el panel flotante está visible
if (floatingPanelElement && floatingPanelElement.style.display !== 'none') {
waitForWazeAPI(() => {
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
setTimeout(reactivateAllActionButtons, 250);
});
} else {
console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
// Opcionalmente, solo resetear el estado del inspector si el panel no está visible
// resetInspectorState(); // Descomentar si se desea este comportamiento
}
});
W.model.actionManager.events.register("afterredoaction", null, () => {
// Verificar si el panel flotante está visible
if (floatingPanelElement && floatingPanelElement.style.display !== 'none') {
waitForWazeAPI(() => {
const places = getVisiblePlaces();
renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
setTimeout(reactivateAllActionButtons, 250);
});
} else {
console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
// Opcionalmente, solo resetear el estado del inspector si el panel no está visible
// resetInspectorState(); // Descomentar si se desea este comportamiento
}
});
// Mostrar el panel flotante al terminar el procesamiento
// createFloatingPanel(inconsistents.length); // Ahora se invoca arriba
// si output existe
}
}// renderPlacesInFloatingPanel
function getLevenshteinDistance(a, b)
{
const matrix = Array.from(
{ length : b.length + 1 },
(_, i) => Array.from({ length : a.length + 1 },
(_, j) => (i === 0 ? j : (j === 0 ? i : 0))));
for (let i = 1; i <= b.length; i++)
{
for (let j = 1; j <= a.length; j++)
{
if (b.charAt(i - 1) === a.charAt(j - 1))
{
matrix[i][j] = matrix[i - 1][j - 1];
}
else
{
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + 1 // substitution
);
}
}
}
return matrix[b.length][a.length];
}
function calculateSimilarity(word1, word2)
{
const distance =
getLevenshteinDistance(word1.toLowerCase(), word2.toLowerCase());
const maxLen = Math.max(word1.length, word2.length);
return 1 - distance / maxLen;
}
function findSimilarWords(word, indexedListOrArray, threshold)
{
const lowerWord = word.toLowerCase();
const firstChar = lowerWord.charAt(0);
let candidates = [];
// === Lógica CLAVE para usar el índice ===
// Si el segundo argumento es un Map (como excludedWordsMap)
if (indexedListOrArray instanceof Map) {
candidates = Array.from(indexedListOrArray.get(firstChar) || []);
}
// Si el segundo argumento es un objeto literal (como window.dictionaryIndex)
else if (indexedListOrArray && typeof indexedListOrArray === 'object' && !Array.isArray(indexedListOrArray) && indexedListOrArray[firstChar]) {
candidates = Array.from(indexedListOrArray[firstChar] || []); // window.dictionaryIndex almacena arrays, asegúrate de que sean copiados o Set a Array.from
}
// Si es un Set o Array (menos óptimo, pero fallback)
else if (indexedListOrArray instanceof Set || Array.isArray(indexedListOrArray)) {
// Este es un fallback, filtra por primera letra aquí si no hay Map/objeto índice
candidates = Array.from(indexedListOrArray).filter(candidate => candidate.charAt(0).toLowerCase() === firstChar);
} else {
return []; // No hay candidatos válidos para buscar
}
return candidates
.map(candidate => {
const similarity = calculateSimilarity(lowerWord, candidate.toLowerCase());
return { word : candidate, similarity };
})
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity);
}
function suggestExcludedReplacements(currentName, excludedWords)
{
const words = currentName.split(/\s+/);
const suggestions = {};
const threshold =
parseFloat(document.getElementById("similarityThreshold")?.value ||
"85") /
100;
words.forEach(word => {
const similar =
findSimilarWords(word, Array.from(excludedWords), threshold);
if (similar.length > 0)
{
suggestions[word] = similar;
}
});
return suggestions;
}
// Reset del inspector: progreso y texto de tab
function resetInspectorState()
{
const inner = document.getElementById("progressBarInnerTab");
const text = document.getElementById("progressBarTextTab");
const outputTab = document.getElementById("wme-normalization-tab-output");
if (inner)
inner.style.width = "0%";
if (text)
text.textContent = `Progreso: 0% (0/0)`;
if (outputTab)
outputTab.textContent = "Presiona 'Start Scan...' para analizar los lugares visibles.";
}
//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.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";
// Dimensiones del panel
const closeBtn = document.createElement("span");
closeBtn.textContent = "×";
closeBtn.style.position = "absolute";
closeBtn.style.top = "8px";
closeBtn.style.right = "12px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontSize = "22px";
closeBtn.style.color = "#555";
closeBtn.title = "Cerrar panel";
closeBtn.addEventListener("click", () => {
if (floatingPanelElement) floatingPanelElement.style.display = 'none';
resetInspectorState();
});
floatingPanelElement.appendChild(closeBtn);
// Dimensiones del panel de procesamiento
const titleElement = document.createElement("h4");
titleElement.id = "wme-pln-panel-title";
titleElement.style.marginTop = "0";
titleElement.style.marginBottom = "10px";
titleElement.style.fontSize = "20px";
titleElement.style.color = "#333";
titleElement.style.textAlign = "center";
titleElement.style.fontWeight = "bold";
floatingPanelElement.appendChild(titleElement);
// Dimensiones del panel de resultados
const outputDivLocal = document.createElement("div");
outputDivLocal.id = "wme-place-inspector-output";
outputDivLocal.style.fontSize = "18px";
outputDivLocal.style.backgroundColor = "#fdfdfd";
outputDivLocal.style.overflowY = "auto";
floatingPanelElement.appendChild(outputDivLocal);
document.body.appendChild(floatingPanelElement);
}
// Dimensiones del panel de procesamiento
const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title");
// Dimensiones del panel de resultados
const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
// Dimensiones del panel de procesamiento
if(outputDiv) outputDiv.innerHTML = "";
// Dimensiones del panel de procesamiento
if (status === "processing")
{
floatingPanelElement.style.width = processingPanelDimensions.width;
floatingPanelElement.style.height = processingPanelDimensions.height;
if(outputDiv) outputDiv.style.height = "150px";
if(titleElement) titleElement.textContent = "Buscando...";
if (outputDiv)
{
outputDiv.innerHTML = "<div style='display:flex; align-items:center; justify-content:center; height:100%;'><span class='loader-spinner' style='width:32px; height:32px; border:4px solid #ccc; border-top:4px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span></div>";
}
// Centrar el panel de procesamiento
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "50%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
else
{ // status === "results"
floatingPanelElement.style.width = resultsPanelDimensions.width;
floatingPanelElement.style.height = resultsPanelDimensions.height;
if(outputDiv) outputDiv.style.height = "660px";
if(titleElement) titleElement.textContent = "Resultado de la búsqueda";
// Mover el panel de resultados más a la derecha
floatingPanelElement.style.top = "50%";
floatingPanelElement.style.left = "60%";
floatingPanelElement.style.transform = "translate(-50%, -50%)";
}
floatingPanelElement.style.display = 'flex';
floatingPanelElement.style.flexDirection = 'column';
}
// Escuchar el botón Guardar de WME para resetear el inspector
const wmeSaveBtn = document.querySelector(
"button.action.save, button[title='Guardar'], button[aria-label='Guardar']");
if (wmeSaveBtn)
{
wmeSaveBtn.addEventListener("click", () => resetInspectorState());
}
function createSidebarTab()
{
try
{
// 1. Verificar si WME y la función para registrar pestañas están listos
if (!W || !W.userscripts ||
typeof W.userscripts.registerSidebarTab !== 'function')
{
console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral.");
return;
}
// 2. Registrar la pestaña principal del script en WME y obtener tabPane
let registration;
try
{
registration = W.userscripts.registerSidebarTab(
"NrmliZer"); // Nombre del Tab que aparece en WME
}
catch (e)
{
if (e.message.includes("already been registered"))
{
console.warn(
"[WME PLN] Tab 'NrmliZer' ya registrado. El script puede no funcionar como se espera si hay múltiples instancias.");
// Podrías intentar obtener el tabPane existente o simplemente
// retornar. Para evitar mayor complejidad, si ya está
// registrado, no continuaremos con la creación de la UI de la
// pestaña.
return;
}
//console.error("[WME PLN] Error registrando el sidebar tab:", e);
throw e; // Relanzar otros errores para que se vean en consola
}
const { tabLabel, tabPane } = registration;
if (!tabLabel || !tabPane)
{
//console.error("[WME PLN] Falló el registro del Tab: 'tabLabel' o 'tabPane' no fueron retornados.");
return;
}
// Configurar el ícono y nombre de la pestaña principal del script
tabLabel.innerHTML = `
<img src="data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q=="
style="height: 16px; vertical-align: middle; margin-right: 5px;">
NrmliZer
`;
// 3. Inicializar las pestañas internas (General, Especiales,
// Diccionario, Reemplazos)
const tabsContainer = document.createElement("div");
tabsContainer.style.display = "flex";
tabsContainer.style.marginBottom = "8px";
tabsContainer.style.gap = "8px";
const tabButtons = {};
const tabContents = {}; // Objeto para guardar los divs de contenido
// Crear botones para cada pestaña
tabNames.forEach(({ label, icon }) => {
const btn = document.createElement("button");
btn.innerHTML = icon
? `<span style="display: inline-flex; align-items: center; font-size: 11px;">
<span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label}
</span>`
: `<span style="font-size: 11px;">${label}</span>`;
btn.style.fontSize = "11px";
btn.style.padding = "4px 8px";
btn.style.marginRight = "4px";
btn.style.minHeight = "28px";
btn.style.border = "1px solid #ccc";
btn.style.borderRadius = "4px 4px 0 0";
btn.style.cursor = "pointer";
btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada
btn.className = "custom-tab-style";
// Agrega el tooltip personalizado para cada tab
if (label === "Gene") btn.title = "Configuración general";
else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)";
else if (label === "Dicc") btn.title = "Diccionario de palabras válidas";
else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos";
// Estilo inicial: la primera pestaña es la activa
if (label === tabNames[0].label) {
btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco)
btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa
btn.style.fontWeight = "bold";
} else {
btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro)
btn.style.fontWeight = "normal";
}
btn.addEventListener("click", () => {
tabNames.forEach(({ label: tabLabel_inner }) => {
const isActive = (tabLabel_inner === label);
const currentButton = tabButtons[tabLabel_inner];
if (tabContents[tabLabel_inner]) {
tabContents[tabLabel_inner].style.display = isActive ? "block" : "none";
}
if (currentButton) {
// Aplicar/Quitar estilos de pestaña activa directamente
if (isActive) {
currentButton.style.backgroundColor = "#ffffff"; // Activo
currentButton.style.borderBottom = "2px solid #007bff";
currentButton.style.fontWeight = "bold";
} else {
currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo
currentButton.style.borderBottom = "none";
currentButton.style.fontWeight = "normal";
}
}
// Llamar a la función de renderizado correspondiente
if (isActive) {
if (tabLabel_inner === "Espe")
{
const ul = document.getElementById("excludedWordsList");
if (ul && typeof renderExcludedWordsList === 'function') renderExcludedWordsList(ul);
}
else if (tabLabel_inner === "Dicc")
{
const ulDict = document.getElementById("dictionaryWordsList");
if (ulDict && typeof renderDictionaryList === 'function') renderDictionaryList(ulDict);
}
else if (tabLabel_inner === "Reemp")
{
const ulReemplazos = document.getElementById("replacementsListElementID");
if (ulReemplazos && typeof renderReplacementsList === 'function') renderReplacementsList(ulReemplazos);
}
}
});
});
tabButtons[label] = btn;
tabsContainer.appendChild(btn);
});
tabPane.appendChild(tabsContainer);
// Crear los divs contenedores para el contenido de cada pestaña
tabNames.forEach(({ label }) => {
const contentDiv = document.createElement("div");
contentDiv.style.display = label === tabNames[0].label ? "block" : "none"; // Mostrar solo la primera
contentDiv.style.padding = "10px";
tabContents[label] = contentDiv; // Guardar referencia
tabPane.appendChild(contentDiv);
});
// --- POBLAR EL CONTENIDO DE CADA PESTAÑA ---
// 4. Poblar el contenido de la pestaña "General"
const containerGeneral = tabContents["Gene"];
if (containerGeneral)
{
let initialUsernameAttempt =
"Pendiente"; // Para la etiqueta simplificada
// No es necesario el polling complejo si solo es para la lógica
// interna del checkbox
if (typeof W !== 'undefined' && W.loginManager &&
W.loginManager.user && W.loginManager.user.userName)
{
initialUsernameAttempt =
W.loginManager.user
.userName; // Se usará internamente en processNextPlace
}
// 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
const versionInfo = document.createElement("div");
versionInfo.textContent =
"V. " + VERSION; // VERSION global
versionInfo.style.textAlign = "right";
versionInfo.style.fontSize = "10px";
versionInfo.style.color = "#777";
versionInfo.style.marginBottom = "15px";
containerGeneral.appendChild(versionInfo);
// Crear el contenedor para el checkbox de usuario
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);
// Crear el checkbox para activar/desactivar la normalización
const scanButton = document.createElement("button");
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", () => {
const places = getVisiblePlaces();
const outputDiv =
document.getElementById("wme-normalization-tab-output");
if (!outputDiv)
{ // Mover esta verificación antes
// console.error("Div de salida (wme-normalization-tab-output) no encontrado en el tab.");
return;
}
if (places.length === 0)
{
outputDiv.textContent = "No se encontraron lugares visibles para analizar.";
return;
}
const maxPlacesInput = document.getElementById("maxPlacesInput");
const maxPlacesToScan = parseInt(maxPlacesInput?.value || "100", 10);
const scannedCount = Math.min(places.length, maxPlacesToScan);
outputDiv.textContent = `Escaneando ${scannedCount} lugares...`;
setTimeout(() => {renderPlacesInFloatingPanel(places.slice(0, maxPlacesToScan));
}, 10);
});
containerGeneral.appendChild(scanButton);
// 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.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.alignItems = "center";
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
// Añadir un poco de sombra
const recommendCategoriesCheckbox = document.createElement("input");
recommendCategoriesCheckbox.type = "checkbox";
recommendCategoriesCheckbox.id = "chk-recommend-categories";
recommendCategoriesCheckbox.style.marginRight = "8px"; // Más espacio al icono/texto
// Recuperar el estado guardado del checkbox
const savedCategoryRecommendationState = localStorage.getItem("wme_pln_recommend_categories");
recommendCategoriesCheckbox.checked = (savedCategoryRecommendationState === "true");
// Crear la etiqueta con estilo
const recommendCategoriesLabel = document.createElement("label");
recommendCategoriesLabel.htmlFor = "chk-recommend-categories";
recommendCategoriesLabel.style.fontSize = "14px"; // Un poco más grande
recommendCategoriesLabel.style.cursor = "pointer";
recommendCategoriesLabel.style.fontWeight = "bold"; // Texto en negrita
recommendCategoriesLabel.style.color = "#00796b"; // Color de texto más oscuro
recommendCategoriesLabel.style.display = "flex";
recommendCategoriesLabel.style.alignItems = "center";
// Añadir un icono al inicio de la etiqueta
const iconSpan = document.createElement("span");
iconSpan.innerHTML = "✨ "; // Icono llamativo (ej. estrella, brillo)
iconSpan.style.marginRight = "4px"; // Espacio entre icono y texto
iconSpan.style.fontSize = "16px"; // Tamaño del icono
// Añadir el icono y el texto a la etiqueta
iconSpan.appendChild(document.createTextNode("Recomendar categorías")); // Mueve el texto dentro del iconSpan
recommendCategoriesLabel.appendChild(iconSpan); // Añade iconSpan (que ahora tiene el texto) a la label
// Añadir el checkbox y la etiqueta al contenedor
recommendCategoriesWrapper.appendChild(recommendCategoriesCheckbox);
recommendCategoriesWrapper.appendChild(recommendCategoriesLabel);
containerGeneral.appendChild(recommendCategoriesWrapper);
// Guardar el estado del checkbox cada vez que cambia
recommendCategoriesCheckbox.addEventListener("change", () => {
localStorage.setItem("wme_pln_recommend_categories", recommendCategoriesCheckbox.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
{
console.error("[WME PLN] No se pudo poblar la pestaña 'General' porque su contenedor no existe.");
}
// 5. Poblar las otras pestañas
if (tabContents["Espe"])
createExcludedWordsManager(tabContents["Espe"]) ;
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'.");
}
if (tabContents["Dicc"])
{
createDictionaryManager(tabContents["Dicc"]);
}
else
{
console.error(
"[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Diccionario'.");
}
// --- LLAMADA A LA FUNCIÓN PARA POBLAR LA NUEVA PESTAÑA "Reemplazos"
// ---
if (tabContents["Reemp"])
{
createReplacementsManager(tabContents["Reemp"]); // Esta es la llamada clave
}
else
{
console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'.");
}
}
catch (error)
{
console.error("[WME PLN] Error creando la pestaña lateral:", error, error.stack);
}
} // Fin de createSidebarTab
// 2. Esperar a que Waze API esté disponible
function waitForSidebarAPI()
{
// Comprobar si Waze API está disponible
if (W && W.userscripts && W.userscripts.registerSidebarTab)
{
const savedExcluded = localStorage.getItem("excludedWordsList");
if (savedExcluded)
{
try
{
const parsed = JSON.parse(savedExcluded);
excludedWords = new Set(); // Reinicializa el Set
excludedWordsMap = new Map(); // Reinicializa el Map
parsed.forEach(word => { // parsed es el array del JSON
excludedWords.add(word);
const firstChar = word.charAt(0).toLowerCase();
if (!excludedWordsMap.has(firstChar)) {
excludedWordsMap.set(firstChar, new Set());
}
excludedWordsMap.get(firstChar).add(word);
});
/* console.log(
"[WME PLN] Palabras especiales restauradas desde
localStorage:", Array.from(excludedWords));*/
}
catch (e)
{
/*console.error(
"[WME PLN] Error al cargar excludedWordsList del localStorage:",
e);*/
excludedWords = new Set();
}
}
else
{
excludedWords = new Set();
/* console.log(
"[WME PLN] No se encontraron palabras especiales en
localStorage.");*/
}
// --- Cargar diccionario desde localStorage ---
const savedDictionary = localStorage.getItem("dictionaryWordsList");
if (savedDictionary)
{
try
{
const parsed = JSON.parse(savedDictionary);
window.dictionaryWords = new Set(parsed);
// Crear el índice de palabras por letra
window.dictionaryIndex = {};
// Iterar sobre las palabras y agregarlas al índice
parsed.forEach(word => {
const letter = word.charAt(0).toLowerCase();
if (!window.dictionaryIndex[letter])
window.dictionaryIndex[letter] = [];
window.dictionaryIndex[letter].push(word);
});
}
catch (e)
{
console.error("[WME PLN] Error al cargar dictionaryWordsList del localStorage:", e);
window.dictionaryWords = new Set();
window.dictionaryIndex = {};
}
}
else
{
window.dictionaryWords = new Set();
window.dictionaryIndex = {};
// console.log("[WME PLN] No se encontró diccionario en
// localStorage.");
}
// Esto añadirá nuevas palabras del Excel a window.dictionaryWords
// y se encarga de guardar en localStorage después.
// Se hace de forma asíncrona pero no bloquea la UI.
loadDictionaryWordsFromSheet().then(() => {
console.log('[WME PLN] Carga del diccionario desde Google Sheets finalizada.');
}).catch(err => {
console.error('[WME PLN] Fallo en la carga del diccionario desde Google Sheets:', err);
});
// --- Cargar palabras de reemplazo desde localStorage ---
loadReplacementWordsFromStorage();
// La llamada a waitForWazeAPI ya se encarga de la lógica de dynamicCategoriesLoaded.
waitForWazeAPI(() => { createSidebarTab(); });
}
else
{
// console.log("[WME PLN] Esperando W.userscripts API...");
setTimeout(waitForSidebarAPI, 1000);
}
}// Fin de waitForSidebarAPI
// 1. normalizePlaceName
function normalizePlaceName(word)
{
//console.log("[NORMALIZER] Analizando nombre:", word);
if (!word || typeof word !== "string")
return "";
// Si la palabra está en la lista de excluidas, respetarla tal cual
const cleanedLowerNoDiacritics = removeDiacritics(word.toLowerCase());
// Si la palabra está en la lista de excluidas (ignorando diacríticos y mayúsculas/minúsculas), respetarla tal cual.
// Esto cubre casos como "O'Clock" y "i-Store" si están en la lista de excluidas.**
if (excludedWords && excludedWordsMap)
{ // Asegura que excludedWordsMap exista
const firstChar = word.charAt(0).toLowerCase();
const excludedCandidates = excludedWordsMap.get(firstChar);
// Verifica si hay candidatos para el primer carácter
if (excludedCandidates)
for (const excludedWord of excludedCandidates)
if (removeDiacritics(excludedWord.toLowerCase()) === cleanedLowerNoDiacritics)
return excludedWord; // Devuelve la forma exacta de la lista de excluidas
}
// Manejar palabras con "/" recursivamente
if (word.includes("/"))
{
if (word === "/") return "/";
return word.split("/").map(part => normalizePlaceName(part.trim())).join("/");
}
// Regla 1: Si la palabra es SOLO números, mantenerla tal cual. (Prioridad alta)
if (/^[0-9]+$/.test(word))
return word;
// Regla 2: Números seguidos de letras (sin espacio)
word = word.replace(/(\d)([a-zA-Z])/g, (_, num, letter) => `${num}${letter.toUpperCase()}`);
// Regla 3: Números romanos: todo en mayúsculas
const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
if (romanRegexStrict.test(word))
return word.toUpperCase();
// Regla 4: Acrónimos/Palabras con puntos/letras mayúsculas que deben mantenerse. Esto es para "St." o "U.S.A." o "EPM", "SURA"
// NOTA: originalNameFull ya no tiene emoticones gracias a `processNextPlace`
if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && (word.includes('.') || /^[A-ZÁÉÍÓÚÑ]+$/.test(word))) {
// Asegurarse de que no sea "MI", "DI", "SI" si están en mayúsculas accidentales
if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI" || word.toUpperCase() === "SI")
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
return word; // Mantener como está
}
// Regla 5: Capitalización estándar para el resto de las palabras.
// Esta será la regla para la mayoría de las palabras que no caen en las anteriores.
let normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
return normalizedWord;
}// Fin de normalizePlaceName
// Función para escapar caracteres especiales en una cadena para usar en regex
function applyWordsToStartMovement(name)
{
let newName = name;
// Asegurarse de que window.swapWords exista y no esté vacío
if (!window.swapWords || window.swapWords.length === 0) {
return newName; // No hay palabras para mover
}
// Ordenar las palabras swap por longitud descendente para procesar primero las frases más largas.
// Esto es crucial para evitar que una palabra corta (ej. "Club") se mueva antes que una frase más larga que la contiene
// (ej. "Club Campestre"), si ambas estuvieran en la lista.
const sortedSwapWords = [...window.swapWords].sort((a, b) => b.length - a.length);
for (const swapWord of sortedSwapWords) {
// Regex para encontrar la palabra swap al final del nombre.
// \s* : Cero o más espacios antes de la palabra.
// (${escapeRegExp(swapWord)}) : Captura la palabra swap (escapando caracteres especiales de regex).
// \s*$ : Cero o más espacios al final del nombre, seguido del fin de la cadena.
// 'i' : Para búsqueda insensible a mayúsculas/minúsculas.
const regex = new RegExp(`\\s*(${escapeRegExp(swapWord)})\\s*$`, 'i');
if (regex.test(newName)) {
// Captura la parte del nombre que coincide con la palabra swap al final.
const match = newName.match(regex);
const matchedSwapWord = match[1]; // La palabra/frase real que coincidió (ej. "apartamentos", "Urbanización")
// Elimina la palabra swap del final del nombre para obtener el resto.
const remainingName = newName.replace(regex, '').trim();
// Capitaliza la palabra movida para que aparezca correctamente al inicio.
// Si ya está correctamente capitalizada (ej. "Urbanización"), se mantiene así.
//const capitalizedSwapWord = matchedSwapWord.charAt(0).toUpperCase() + matchedSwapWord.slice(1).toLowerCase();
const capitalizedSwapWord = capitalizeEachWord(matchedSwapWord); // Usar la nueva función aquí
// Reconstruye el nombre: Palabra movida + espacio + resto del nombre.
newName = `${capitalizedSwapWord} ${remainingName}`.trim();
// Una vez que se mueve una palabra, asumimos que no hay más movimientos
// que hacer con otras palabras swap para esta misma entrada,
// a menos que quieras permitir múltiples movimientos, lo cual
// complicaría la lógica. Por ahora, nos detenemos en la primera coincidencia.
break;
}
}
return newName;
}//applyWordsToStartMovement
function createCategoryDropdown(currentCategoryKey, rowIndex, venue)
{
const select = document.createElement("select");
select.style.padding = "4px";
select.style.borderRadius = "4px";
select.style.fontSize = "12px";
select.title = "Selecciona una categoría";
select.id = `categoryDropdown-${rowIndex}`;
Object.entries(categoryIcons).forEach(([key, value]) => {
const option = document.createElement("option");
option.value = key;
option.textContent = `${value.icon} ${value.en}`;
if (key === currentCategoryKey)
option.selected = true;
select.appendChild(option);
});
// Evento: al cambiar la categoría
select.addEventListener("change", (e) => {
const selectedCategory = e.target.value;
if (!venue || !venue.model || !venue.model.attributes)
{
console.error("Venue inválido al intentar actualizar la categoría");
return;
}
// Actualizar la categoría en el modelo
venue.model.attributes.categories = [selectedCategory];
venue.model.save();
// Mensaje opcional de confirmación
WazeWrap.Alerts.success("Categoría actualizada", `Nueva categoría: ${categoryIcons[selectedCategory].en}`);
});
return select;
}
function normalizeWordInternal(word, isFirstWordInSequence = false, isInsideQuotesOrParentheses = false)
{
if (!word || typeof word !== "string") return "";
// Casos especiales "MI" y "DI" tienen la MÁS ALTA prioridad.
if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI" || word.toUpperCase() === "SI")
{
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
// Usar la regex insensible para la detección de romanos
const romanRegexInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
// Si es un número romano (y no es "MI" o "DI", aunque ya se cubrió arriba), convertir a mayúsculas.
if (romanRegexInsensitive.test(word)) { // No es necesario verificar MI/DI de nuevo debido a la primera condición.
return word.toUpperCase();
}
word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`);
let resultWord;
if (isInsideQuotesOrParentheses && !isFirstWordInSequence && commonWords.includes(word.toLowerCase()))
{
resultWord = word.toLowerCase();
} else if (/^[0-9]+$/.test(word)) {
resultWord = word;
} else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && word.includes('.')) {
// Mantener "St." dentro de comillas/paréntesis. No debería afectar a "MI".
resultWord = word;
}
else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1) {
// Mantener acrónimos sin puntos (ej. "ABC"). "MI" ya no caerá .
resultWord = word;
}
else {
// Capitalización estándar para todo lo demás.
resultWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
return resultWord;
}
// 3. La función postProcessQuotesAndParentheses (CORREGIDA de la respuesta anterior)
function postProcessQuotesAndParentheses(text)
{
if (typeof text !== 'string') return text;
// Normalizar contenido dentro de comillas dobles
text = text.replace(/"([^"]*)"/g, (match, content) => {
const trimmedContent = content.trim();
if (trimmedContent === "") return '""';
const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0);
const normalizedWordsInside = wordsInside.map((singleWord, index) => {
return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses
}).join(' ');
return `"${normalizedWordsInside}"`; // Sin espacios extra
});
// Normalizar contenido dentro de paréntesis
text = text.replace(/\(([^)]*)\)/g, (match, content) => {
const trimmedContent = content.trim();
if (trimmedContent === "") return '()';
const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0);
const normalizedWordsInside = wordsInside.map((singleWord, index) => {
return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses
}).join(' ');
return `(${normalizedWordsInside})`; // Sin espacios extra
});
return text.replace(/\s+/g, ' ').trim(); // Limpieza final general
}
// === Palabras especiales ===
let excludedWords = new Set(); // Mantenemos el Set para facilitar el renderizado original
let excludedWordsMap = new Map(); // <-- NUEVO: Para la búsqueda optimizada
let dictionaryWords = new Set(); // O window.dictionaryWords = new Set();
function createExcludedWordsManager(parentContainer)
{
const section = document.createElement("div");
section.id = "excludedWordsManagerSection"; // ID para la sección
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
// Título de la sección
const title = document.createElement("h4"); // Cambiado a h4 para jerarquía
title.textContent = "Gestión de Palabras Especiales";
title.style.fontSize = "15px"; // Consistente con el otro título de sección
title.style.marginBottom = "10px"; // Más espacio abajo
section.appendChild(title);
// Contenedor para los controles de añadir palabra
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems =
"center"; // Alinear verticalmente
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra o frase";
input.style.flexGrow = "1";
input.style.padding = "6px"; // Mejor padding
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px"; // Mejor padding
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function() {
const newWord = input.value.trim();
const validation = isValidExcludedWord(newWord);
if (!validation.valid)
{
alert(validation.msg);
return;
}
// isValidExcludedWord ya comprueba duplicados en excludedWords y
// commonWords y ahora también si existe en dictionaryWords.
excludedWords.add(newWord);
const firstCharNew = newWord.charAt(0).toLowerCase();
if (!excludedWordsMap.has(firstCharNew))
{
excludedWordsMap.set(firstCharNew, new Set());
}
excludedWordsMap.get(firstCharNew).add(newWord); // Añadir al Map optimizado
input.value = "";
renderExcludedWordsList(
document.getElementById("excludedWordsList"));
saveExcludedWordsToLocalStorage();
});
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px"; // Más espacio
const exportBtn = document.createElement("button");
exportBtn.textContent = "Exportar"; // Más corto
exportBtn.title = "Exportar Lista a XML";
exportBtn.style.padding = "6px 10px";
exportBtn.style.cursor = "pointer";
exportBtn.addEventListener("click", exportSharedDataToXml);
actionButtonsContainer.appendChild(exportBtn);
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar"; // Más corto
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function() {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?"))
{
excludedWords.clear();
renderExcludedWordsList(document.getElementById("excludedWordsList")); // Pasar el elemento UL
}
});
actionButtonsContainer.appendChild(clearBtn);
section.appendChild(actionButtonsContainer);
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en especiales...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)"; // Considerar padding y borde
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginBottom = "5px";
search.addEventListener("input", () => {
// Pasar el ulElement directamente
renderExcludedWordsList(
document.getElementById("excludedWordsList"),
search.value.trim());
});
section.appendChild(search);
const listContainerElement = document.createElement("ul");
listContainerElement.id = "excludedWordsList"; // Este es el UL
listContainerElement.style.maxHeight = "150px";
listContainerElement.style.overflowY = "auto";
listContainerElement.style.border = "1px solid #ddd";
listContainerElement.style.padding = "5px"; // Padding interno
listContainerElement.style.margin = "0"; // Resetear margen
listContainerElement.style.background = "#fff";
listContainerElement.style.listStyle = "none";
section.appendChild(listContainerElement);
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML de palabras especiales";
dropArea.style.border = "2px dashed #ccc"; // Borde más visible
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px"; // Más padding
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
dropArea.addEventListener("dragleave", () => {
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0]);
if (file &&
(file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(
evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
alert("Error al parsear el archivo XML.");
return;
}
// Detectar raíz
const rootTag =
xmlDoc.documentElement.tagName.toLowerCase();
if (rootTag !== "excludedwords" &&
rootTag !== "diccionario")
{
alert(
"El archivo XML no es válido. Debe tener <ExcludedWords> o <diccionario> como raíz.");
return;
}
// Importar palabras
const words = xmlDoc.getElementsByTagName("word");
let newWordsAddedCount = 0;
for (let i = 0; i < words.length; i++)
{
const val = words[i].textContent.trim();
if (val && !excludedWords.has(val))
{
excludedWords.add(val);
newWordsAddedCount++;
}
}
// Importar reemplazos si existen
const replacements =
xmlDoc.getElementsByTagName("replacement");
for (let i = 0; i < replacements.length; i++)
{
const from = replacements[i].getAttribute("from");
const to = replacements[i].textContent.trim();
if (from && to)
{
replacementWords[from] = to;
}
}
renderExcludedWordsList(
document.getElementById("excludedWordsList"));
alert(`Importación completada. Palabras nuevas: ${
newWordsAddedCount}`);
}
catch (err)
{
alert("Error procesando el archivo XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
parentContainer.appendChild(section);
}
// === Diccionario ===
function createDictionaryManager(parentContainer)
{
const section = document.createElement("div");
section.id = "dictionaryManagerSection";
section.style.marginTop = "20px";
section.style.borderTop = "1px solid #ccc";
section.style.paddingTop = "10px";
const title = document.createElement("h4");
title.textContent = "Gestión del Diccionario";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
section.appendChild(title);
const addControlsContainer = document.createElement("div");
addControlsContainer.style.display = "flex";
addControlsContainer.style.gap = "8px";
addControlsContainer.style.marginBottom = "8px";
addControlsContainer.style.alignItems = "center"; // Alinear verticalmente
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Nueva palabra";
input.style.flexGrow = "1";
input.style.padding = "6px"; // Mejor padding
input.style.border = "1px solid #ccc";
input.style.borderRadius = "3px";
addControlsContainer.appendChild(input);
const addBtn = document.createElement("button");
addBtn.textContent = "Añadir";
addBtn.style.padding = "6px 10px"; // Mejor padding
addBtn.style.cursor = "pointer";
addBtn.addEventListener("click", function() {
const newWord = input.value.trim();
if (newWord)
{
const lowerNewWord = newWord.toLowerCase();
const alreadyExists =
Array.from(window.dictionaryWords)
.some(w => w.toLowerCase() === lowerNewWord);
if (commonWords.includes(lowerNewWord))
{
alert(
"La palabra es muy común y no debe agregarse a la lista.");
return;
}
if (alreadyExists)
{
alert("La palabra ya está en la lista.");
return;
}
window.dictionaryWords.add(lowerNewWord);
input.value = "";
renderDictionaryList(
document.getElementById("dictionaryWordsList"));
}
});
addControlsContainer.appendChild(addBtn);
section.appendChild(addControlsContainer);
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px"; // Más espacio
const exportBtn = document.createElement("button");
exportBtn.textContent = "Exportar"; // Más corto
exportBtn.title = "Exportar Diccionario a XML";
exportBtn.style.padding = "6px 10px";
exportBtn.style.cursor = "pointer";
exportBtn.addEventListener("click", exportDictionaryWordsList);
actionButtonsContainer.appendChild(exportBtn);
const clearBtn = document.createElement("button");
clearBtn.textContent = "Limpiar"; // Más corto
clearBtn.title = "Limpiar toda la lista";
clearBtn.style.padding = "6px 10px";
clearBtn.style.cursor = "pointer";
clearBtn.addEventListener("click", function() {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?"))
{
window.dictionaryWords.clear();
renderDictionaryList(document.getElementById(
"dictionaryWordsList")); // Pasar el elemento UL
}
});
actionButtonsContainer.appendChild(clearBtn);
section.appendChild(actionButtonsContainer);
// Diccionario: búsqueda
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Buscar en diccionario...";
search.style.display = "block";
search.style.width = "calc(100% - 14px)";
search.style.padding = "6px";
search.style.border = "1px solid #ccc";
search.style.borderRadius = "3px";
search.style.marginTop = "5px";
// On search input, render filtered list
search.addEventListener("input", () => {
renderDictionaryList(document.getElementById("dictionaryWordsList"),
search.value.trim());
});
section.appendChild(search);
// Lista UL para mostrar palabras del diccionario
const listContainerElement = document.createElement("ul");
listContainerElement.id = "dictionaryWordsList";
listContainerElement.style.maxHeight = "150px";
listContainerElement.style.overflowY = "auto";
listContainerElement.style.border = "1px solid #ddd";
listContainerElement.style.padding = "5px";
listContainerElement.style.margin = "0";
listContainerElement.style.background = "#fff";
listContainerElement.style.listStyle = "none";
section.appendChild(listContainerElement);
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML del diccionario";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
dropArea.style.borderColor = "#aaa";
});
dropArea.addEventListener("dragleave", () => {
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
});
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
dropArea.style.borderColor = "#ccc";
const file = e.dataTransfer.files[0];
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(evt.target.result,
"application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
console.error("[WME PLN] Error parseando XML:", parserError.textContent);
alert(
"Error al parsear el archivo XML del diccionario.");
return;
}
const xmlWords = xmlDoc.querySelectorAll("word");
let newWordsAddedCount = 0;
for (let i = 0; i < xmlWords.length; i++)
{
const val = xmlWords[i].textContent.trim();
if (val && !window.dictionaryWords.has(val))
{
window.dictionaryWords.add(val);
newWordsAddedCount++;
}
}
if (newWordsAddedCount > 0)
console.log(`[WME PLN] ${newWordsAddedCount} nuevas palabras añadidas desde XML.`);
// Renderizar la lista en el panel
renderDictionaryList(listContainerElement);
}
catch (err)
{
alert("Error procesando el diccionario XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
});
section.appendChild(dropArea);
parentContainer.appendChild(section);
renderDictionaryList(listContainerElement);
}
// Carga las palabras excluidas desde localStorage
function loadReplacementWordsFromStorage()
{
const savedReplacements = localStorage.getItem("replacementWordsList");
if (savedReplacements)
{
try
{
replacementWords = JSON.parse(savedReplacements);
if (typeof replacementWords !== 'object' ||
replacementWords === null)
{ // Asegurar que sea un objeto
replacementWords = {};
}
}
catch (e)
{
console.error("[WME PLN] Error cargando lista de reemplazos desde localStorage:", e);
replacementWords = {};
}
}
else
{
replacementWords = {}; // Inicializar si no hay nada guardado
}
console.log("[WME PLN] Reemplazos cargados:", Object.keys(replacementWords).length, "reglas.");
}
// Carga las palabras excluidas desde localStorage
function saveSwapWordsToStorage()
{
localStorage.setItem("swapWords", JSON.stringify(window.swapWords || []));
}
// Carga las palabras reemplazo
function saveReplacementWordsToStorage()
{
try
{
localStorage.setItem("replacementWordsList",
JSON.stringify(replacementWords));
// console.log("[WME PLN] Lista de reemplazos guardada en localStorage.");
}
catch (e)
{
console.error("[WME PLN] Error guardando lista de reemplazos en localStorage:", e);
}
}
// Carga las palabras excluidas desde localStorage
function saveExcludedWordsToLocalStorage()
{
try {
localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords)));
// console.log("[WME PLN] Lista de palabras especiales guardada en localStorage.");
} catch (e) {
console.error("[WME PLN] Error guardando palabras especiales en localStorage:", e);
}
}//
// Renderiza la lista de reemplazos
function renderReplacementsList(ulElement)
{
//console.log("[WME PLN DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo");
if (!ulElement)
{
//console.error("[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList.");
return;
}
ulElement.innerHTML = ""; // Limpiar lista actual
const entries = Object.entries(replacementWords);
if (entries.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay reemplazos definidos.";
li.style.textAlign = "center";
li.style.color = "#777";
li.style.padding = "5px";
ulElement.appendChild(li);
return;
}
// Ordenar alfabéticamente por la palabra original (from)
entries.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()));
entries.forEach(([ from, to ]) => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
const textContainer = document.createElement("div");
textContainer.style.flexGrow = "1";
textContainer.style.overflow = "hidden";
textContainer.style.textOverflow = "ellipsis";
textContainer.style.whiteSpace = "nowrap";
textContainer.title = `Reemplazar "${from}" con "${to}"`;
const fromSpan = document.createElement("span");
fromSpan.textContent = from;
fromSpan.style.fontWeight = "bold";
textContainer.appendChild(fromSpan);
const arrowSpan = document.createElement("span");
arrowSpan.textContent = " → ";
arrowSpan.style.margin = "0 5px";
textContainer.appendChild(arrowSpan);
const toSpan = document.createElement("span");
toSpan.textContent = to;
toSpan.style.color = "#007bff";
textContainer.appendChild(toSpan);
li.appendChild(textContainer);
// Botón Editar
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar este reemplazo";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px 4px";
editBtn.style.fontSize = "14px";
editBtn.style.marginLeft = "4px";
editBtn.addEventListener("click", () => {
const newFrom = prompt("Editar texto original:", from);
if (newFrom === null) return;
const newTo = prompt("Editar texto de reemplazo:", to);
if (newTo === null) return;
if (!newFrom.trim()) {
alert("El campo 'Texto Original' es requerido.");
return;
}
if (newFrom === newTo) {
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
// Si cambia la clave, elimina la anterior
if (newFrom !== from) delete replacementWords[from];
replacementWords[newFrom] = newTo;
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
});
// Botón Eliminar
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = `Eliminar este reemplazo`;
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px 4px";
deleteBtn.style.fontSize = "14px";
deleteBtn.style.marginLeft = "4px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`))
{
delete replacementWords[from];
renderReplacementsList(ulElement);
saveReplacementWordsToStorage();
}
});
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "4px";
btnContainer.appendChild(editBtn);
btnContainer.appendChild(deleteBtn);
li.appendChild(btnContainer);
ulElement.appendChild(li);
});
}
// Exporta las palabras especiales y reemplazos a un archivo XML
function exportSharedDataToXml()
{
if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0 &&
(!window.swapWords || window.swapWords.length === 0)
) {
alert("No hay palabras especiales, reemplazos ni palabras swap definidos para exportar.");
return;
}
let xmlParts = [];
// Exportar palabras excluidas
if (excludedWords.size > 0) {
xmlParts.push(" <words>");
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.forEach(w => xmlParts.push(` <word>${xmlEscape(w)}</word>`));
xmlParts.push(" </words>");
}
// Exportar reemplazos
if (Object.keys(replacementWords).length > 0)
{
xmlParts.push(" <replacements>");
Object.entries(replacementWords)
.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()))
.forEach(([from, to]) => {
xmlParts.push(` <replacement from="${xmlEscape(from)}">${xmlEscape(to)}</replacement>`);
});
xmlParts.push(" </replacements>");
}
// Exportar palabras swap en orden de ingreso (sin sort)
if (window.swapWords && window.swapWords.length > 0) {
xmlParts.push(" <swapWords>");
window.swapWords.forEach(val => {
xmlParts.push(` <swap value="${xmlEscape(val)}"/>`);
});
xmlParts.push(" </swapWords>");
}
const xmlContent =
`<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n${xmlParts.join("\n")}\n</ExcludedWords>`;
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wme_normalizer_data_export.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}//exportSharedDataToXml
// Escapa caracteres especiales para XML
function handleXmlFileDrop(file)
{
if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
{
const reader = new FileReader();
reader.onload = function(evt) {
try
{
const parser = new DOMParser();
const xmlDoc =
parser.parseFromString(evt.target.result, "application/xml");
const parserError = xmlDoc.querySelector("parsererror");
if (parserError)
{
alert("Error al parsear el archivo XML: " +
parserError.textContent);
return;
}
const rootTag = xmlDoc.documentElement.tagName.toLowerCase();
if (rootTag !== "excludedwords")
{ // Asumiendo que la raíz sigue siendo esta
alert(
"El archivo XML no es válido. Debe tener <ExcludedWords> como raíz.");
return;
}
let newExcludedAdded = 0;
let newReplacementsAdded = 0;
let replacementsOverwritten = 0;
// Importar palabras excluidas
const words = xmlDoc.getElementsByTagName("word");
for (let i = 0; i < words.length; i++)
{
const val = words[i].textContent.trim();
if (val && !excludedWords.has(val))
{
const validation = isValidExcludedWord(val);
if (validation.valid)
{
excludedWords.add(val);
newExcludedAdded++;
}
else
{
console.warn(`Palabra excluida omitida desde XML "${val}": ${validation.msg}`);
}
}
}
// Importar reemplazos
const replacements = xmlDoc.getElementsByTagName("replacement");
for (let i = 0; i < replacements.length; i++)
{
const from = replacements[i].getAttribute("from")?.trim();
const to = replacements[i].textContent.trim();
if (from && to)
{
if (replacementWords.hasOwnProperty(from) &&
replacementWords[from] !== to)
{
replacementsOverwritten++;
}
else if (!replacementWords.hasOwnProperty(from))
{
newReplacementsAdded++;
}
replacementWords[from] = to;
}
}
// === Importar swapWords, respetando orden ===
const swapWordsNode = xmlDoc.querySelector("swapWords");
if (swapWordsNode) {
if (!window.swapWords) window.swapWords = [];
window.swapWords = [];
swapWordsNode.querySelectorAll("swap").forEach(swapNode => {
const value = swapNode.getAttribute("value");
if (value && !window.swapWords.includes(value)) {
window.swapWords.push(value);
saveSwapWordsToStorage();
}
});
}
// Guardar y Re-renderizar AMBAS listas
saveExcludedWordsToLocalStorage();
saveReplacementWordsToStorage();
// Re-renderizar las listas en sus respectivas pestañas si están
// visibles o al activarse
const excludedListElement =
document.getElementById("excludedWordsList");
if (excludedListElement)
renderExcludedWordsList(excludedListElement);
const replacementsListElement =
document.getElementById("replacementsListElementID");
if (replacementsListElement)
renderReplacementsList(replacementsListElement);
alert(`Importación completada.\nPalabras Especiales nuevas: ${newExcludedAdded}\nReemplazos nuevos: ${newReplacementsAdded}\nReemplazos sobrescritos: ${replacementsOverwritten}`);
}
catch (err)
{
console.error(
"[WME PLN] Error procesando el archivo XML importado:", err);
alert("Ocurrió un error procesando el archivo XML.");
}
};
reader.readAsText(file);
}
else
{
alert("Por favor, arrastra un archivo XML válido.");
}
}//handleXmlFileDrop
// Carga las palabras swap desde localStorage
function loadSwapWordsFromStorage()
{
const stored = localStorage.getItem("swapWords");
if (stored)
{
try
{
window.swapWords = JSON.parse(stored);
}
catch (e)
{
window.swapWords = [];
}
}
else
{
window.swapWords = [];
}
}// loadSwapWordsFromStorage
// Crea el gestor de reemplazos
function createReplacementsManager(parentContainer)
{
loadSwapWordsFromStorage();
parentContainer.innerHTML = ''; // Limpiar por si acaso
// --- Contenedor principal ---
const title = document.createElement("h4");
title.textContent = "Gestión de Reemplazos";
title.style.fontSize = "15px";
title.style.marginBottom = "10px";
parentContainer.appendChild(title);
// --- Dropdown de modo de reemplazo ---
const modeSelector = document.createElement("select");
modeSelector.id = "replacementModeSelector";
modeSelector.style.marginBottom = "10px";
modeSelector.style.marginTop = "5px";
// Añadir opciones al selector
const optionWords = document.createElement("option");
optionWords.value = "words";
optionWords.textContent = "Reemplazos de palabras";
modeSelector.appendChild(optionWords);
// Añadir opción para swap
const optionSwap = document.createElement("option");
optionSwap.value = "swapStart";
optionSwap.textContent = "Palabras al inicio (swap)";
modeSelector.appendChild(optionSwap);
parentContainer.appendChild(modeSelector);
//Contenedor para reemplazos y controles
const replacementsContainer = document.createElement("div");
replacementsContainer.id = "replacementsContainer";
// Sección para añadir nuevos reemplazos
const addSection = document.createElement("div");
addSection.style.display = "flex";
addSection.style.gap = "8px";
addSection.style.marginBottom = "12px";
addSection.style.alignItems = "flex-end"; // Alinear inputs y botón
// Contenedores para inputs de texto
const fromInputContainer = document.createElement("div");
fromInputContainer.style.flexGrow = "1";
const fromLabel = document.createElement("label");
fromLabel.textContent = "Texto Original:";
fromLabel.style.display = "block";
fromLabel.style.fontSize = "12px";
fromLabel.style.marginBottom = "2px";
// Input para el texto original
const fromInput = document.createElement("input");
fromInput.type = "text";
fromInput.placeholder = "Ej: Urb.";
fromInput.style.width = "95%"; // Para que quepa bien
fromInput.style.padding = "6px";
fromInput.style.border = "1px solid #ccc";
// Añadir label e input al contenedor
fromInputContainer.appendChild(fromLabel);
fromInputContainer.appendChild(fromInput);
addSection.appendChild(fromInputContainer);
// Contenedor para el texto de reemplazo
const toInputContainer = document.createElement("div");
toInputContainer.style.flexGrow = "1";
const toLabel = document.createElement("label");
toLabel.textContent = "Texto de Reemplazo:";
toLabel.style.display = "block";
toLabel.style.fontSize = "12px";
toLabel.style.marginBottom = "2px";
// Input para el texto de reemplazo
const toInput = document.createElement("input");
toInput.type = "text";
toInput.placeholder = "Ej: Urbanización";
toInput.style.width = "95%";
toInput.style.padding = "6px";
toInput.style.border = "1px solid #ccc";
toInputContainer.appendChild(toLabel);
toInputContainer.appendChild(toInput);
addSection.appendChild(toInputContainer);
// Atributos para evitar corrección ortográfica
fromInput.setAttribute('spellcheck', 'false');
toInput.setAttribute('spellcheck', 'false');
// Botón para añadir el reemplazo
const addReplacementBtn = document.createElement("button");
addReplacementBtn.textContent = "Añadir";
addReplacementBtn.style.padding = "6px 10px";
addReplacementBtn.style.cursor = "pointer";
addReplacementBtn.style.height = "30px"; // Para alinear con los inputs
addSection.appendChild(addReplacementBtn);
// Elemento UL para la lista de reemplazos
const listElement = document.createElement("ul");
listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista
listElement.style.maxHeight = "150px";
listElement.style.overflowY = "auto";
listElement.style.border = "1px solid #ddd";
listElement.style.padding = "8px";
listElement.style.margin = "0 0 10px 0";
listElement.style.background = "#fff";
listElement.style.listStyle = "none";
// Event listener para el botón "Añadir"
addReplacementBtn.addEventListener("click", () => {
const fromValue = fromInput.value.trim();
const toValue = toInput.value.trim();
if (!fromValue)
{
alert("El campo 'Texto Original' es requerido.");
return;
}
// Validar que no sea solo caracteres especiales
if (fromValue === toValue)
{
alert("El texto original y el de reemplazo no pueden ser iguales.");
return;
}
// Validar que no sea solo caracteres especiales
if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue)
{
if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`))
return;
}
replacementWords[fromValue] = toValue;
fromInput.value = "";
toInput.value = "";
// Renderiza toda la lista (más seguro y rápido en la práctica)
renderReplacementsList(listElement);
saveReplacementWordsToStorage();
});
// Botones de Acción y Drop Area (usarán la lógica compartida)
const actionButtonsContainer = document.createElement("div");
actionButtonsContainer.style.display = "flex";
actionButtonsContainer.style.gap = "8px";
actionButtonsContainer.style.marginBottom = "10px";
// Botones de acción
const exportButton = document.createElement("button");
exportButton.textContent = "Exportar Todo";
exportButton.title = "Exportar Excluidas y Reemplazos a XML";
exportButton.style.padding = "6px 10px";
exportButton.addEventListener("click", exportSharedDataToXml); // Llamar a la función compartida
actionButtonsContainer.appendChild(exportButton);
// Botón para exportar solo reemplazos
const clearButton = document.createElement("button");
clearButton.textContent = "Limpiar Reemplazos";
clearButton.title = "Limpiar solo la lista de reemplazos";
clearButton.style.padding = "6px 10px";
clearButton.addEventListener("click", () => {
if (
confirm(
"¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?"))
{
replacementWords = {};
saveReplacementWordsToStorage();
renderReplacementsList(listElement);
}
});
actionButtonsContainer.appendChild(clearButton);
// Botón para importar desde XML
const dropArea = document.createElement("div");
dropArea.textContent = "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)";
dropArea.style.border = "2px dashed #ccc";
dropArea.style.borderRadius = "4px";
dropArea.style.padding = "15px";
dropArea.style.marginTop = "10px";
dropArea.style.textAlign = "center";
dropArea.style.background = "#f9f9f9";
dropArea.style.color = "#555";
dropArea.addEventListener("dragover", (e) => {
e.preventDefault();
dropArea.style.background = "#e9e9e9";
});
dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; });
dropArea.addEventListener("drop", (e) => {
e.preventDefault();
dropArea.style.background = "#f9f9f9";
handleXmlFileDrop(e.dataTransfer.files[0]);
});
// --- Ensamblar en replacementsContainer ---
replacementsContainer.appendChild(addSection);
replacementsContainer.appendChild(listElement);
replacementsContainer.appendChild(actionButtonsContainer);
replacementsContainer.appendChild(dropArea);
parentContainer.appendChild(replacementsContainer);
// --- Contenedor para swapStart/frases al inicio ---
const swapContainer = document.createElement("div");
swapContainer.id = "swapContainer";
swapContainer.style.display = "none";
// Título y explicación del swap
const swapTitle = document.createElement("h4");
swapTitle.textContent = "Palabras al inicio";
// Estilo del título
const swapExplanationBox = document.createElement("div");
swapExplanationBox.style.background = "#f4f8ff";
swapExplanationBox.style.borderLeft = "4px solid #2d6df6";
swapExplanationBox.style.padding = "10px";
swapExplanationBox.style.margin = "10px 0";
swapExplanationBox.style.fontSize = "13px";
swapExplanationBox.style.lineHeight = "1.4";
swapExplanationBox.innerHTML =
"<strong>🔄 ¿Qué hace esta lista?</strong><br>" +
"Las palabras ingresadas aquí se moverán del final al inicio del nombre del lugar si se encuentran al final.<br>" +
"<em>Ej:</em> “Las Palmas <strong>Urbanización</strong>” → “<strong>Urbanización</strong> Las Palmas”<br>" +
"<em>Ej:</em> “Tornillos <strong>Ferretería</strong>” → “<strong>Ferretería</strong> Tornillos”";
// Añadir caja de explicación al contenedor
swapContainer.appendChild(swapExplanationBox);
const swapExplanation = document.createElement("p");
swapExplanation.textContent = "El orden importa: las palabras se evalúan una a una desde el inicio. Si se ordenan alfabéticamente, una más corta podría bloquear otra más específica.";
swapExplanation.style.fontSize = "12px";
swapExplanation.style.fontStyle = "italic";
swapExplanation.style.marginTop = "6px";
swapExplanation.style.marginBottom = "10px";
swapExplanation.style.color = "#555";
// Inserta este nodo justo después del swapTitle, por ejemplo:
swapContainer.appendChild(swapExplanation);
swapTitle.style.fontSize = "14px";
swapTitle.style.marginBottom = "8px";
swapContainer.appendChild(swapTitle);
// Contenedor para añadir nuevas palabras swap
const swapInput = document.createElement("input");
swapInput.type = "text";
swapInput.placeholder = "Ej: Urbanización";
swapInput.style.width = "70%";
swapInput.style.padding = "6px";
swapInput.style.marginRight = "8px";
// Atributos para evitar corrección ortográfica
const swapBtn = document.createElement("button");
swapBtn.textContent = "Añadir";
swapBtn.style.padding = "6px 10px";
swapBtn.addEventListener("click", () => {
const val = swapInput.value.trim();
if (!val || /^[^a-zA-Z0-9]+$/.test(val))
{
alert("No se permiten caracteres especiales solos");
return;
}
if (window.swapWords.includes(val))
{
alert("Ya existe en la lista.");
return;
}
window.swapWords.push(val); // mantiene orden
localStorage.setItem("wme_swapWords", JSON.stringify(window.swapWords));
saveSwapWordsToStorage(); // Guardar en localStorage
swapInput.value = "";
renderSwapList();
});
swapContainer.appendChild(swapInput);
swapContainer.appendChild(swapBtn);
// Añadir campo de búsqueda justo después de swapBtn
searchSwapInput = document.createElement("input");
searchSwapInput.type = "text";
searchSwapInput.placeholder = "Buscar palabra...";
searchSwapInput.id = "searchSwapInput";
searchSwapInput.style.width = "70%";
searchSwapInput.style.padding = "6px";
searchSwapInput.style.marginTop = "8px";
searchSwapInput.style.marginBottom = "8px";
searchSwapInput.style.border = "1px solid #ccc";
// Escuchar el input para actualizar lista
searchSwapInput.addEventListener("input", () => {
renderSwapList(searchSwapInput);
});
swapContainer.appendChild(searchSwapInput);
// Renderiza la lista
renderSwapList(searchSwapInput);
parentContainer.appendChild(swapContainer);
// --- Alternar visibilidad según modo seleccionado ---
modeSelector.addEventListener("change", () => {
replacementsContainer.style.display = modeSelector.value === "words" ? "block" : "none";
swapContainer.style.display = modeSelector.value === "swapStart" ? "block" : "none";
});
// --- Función para renderizar la lista de swapWords ---
function renderSwapList(searchInput = null)
{
// Buscar automáticamente el campo si no se pasó como parámetro
if (!searchInput)
searchInput = document.getElementById("searchSwapInput");
// Asegurarse de que swapContainer existe
const swapList = swapContainer.querySelector("ul") || (() => {
const ul = document.createElement("ul");
ul.id = "swapList";
ul.style.maxHeight = "120px";
ul.style.overflowY = "auto";
ul.style.border = "1px solid #ddd";
ul.style.padding = "8px";
ul.style.margin = "10px 0 0 0";
ul.style.background = "#fff";
ul.style.listStyle = "none";
swapContainer.appendChild(ul);
return ul;
})();
swapList.innerHTML = "";
// Verificar si hay palabras swap definidas
if (!window.swapWords || window.swapWords.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay palabras al inicio definidas.";
li.style.textAlign = "center";
li.style.color = "#777";
li.style.padding = "5px";
swapList.appendChild(li);
return;
}
// Filtrar palabras swap según el término de búsqueda
const searchTerm = searchSwapInput && searchSwapInput.value ? searchSwapInput.value.trim().toLowerCase() : "";
let filteredSwapWords = Array.from(window.swapWords);
// Si hay un término de búsqueda, filtrar la lista
if (searchTerm)
filteredSwapWords = filteredSwapWords.filter(word => word.toLowerCase().includes(searchTerm));
// Ordenar alfabéticamente
filteredSwapWords.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
// Span para la palabra
const wordSpan = document.createElement("span");
wordSpan.title = word;
// Aplicar estilos para truncar texto largo
if (searchTerm)
{
const i = word.toLowerCase().indexOf(searchTerm);
if (i !== -1)
{
const before = word.substring(0, i);
const match = word.substring(i, i + searchTerm.length);
const after = word.substring(i + searchTerm.length);
wordSpan.innerHTML = `${before}<mark>${match}</mark>${after}`;
}
else
{
wordSpan.textContent = word;
}
}
else
{
wordSpan.textContent = word;
}
// Estilos para el span de la palabra
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "4px";
// Botón Editar
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word)
{ // Permitir string vacío para borrar si se quisiera, pero
const trimmedNewWord = newWord.trim();
if (trimmedNewWord === "")
{
alert("La palabra no puede estar vacía.");
return;
}
if (window.swapWords.includes(trimmedNewWord) && trimmedNewWord !== word)
{
alert("Esa palabra ya existe en la lista.");
return;
}
window.swapWords = window.swapWords.filter(w => w !== word);
window.swapWords.push(trimmedNewWord);
saveSwapWordsToStorage();
renderSwapList(searchInput);
}
});
// Botón Eliminar
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Eliminar la palabra '${word}' de la lista?`))
{
window.swapWords = window.swapWords.filter(w => w !== word);
renderSwapList(searchInput);
saveSwapWordsToStorage();
}
});
btnContainer.appendChild(editBtn);
btnContainer.appendChild(deleteBtn);
li.appendChild(wordSpan);
li.appendChild(btnContainer);
swapList.appendChild(li);
});
}
// Render inicial
renderReplacementsList(listElement);
if (window.swapWords && window.swapWords.size > 0) renderSwapList();
// Listener de búsqueda para swap
searchSwapInput.addEventListener("input", renderSwapList);
}// createReplacementsManager
// Renderiza la lista de palabras excluidas
function renderExcludedWordsList(ulElement, filter = "")
{
if (!ulElement) {
return;
}
// Asegurarse de que ulElement es válido
const currentFilter = filter.toLowerCase();
ulElement.innerHTML = "";
// Asegurarse de que excludedWords es un Set
const wordsToRender = Array.from(excludedWords)
.filter(word => word.toLowerCase().includes(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Si no hay palabras para renderizar, mostrar mensaje
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = "No hay palabras excluidas.";
li.style.textAlign = "center";
li.style.color = "#777";
ulElement.appendChild(li);
return;
}
// Renderizar cada palabra
wordsToRender.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex"; // Agregado para alinear texto y botones
li.style.justifyContent = "space-between"; // Agregado para espacio entre texto y botones
li.style.alignItems = "center"; // Agregado para centrado vertical
li.style.padding = "5px";
li.style.borderBottom = "1px solid #ddd";
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.flexGrow = "1"; // Permite que el texto ocupe el espacio disponible
wordSpan.style.marginRight = "10px"; // Espacio entre el texto y los botones
li.appendChild(wordSpan);
// --- AÑADE ESTE BLOQUE PARA LOS BOTONES ---
const btnContainer = document.createElement("span");
btnContainer.style.display = "flex";
btnContainer.style.gap = "8px"; // Espacio entre los botones
// Botón de edición
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️"; // Icono de lápiz
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word) {
const validation = isValidExcludedWord(newWord.trim());
if (!validation.valid) {
alert(validation.msg);
return;
}
// Eliminar la palabra antigua del Set y Map
excludedWords.delete(word);
const oldFirstChar = word.charAt(0).toLowerCase();
if (excludedWordsMap.has(oldFirstChar)) {
excludedWordsMap.get(oldFirstChar).delete(word);
if (excludedWordsMap.get(oldFirstChar).size === 0) {
excludedWordsMap.delete(oldFirstChar);
}
}
// Añadir la nueva palabra al Set y Map
const trimmedNewWord = newWord.trim();
excludedWords.add(trimmedNewWord);
const newFirstChar = trimmedNewWord.charAt(0).toLowerCase();
if (!excludedWordsMap.has(newFirstChar)) {
excludedWordsMap.set(newFirstChar, new Set());
}
excludedWordsMap.get(newFirstChar).add(trimmedNewWord);
renderExcludedWordsList(ulElement, currentFilter);
saveExcludedWordsToLocalStorage();
}
});
btnContainer.appendChild(editBtn);
// Botón de eliminación
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️"; // Icono de bote de basura
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
if (confirm(`¿Eliminar la palabra '${word}' de la lista de especiales?`)) {
excludedWords.delete(word);
const firstChar = word.charAt(0).toLowerCase();
if (excludedWordsMap.has(firstChar)) {
excludedWordsMap.get(firstChar).delete(word);
if (excludedWordsMap.get(firstChar).size === 0) {
excludedWordsMap.delete(firstChar);
}
}
renderExcludedWordsList(ulElement, currentFilter);
saveExcludedWordsToLocalStorage();
}
});
btnContainer.appendChild(deleteBtn);
li.appendChild(btnContainer);
// --- FIN DEL BLOQUE PARA LOS BOTONES ---
ulElement.appendChild(li);
});//
}// renderExcludedWordsList
// Crea un dropdown para seleccionar categorías recomendadas
function createRecommendedCategoryDropdown(placeId, currentCategoryKey, dynamicCategorySuggestions)
{
const wrapperDiv = document.createElement("div");
wrapperDiv.style.position = "relative";
wrapperDiv.style.width = "100%";
wrapperDiv.style.minWidth = "150px";
wrapperDiv.style.display = "flex";
wrapperDiv.style.flexDirection = "column";
// Parte de sugerencias dinámicas existentes
const suggestionsWrapper = document.createElement("div"); // Contenedor para sugerencias
suggestionsWrapper.style.display = "flex";
suggestionsWrapper.style.flexDirection = "column";
suggestionsWrapper.style.alignItems = "flex-start";
suggestionsWrapper.style.gap = "4px";
// Filtrar y ordenar las sugerencias dinámicas para la presentación
const filteredSuggestions = dynamicCategorySuggestions.filter(suggestion => suggestion.categoryKey.toUpperCase() !== currentCategoryKey.toUpperCase());
if (filteredSuggestions.length > 0)
{ // Solo si hay sugerencias diferentes a la actual
filteredSuggestions.forEach(suggestion => {
const suggestionEntry = document.createElement("div");
suggestionEntry.style.display = "flex";
suggestionEntry.style.alignItems = "center";
suggestionEntry.style.gap = "4px";
suggestionEntry.style.padding = "2px 4px";
suggestionEntry.style.border = "1px solid #dcdcdc";
suggestionEntry.style.borderRadius = "3px";
suggestionEntry.style.backgroundColor = "#eaf7ff"; // Un color distinto para sugerencias
suggestionEntry.style.cursor = "pointer";
suggestionEntry.title = `Sugerencia: ${getCategoryDetails(suggestion.categoryKey).description}`;
//Añadir icono y descripción de la categoría
const suggestedIconSpan = document.createElement("span");// Icono de la sugerencia
suggestedIconSpan.textContent = suggestion.icon;
suggestedIconSpan.style.fontSize = "16px";
suggestionEntry.appendChild(suggestedIconSpan);
// Añadir descripción de la categoría
const suggestedDescSpan = document.createElement("span");
suggestedDescSpan.textContent = getCategoryDetails(suggestion.categoryKey).description;
suggestionEntry.appendChild(suggestedDescSpan);
suggestionEntry.addEventListener("click", async function handler() { // Cambiado a función con nombre 'handler'
const placeToUpdate = W.model.venues.getObjectById(placeId);
if (!placeToUpdate)
{
console.error("Lugar no encontrado para actualizar categoría.");
return;
}
try
{
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(placeToUpdate, { categories: [suggestion.categoryKey] });
W.model.actionManager.add(action);
// : Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, suggestion.categoryKey);
// : Mostrar chulito verde en la sugerencia misma
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.marginLeft = "5px";
suggestionEntry.appendChild(successIcon); // Añadir el chulito a la entrada de la sugerencia
suggestionEntry.style.cursor = "default"; // Deshabilitar clic posterior
// AÑADE ESTAS LÍNEAS PARA EVITAR MÚLTIPLES CLICS Y CHULITOS
suggestionEntry.removeEventListener("click", handler); // Deshabilita el listener una vez que se ha hecho clic
suggestionEntry.style.opacity = "0.7"; // Opcional: Atenúa la sugerencia para indicar que ya se usó
optionsListDiv.style.display = "none"; // Ocultar lista
searchInput.blur(); // Quitar el foco
// : Eliminar la selección temporal para la categoría, ya se guardó
tempSelectedCategories.delete(placeId); // Si esta categoría se guardó directamente
}
catch (e)
{
console.error("Error al actualizar la categoría desde sugerencia:", e);
alert("Error al actualizar la categoría: " + e.message); // Mantener alerta para errores
}
});
suggestionsWrapper.appendChild(suggestionEntry);
});
wrapperDiv.appendChild(suggestionsWrapper); // Añadir contenedor de sugerencias
}// createRecommendedCategoryDropdown
//Fin de parte de sugerencias dinámicas
// Input para buscar
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Buscar o Seleccionar Categoría";// Placeholder más descriptivo
searchInput.style.width = "calc(100% - 10px)";
searchInput.style.padding = "5px";
searchInput.style.marginTop = "5px"; // Espacio después de sugerencias
searchInput.style.marginBottom = "5px";
searchInput.style.border = "1px solid #ccc";
searchInput.style.borderRadius = "3px";
searchInput.setAttribute('spellcheck', 'false');// Evitar corrección ortográfica
searchInput.readOnly = false;// Permitir escribir pero no editar directamente
searchInput.style.cursor = 'auto';// Permitir escribir pero no editar directamente
wrapperDiv.appendChild(searchInput); // Añadir el input al wrapper
// Div que actuará como la lista desplegable de opciones
const optionsListDiv = document.createElement("div");
optionsListDiv.style.position = "absolute";
// Ajuste de top para que aparezca debajo del input, incluso con sugerencias
optionsListDiv.style.top = "calc(100% + 5px)"; // Se ajusta dinámicamente o se puede hacer con position: relative dentro de un contenedor fijo.
optionsListDiv.style.left = "0";
optionsListDiv.style.width = "calc(100% - 2px)";
optionsListDiv.style.maxHeight = "200px";
optionsListDiv.style.overflowY = "auto";
optionsListDiv.style.border = "1px solid #ddd";
optionsListDiv.style.backgroundColor = "#fff";
optionsListDiv.style.zIndex = "1001";
optionsListDiv.style.display = "none";
optionsListDiv.style.borderRadius = "3px";
optionsListDiv.style.boxShadow = "0 2px 5px rgba(0,0,0,0.2)";
wrapperDiv.appendChild(optionsListDiv);
// --- Populate options list ---
function populateOptions(filterText = "")
{
optionsListDiv.innerHTML = ""; // Clear existing options
const lowerFilterText = filterText.toLowerCase(); // Normalize filter text for case-insensitive search
// Sort rules alphabetically by their Spanish description for display
const sortedRules = [...window.dynamicCategoryRules].sort((a, b) => {
const descA = (getWazeLanguage() === 'es' && a.desc_es) ? a.desc_es : a.desc_en;
const descB = (getWazeLanguage() === 'es' && b.desc_es) ? b.desc_es : b.desc_en;
return descA.localeCompare(descB);
});
sortedRules.forEach(rule => {// Iterate through each rule
const displayDesc = (getWazeLanguage() === 'es' && rule.desc_es) ? rule.desc_es : rule.desc_en;
if (filterText === "" || displayDesc.toLowerCase().includes(lowerFilterText) ||
rule.categoryKey.toLowerCase().includes(lowerFilterText))
{// Check if displayDesc or categoryKey contains the filter text
const optionDiv = document.createElement("div");
optionDiv.style.padding = "5px";
optionDiv.style.cursor = "pointer";
optionDiv.style.borderBottom = "1px solid #eee";
optionDiv.style.display = "flex";
optionDiv.style.alignItems = "center";
optionDiv.style.gap = "5px";
optionDiv.title = `Seleccionar: ${displayDesc} (${rule.categoryKey})`;
// Resaltar si es la categoría actual o la temporalmente seleccionada
const tempSelectedKey = tempSelectedCategories.get(placeId); // Obtener selección temporal
if (rule.categoryKey.toUpperCase() === currentCategoryKey.toUpperCase())
{// Resaltar la categoría actual
optionDiv.style.backgroundColor = "#e0f7fa"; // Azul claro para la actual
optionDiv.style.fontWeight = "bold";
}
else if (tempSelectedKey && rule.categoryKey.toUpperCase() === tempSelectedKey.toUpperCase()) // Resaltar selección temporal
optionDiv.style.backgroundColor = "#fffacd"; // Amarillo claro para la seleccionada temporalmente
else if (dynamicCategorySuggestions.some(s => s.categoryKey.toUpperCase() === rule.categoryKey.toUpperCase()))
optionDiv.style.backgroundColor = "#e6ffe6"; // Verde claro para sugerida por el sistema
const iconSpan = document.createElement("span");// Icono de la categoría
iconSpan.textContent = rule.icon;
iconSpan.style.fontSize = "16px";
optionDiv.appendChild(iconSpan);
const textSpan = document.createElement("span");// Descripción de la categoría
textSpan.textContent = displayDesc;
optionDiv.appendChild(textSpan);// Añadir descripción de la categoría
optionDiv.addEventListener("mouseenter", () => optionDiv.style.backgroundColor = "#f0f0f0");
optionDiv.addEventListener("mouseleave", () => {
if (tempSelectedKey && rule.categoryKey.toUpperCase() === tempSelectedKey.toUpperCase())
{
optionDiv.style.backgroundColor = "#fffacd";
}
else if (rule.categoryKey.toUpperCase() === currentCategoryKey.toUpperCase())
{
optionDiv.style.backgroundColor = "#e0f7fa";
}
else if (dynamicCategorySuggestions.some(s => s.categoryKey.toUpperCase() === rule.categoryKey.toUpperCase()))
{
optionDiv.style.backgroundColor = "#e6ffe6";
}
else
{
optionDiv.style.backgroundColor = "#fff";
}
});
optionDiv.addEventListener("click", async () => { // : Hacer async
const placeToUpdate = W.model.venues.getObjectById(placeId);
if (!placeToUpdate)
{// Si no se encuentra el lugar, mostrar error
console.error("Lugar no encontrado para actualizar categoría.");
return;
}
try
{// : Actualizar la categoría del lugar
const UpdateObject = require("Waze/Action/UpdateObject");
const action = new UpdateObject(placeToUpdate, { categories: [rule.categoryKey] });
W.model.actionManager.add(action);
// : Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, rule.categoryKey);
// : Actualizar el valor del input con icono y descripción de la selección
searchInput.value = `${rule.icon} ${displayDesc}`;
optionsListDiv.style.display = "none"; // Ocultar la lista
// : Mostrar chulito verde junto al searchInput del dropdown
const existingCheck = searchInput.parentElement.querySelector('.category-update-check');
if (existingCheck) existingCheck.remove(); // Eliminar chulito previo
const successIcon = document.createElement("span");
successIcon.textContent = " ✅";
successIcon.style.marginLeft = "5px";
successIcon.classList.add('category-update-check'); // Clase para fácil referencia
searchInput.parentElement.appendChild(successIcon);
searchInput.blur(); // Quitar el foco para ocultar el teclado en móviles
// : Eliminar la selección temporal para la categoría, ya se guardó
tempSelectedCategories.delete(placeId); // Si esta categoría se guardó directamente
}
catch (e)
{
console.error("Error al actualizar la categoría desde dropdown:", e);
alert("Error al actualizar la categoría: " + e.message); // Mantener alerta para errores
}
});
optionsListDiv.appendChild(optionDiv);
}
});
if (optionsListDiv.childElementCount === 0)
{// Si no hay opciones que coincidan con el filtro, mostrar mensaje
const noResults = document.createElement("div");
noResults.style.padding = "5px";
noResults.style.color = "#777";
noResults.textContent = "No hay resultados.";
optionsListDiv.appendChild(noResults);
}
}// populateOptions
// Event Listeners para el dropdown (ajustados para input deshabilitado)
let debounceTimer;
// Escuchar el input para filtrar opciones
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
}, 200);
});
// Escuchar el foco para mostrar opciones
searchInput.addEventListener("focus", () => {
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
});
document.addEventListener("click", (e) => {
if (!wrapperDiv.contains(e.target)) {
optionsListDiv.style.display = "none";
}
});
// Cambiado de 'input' a 'click' para el searchInput (ya que está deshabilitado)
searchInput.addEventListener("click", () => { // Al hacer clic en el campo, mostrar la lista
populateOptions(""); // Mostrar todas las opciones (no hay texto para filtrar)
optionsListDiv.style.display = "none";
});
// También puedes querer un botón o icono para abrir el dropdown si el input está deshabilitado.
// O si quieres búsqueda, habilitar el input y usarlo. Para este caso, el click en el input lo abre.
// Ocultar opciones al hacer clic fuera del componente
document.addEventListener("click", (e) => {
if (!wrapperDiv.contains(e.target))
optionsListDiv.style.display = "none";
});
// Lógica de inicialización del valor del input: SIEMPRE EN BLANCO por defecto.
const storedTempCategory = tempSelectedCategories.get(placeId);// Recuperar selección temporal
if (storedTempCategory)
{
// Si hay una selección temporal, mostrarla con icono
const selectedCategoryDetails = getCategoryDetails(storedTempCategory);
searchInput.value = `${selectedCategoryDetails.icon} ${selectedCategoryDetails.description}`;
}
else
{
// : Si no hay selección temporal, el campo inicia en blanco (placeholder visible)
// No mostrar currentCategoryKey por defecto
searchInput.value = "";
}
populateOptions(""); // Cargar las opciones inicialmente (sin filtro)
return wrapperDiv;
}// createRecommendedCategoryDropdown
// Función auxiliar para actualizar el display de la categoría actual en la tabla
function updateCategoryDisplayInTable(placeId, newCategoryKey)
{
const row = document.querySelector(`tr[data-place-id="${placeId}"]`); // Asume que cada fila tiene un data-place-id
if (!row) return;
const categoryCell = row.querySelector('td:nth-child(8)'); // Asume que la 8ª columna es la de Categoría Actual
if (!categoryCell) return;// Asegurarse de que la celda existe
const categoryDetails = getCategoryDetails(newCategoryKey); // Obtener detalles de la categoría
const currentCategoryDiv = categoryCell.querySelector('div'); // Contenedor del texto y el ícono
if (currentCategoryDiv)
{// Actualizar el contenido del div existente
currentCategoryDiv.querySelector('span:first-child').textContent = categoryDetails.description; // Actualiza el texto
currentCategoryDiv.querySelector('span:last-child').textContent = categoryDetails.icon; // Actualiza el ícono
currentCategoryDiv.querySelector('span:first-child').title = `Categoría Actual: ${categoryDetails.description}`; // Actualiza el título
}
}
// Renderizar lista de palabras del diccionario
function renderDictionaryList(ulElement, filter = "")
{
// Asegurarse de que ulElement es válido
if (!ulElement || !window.dictionaryWords)
return;
// Asegurarse de que ulElement es válido
const currentFilter = filter.toLowerCase();
ulElement.innerHTML = "";
// Asegurarse de que dictionaryWords es un Set
const wordsToRender =
Array.from(window.dictionaryWords)
.filter(word => word.toLowerCase().startsWith(currentFilter))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Si no hay palabras que renderizar, mostrar mensaje
if (wordsToRender.length === 0)
{
const li = document.createElement("li");
li.textContent = window.dictionaryWords.size === 0
? "El diccionario está vacío."
: "No hay coincidencias.";
li.style.textAlign = "center";
li.style.color = "#777";
ulElement.appendChild(li);
// Guardar diccionario también cuando está vacío
try
{
localStorage.setItem(
"dictionaryWordsList",
JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error( "[WME PLN] Error guardando el diccionario en localStorage:", e);
}
return;
}
// Renderizar cada palabra
wordsToRender.forEach(word => {
const li = document.createElement("li");
li.style.display = "flex";
li.style.justifyContent = "space-between";
li.style.alignItems = "center";
li.style.padding = "4px 2px";
li.style.borderBottom = "1px solid #f0f0f0";
// Span para la palabra
const wordSpan = document.createElement("span");
wordSpan.textContent = word;
wordSpan.style.maxWidth = "calc(100% - 60px)";
wordSpan.style.overflow = "hidden";
wordSpan.style.textOverflow = "ellipsis";
wordSpan.style.whiteSpace = "nowrap";
wordSpan.title = word;
li.appendChild(wordSpan);
// Contenedor para los iconos de acción
const iconContainer = document.createElement("span");
iconContainer.style.display = "flex";
iconContainer.style.gap = "8px";
// Botón de edición y eliminación
const editBtn = document.createElement("button");
editBtn.innerHTML = "✏️";
editBtn.title = "Editar";
editBtn.style.border = "none";
editBtn.style.background = "transparent";
editBtn.style.cursor = "pointer";
editBtn.style.padding = "2px";
editBtn.style.fontSize = "14px";
editBtn.addEventListener("click", () => {
const newWord = prompt("Editar palabra:", word);
if (newWord !== null && newWord.trim() !== word)
{
window.dictionaryWords.delete(word);
window.dictionaryWords.add(newWord.trim());
renderDictionaryList(ulElement, currentFilter);
}
});
// Botón de eliminación
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "🗑️";
deleteBtn.title = "Eliminar";
deleteBtn.style.border = "none";
deleteBtn.style.background = "transparent";
deleteBtn.style.cursor = "pointer";
deleteBtn.style.padding = "2px";
deleteBtn.style.fontSize = "14px";
deleteBtn.addEventListener("click", () => {
// Confirmación antes de eliminar
if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`))
{
window.dictionaryWords.delete(word);
renderDictionaryList(ulElement, currentFilter);
}
});
iconContainer.appendChild(editBtn);
iconContainer.appendChild(deleteBtn);
li.appendChild(iconContainer);
ulElement.appendChild(li);
});
// Guardar el diccionario actualizado en localStorage después de cada render
try
{
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando el diccionario en localStorage:", e);
}
}
// Función para manejar el archivo XML arrastrado
function exportExcludedWordsList()
{
// Verificar si hay palabras excluidas
if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
{
alert("No hay palabras especiales ni reemplazos para exportar.");
return;
}
// Crear el contenido XML
let xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n`;
xmlContent +=
Array.from(excludedWords)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map(w => ` <word>${xmlEscape(w)}</word>`)
.join("\n");
// Añadir reemplazos si existen
if (Object.keys(replacementWords).length > 0)
{
xmlContent += "\n";
xmlContent +=
Object.entries(replacementWords)
.map(([ from, to ]) => ` <replacement from="${
xmlEscape(from)}">${xmlEscape(to)}</replacement>`)
.join("\n");
}
xmlContent += "\n</ExcludedWords>";
// Crear el Blob y descargarlo
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
// Crear un enlace temporal para descargar el archivo
const url = URL.createObjectURL(blob);
// Crear un elemento <a> para descargar el archivo
const a = document.createElement("a");
a.href = url;
a.download = "wme_excluded_words_export.xml";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Función para exportar palabras del diccionario a XML
function exportDictionaryWordsList()
{
// Verificar si hay palabras en el diccionario
if (window.dictionaryWords.size === 0)
{
alert(
"La lista de palabras del diccionario está vacía. Nada que exportar.");
return;
}
// Crear el contenido XML
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<diccionario>\n${
Array.from(window.dictionaryWords)
.sort((a, b) => a.toLowerCase().localeCompare(
b.toLowerCase())) // Exportar ordenado
.map(w => ` <word>${xmlEscape(w)}</word>`) // Indentación y escape
.join("\n")}\n</diccionario>`;
// Crear el Blob y descargarlo
const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" }); // Añadir charset
// Crear un enlace temporal para descargar el archivo
const url = URL.createObjectURL(blob);
// Crear un elemento <a> para descargar el archivo
const a = document.createElement("a");
a.href = url;
a.download = "wme_dictionary_words_export.xml"; // Nombre más descriptivo
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Función para exportar datos compartidos a XML
function xmlEscape(str)
{
return str.replace(/[<>&"']/g, function(match) {
switch (match)
{
case '<':
return '<';
case '>':
return '>';
case '&':
return '&';
case '"':
return '"';
case "'":
return ''';
default:
return match;
}
});
}
// Función para manejar el archivo XML arrastrado
waitForSidebarAPI();
//Llamar a la función para mostrar el changelog
showChangelogOnUpdate();
})();
// Función reutilizable para mostrar el spinner de carga
function showLoadingSpinner()
{
const scanSpinner = document.createElement("div");
scanSpinner.id = "scanSpinnerOverlay";
scanSpinner.style.position = "fixed";
scanSpinner.style.top = "0";
scanSpinner.style.left = "0";
scanSpinner.style.width = "100%";
scanSpinner.style.height = "100%";
scanSpinner.style.background = "rgba(0, 0, 0, 0.5)";
scanSpinner.style.zIndex = "10000";
scanSpinner.style.display = "flex";
scanSpinner.style.justifyContent = "center";
scanSpinner.style.alignItems = "center";
const scanContent = document.createElement("div");
scanContent.style.background = "#fff";
scanContent.style.padding = "20px";
scanContent.style.borderRadius = "8px";
scanContent.style.textAlign = "center";
const spinner = document.createElement("div");
spinner.classList.add("spinner");
spinner.style.border = "6px solid #f3f3f3";
spinner.style.borderTop = "6px solid #3498db";
spinner.style.borderRadius = "50%";
spinner.style.width = "40px";
spinner.style.height = "40px";
spinner.style.animation = "spin 1s linear infinite";
spinner.style.margin = "0 auto 10px auto";
const progressText = document.createElement("div");
progressText.id = "scanProgressText";
progressText.textContent = "Analizando lugares: 0%";
progressText.style.fontSize = "14px";
progressText.style.color = "#333";
scanContent.appendChild(spinner);
scanContent.appendChild(progressText);
scanSpinner.appendChild(scanContent);
document.body.appendChild(scanSpinner);
const style = document.createElement("style");
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
// Función para obtener el ícono de categoría
function getCategoryIcon(categoryName)
{
// Mapa de categorías a íconos con soporte bilingüe
const categoryIcons = {
// Comida y Restaurantes / Food & Restaurants
"FOOD_AND_DRINK": { icon: "🦞🍷", es: "Comida y Bebidas", en: "Food and Drinks" },
"RESTAURANT": { icon: "🍽️", es: "Restaurante", en: "Restaurant" },
"FAST_FOOD": { icon: "🍔", es: "Comida rápida", en: "Fast Food" },
"CAFE": { icon: "☕", es: "Cafetería", en: "Cafe" },
"BAR": { icon: "🍺", es: "Bar", en: "Bar" },
"BAKERY": { icon: "🥖", es: "Panadería", en: "Bakery" },
"ICE_CREAM": { icon: "🍦", es: "Heladería", en: "Ice Cream Shop" },
"DEPARTMENT_STORE": { icon: "🏬", es: "Tienda por departamentos", en: "Department Store" },
"PARK": { icon: "🌳", es: "Parque", en: "Park" },
// Compras y Servicios / Shopping & Services
"FASHION_AND_CLOTHING": { icon: "👗", es: "Moda y Ropa", en: "Fashion and Clothing" },
"SHOPPING_AND_SERVICES": { icon: "👜👝", es: "Mercado o Tienda", en: "Shopping and Services" },
"SHOPPING_CENTER": { icon: "🛍️", es: "Centro comercial", en: "Shopping Center" },
"SUPERMARKET_GROCERY": { icon: "🛒", es: "Supermercado", en: "Supermarket" },
"MARKET": { icon: "🛒", es: "Mercado", en: "Market" },
"CONVENIENCE_STORE": { icon: "🏪", es: "Tienda", en: "Convenience Store" },
"PHARMACY": { icon: "💊", es: "Farmacia", en: "Pharmacy" },
"BANK": { icon: "🏦", es: "Banco", en: "Bank" },
"ATM": { icon: "💳", es: "Cajero automático", en: "ATM" },
"HARDWARE_STORE": { icon: "🔧", es: "Ferretería", en: "Hardware Store" },
"COURTHOUSE": { icon: "⚖️", es: "Corte", en: "Courthouse" },
"FURNITURE_HOME_STORE": { icon: "🛋️", es: "Tienda de muebles", en: "Furniture Store" },
"TOURIST_ATTRACTION_HISTORIC_SITE": { icon: "🗿", es: "Atracción turística o Sitio histórico", en: "Tourist Attraction or Historic Site" },
"PET_STORE_VETERINARIAN_SERVICES": { icon: "🦮🐈", es: "Tienda de mascotas o Veterinaria", en: "Pet Store or Veterinary Services" },
"CEMETERY": { icon: "🪦", es: "Cementerio", en: "Cemetery" },
"KINDERGARDEN": { icon: "🍼", es: "Jardín Infantil", en: "Kindergarten" },
"JUNCTION_INTERCHANGE": { icon: "🔀", es: "Cruce o Intercambio", en: "Junction or Interchange" },
"OUTDOORS": { icon: "🏞️", es: "Aire libre", en: "Outdoors" },
"ORGANIZATION_OR_ASSOCIATION": { icon: "👔", es: "Organización o Asociación", en: "Organization or Association" },
"TRAVEL_AGENCY": { icon: "🧳", es: "Agencia de viajes", en: "Travel Agency" },
"BANK_FINANCIAL": { icon: "💰", es: "Banco o Financiera", en: "Bank or Financial Institution" },
"SPORTING_GOODS": { icon: "🛼🏀🏐", es: "Artículos deportivos", en: "Sporting Goods" },
"TOY_STORE": { icon: "🧸", es: "Tienda de juguetes", en: "Toy Store" },
"CURRENCY_EXCHANGE": { icon: "💶💱", es: "Casa de cambio", en: "Currency Exchange" },
"PHOTOGRAPHY": { icon: "📸", es: "Fotografía", en: "Photography" },
"DESSERT": { icon: "🍰", es: "Postre", en: "Dessert" },
"FOOD_COURT": { icon: "🥗", es: "Comedor o Patio de comidas", en: "Food Court" },
"CANAL": { icon: "〰", es: "Canal", en: "Canal" },
"JEWELRY": { icon: "💍", es: "Joyería", en: "Jewelry" },
// Transporte / Transportation
"TRAIN_STATION": { icon: "🚂", es: "Estación de tren", en: "Train Station" },
"GAS_STATION": { icon: "⛽", es: "Estación de servicio", en: "Gas Station" },
"PARKING_LOT": { icon: "🅿️", es: "Estacionamiento", en: "Parking Lot" },
"BUS_STATION": { icon: "🚍", es: "Terminal de bus", en: "Bus Station" },
"AIRPORT": { icon: "✈️", es: "Aeropuerto", en: "Airport" },
"CAR_WASH": { icon: "🚗💦", es: "Lavado de autos", en: "Car Wash" },
"CAR_RENTAL": { icon: "🚘🛺🛻🚙", es: "Alquiler de Vehículos", en: "Car Rental" },
"TAXI_STATION": { icon: "🚕", es: "Estación de taxis", en: "Taxi Station" },
"FOREST_GROVE": { icon: "🌳", es: "Bosque", en: "Forest Grove" },
"GARAGE_AUTOMOTIVE_SHOP": { icon: "🔧🚗", es: "Taller mecánico", en: "Automotive Garage" },
"GIFTS": { icon: "🎁", es: "Tienda de regalos", en: "Gift Shop" },
"TOLL_BOOTH": { icon: "🚧", es: "Peaje", en: "Toll Booth" },
"CHARGING_STATION": { icon: "🔋", es: "Estación de carga", en: "Charging Station" },
"CAR_SERVICES": { icon: "🚗🔧", es: "Servicios de automóviles", en: "Car Services" },
"STADIUM_ARENA": { icon: "🏟️", es: "Estadio o Arena", en: "Stadium or Arena" },
"CAR_DEALERSHIP": { icon: "🚘🏢", es: "Concesionario de autos", en: "Car Dealership" },
"FERRY_PIER": { icon: "⛴️", es: "Muelle de ferry", en: "Ferry Pier" },
"INFORMATION_POINT": { icon: "ℹ️", es: "Punto de información", en: "Information Point" },
"REST_AREAS": { icon: "🏜", es: "Áreas de descanso", en: "Rest Areas" },
"MUSIC_VENUE": { icon: "🎶", es: "Lugar de música", en: "Music Venue" },
"CASINO": { icon: "🎰", es: "Casino", en: "Casino" },
"CITY_HALL": { icon: "🎩", es: "Ayuntamiento", en: "City Hall" },
"PERFORMING_ARTS_VENUE": { icon: "🎭", es: "Lugar de artes escénicas", en: "Performing Arts Venue" },
"TUNNEL": { icon: "🔳", es: "Túnel", en: "Tunnel" },
"SEAPORT_MARINA_HARBOR": { icon: "⚓", es: "Puerto o Marina", en: "Seaport or Marina" },
// Alojamiento / Lodging
"HOTEL": { icon: "🏨", es: "Hotel", en: "Hotel" },
"HOSTEL": { icon: "🛏️", es: "Hostal", en: "Hostel" },
"LODGING": { icon: "⛺", es: "Alojamiento", en: "Lodging" },
"MOTEL": { icon: "🛕", es: "Motel", en: "Motel" },
"SWIMMING_POOL": { icon: "🏊", es: "Piscina", en: "Swimming Pool" },
"RIVER_STREAM": { icon: "🌊", es: "Río o Arroyo", en: "River or Stream" },
"CAMPING_TRAILER_PARK": { icon: "🏕️", es: "Camping o Parque de Trailers", en: "Camping or Trailer Park" },
"SEA_LAKE_POOL": { icon: "🏖️", es: "Mar, Lago o Piscina", en: "Sea, Lake or Pool" },
"FARM": { icon: "🚜", es: "Granja", en: "Farm" },
"NATURAL_FEATURES": { icon: "🌲", es: "Características naturales", en: "Natural Features" },
// Salud / Healthcare
"HOSPITAL": { icon: "🏥", es: "Hospital", en: "Hospital" },
"HOSPITAL_URGENT_CARE": { icon: "🏥🚑", es: "Urgencias", en: "Urgent Care" },
"DOCTOR_CLINIC": { icon: "🏥⚕️", es: "Clínica", en: "Clinic" },
"DOCTOR": { icon: "👨⚕️", es: "Consultorio médico", en: "Doctor's Office" },
"VETERINARY": { icon: "🐾", es: "Veterinaria", en: "Veterinary" },
"PERSONAL_CARE": { icon: "💅💇🦷", es: "Cuidado personal", en: "Personal Care" },
"FACTORY_INDUSTRIAL": { icon: "🏭", es: "Fábrica o Industrial", en: "Factory or Industrial" },
"MILITARY": { icon: "🪖", es: "Militar", en: "Military" },
"LAUNDRY_DRY_CLEAN": { icon: "🧺", es: "Lavandería o Tintorería", en: "Laundry or Dry Clean" },
"PLAYGROUND": { icon: "🛝", es: "Parque infantil", en: "Playground" },
"TRASH_AND_RECYCLING_FACILITIES": { icon: "🗑️♻️", es: "Instalaciones de basura y reciclaje", en: "Trash and Recycling Facilities" },
// Educación / Education
"UNIVERSITY": { icon: "🎓", es: "Universidad", en: "University" },
"COLLEGE_UNIVERSITY": { icon: "🏫", es: "Colegio", en: "College" },
"SCHOOL": { icon: "🎒", es: "Escuela", en: "School" },
"LIBRARY": { icon: "📖", es: "Biblioteca", en: "Library" },
"FLOWERS": { icon: "💐", es: "Floristería", en: "Flower Shop" },
"CONVENTIONS_EVENT_CENTER": { icon: "🎤🥂", es: "Centro de convenciones o eventos", en: "Convention or Event Center" },
"CLUB": { icon: "♣", es: "Club", en: "Club" },
"ART_GALLERY": { icon: "🖼️", es: "Galería de arte", en: "Art Gallery" },
"NATURAL_FEATURES": { icon: "🌄", es: "Características naturales", en: "Natural Features" },
// Entretenimiento / Entertainment
"CINEMA": { icon: "🎬", es: "Cine", en: "Cinema" },
"THEATER": { icon: "🎭", es: "Teatro", en: "Theater" },
"MUSEUM": { icon: "🖼", es: "Museo", en: "Museum" },
"CULTURE_AND_ENTERTAINEMENT": { icon: "🎨", es: "Cultura y Entretenimiento", en: "Culture and Entertainment" },
"STADIUM": { icon: "🏟️", es: "Estadio", en: "Stadium" },
"GYM": { icon: "💪", es: "Gimnasio", en: "Gym" },
"GYM_FITNESS": { icon: "🏋️", es: "Gimnasio o Fitness", en: "Gym or Fitness" },
"GAME_CLUB": { icon: "⚽🏓", es: "Club de juegos", en: "Game Club" },
"BOOKSTORE": { icon: "📖📚", es: "Librería", en: "Bookstore" },
"ELECTRONICS": { icon: "📱💻", es: "Electrónica", en: "Electronics" },
"SPORTS_COURT": { icon: "⚽🏀", es: "Cancha deportiva", en: "Sports Court" },
"GOLF_COURSE": { icon: "⛳", es: "Campo de golf", en: "Golf Course" },
"SKI_AREA": { icon: "⛷️", es: "Área de esquí", en: "Ski Area" },
"RACING_TRACK": { icon: "🛷⛸🏎️", es: "Pista de carreras", en: "Racing Track" },
// Gobierno y Servicios Públicos / Government & Public Services
"GOVERNMENT": { icon: "🏛️", es: "Oficina gubernamental", en: "Government Office" },
"POLICE_STATION": { icon: "👮", es: "Estación de policía", en: "Police Station" },
"FIRE_STATION": { icon: "🚒", es: "Estación de bomberos", en: "Fire Station" },
"FIRE_DEPARTMENT": { icon: "🚒", es: "Departamento de bomberos", en: "Fire Department" },
"POST_OFFICE": { icon: "📫", es: "Correo", en: "Post Office" },
"TRANSPORTATION": { icon: "🚌", es: "Transporte", en: "Transportation" },
"PRISON_CORRECTIONAL_FACILITY": { icon: "👁️🗨️", es: "Prisión o Centro Correccional", en: "Prison or Correctional Facility" },
// Religión / Religion
"RELIGIOUS_CENTER": { icon: "⛪", es: "Iglesia", en: "Church" },
// Otros / Others
"RESIDENTIAL": { icon: "🏘️", es: "Residencial", en: "Residential" },
"RESIDENCE_HOME": { icon: "🏠", es: "Residencia o Hogar", en: "Residence or Home" },
"OFFICES": { icon: "🏢", es: "Oficina", en: "Office" },
"FACTORY": { icon: "🏭", es: "Fábrica", en: "Factory" },
"CONSTRUCTION_SITE": { icon: "🏗️", es: "Construcción", en: "Construction" },
"MONUMENT": { icon: "🗽", es: "Monumento", en: "Monument" },
"BRIDGE": { icon: "🌉", es: "Puente", en: "Bridge" },
"PROFESSIONAL_AND_PUBLIC": { icon: "🗄💼", es: "Profesional y Público", en: "Professional and Public" },
"OTHER": { icon: "🚪", es: "Otro", en: "Other" },
"ARTS_AND_CRAFTS": { icon: "🎨", es: "Artes y Manualidades", en: "Arts and Crafts" },
"COTTAGE_CABIN": { icon: "🏡", es: "Cabaña", en: "Cottage Cabin" },
"TELECOM": { icon: "📡", es: "Telecomunicaciones", en: "Telecommunications" }
};
// Si no hay categoría, devolver ícono por defecto
if (!categoryName) {
return { icon: "❓", title: "Sin categoría / No category" };
}
// Normalizar el nombre de la categoría
const normalizedInput = categoryName.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
//console.log("[WME PLN DEBUG] Buscando ícono para categoría:", categoryName);
//console.log("[WME PLN DEBUG] Nombre normalizado:", normalizedInput);
// 1. Buscar coincidencia exacta por clave interna (ej: "PARK")
for (const [key, data] of Object.entries(categoryIcons)) {
if (key.toLowerCase() === normalizedInput) {
return { icon: data.icon, title: `${data.es} / ${data.en}` };
}
}
// Buscar coincidencia en el mapa de categorías
for (const [key, data] of Object.entries(categoryIcons))
{
// Normalizar los nombres en español e inglés para la comparación
const normalizedES = data.es.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
const normalizedEN = data.en.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
if (normalizedInput === normalizedES || normalizedInput === normalizedEN)
{
return { icon: data.icon, title: `${data.es} / ${data.en}` };
}
}
// Si no se encuentra coincidencia, devolver ícono por defecto
//console.log("[WME PLN DEBUG] No se encontró coincidencia, usando ícono por defecto");
return {
icon: "⚪",
title: `${categoryName} (Sin coincidencia / No match)`
};
}// getCategoryIcon
// Función para agregar una palabra al diccionario
function addWordToDictionary(input)
{
const newWord = input.value.trim().toLowerCase();
if (!newWord)
{
alert("La palabra no puede estar vacía.");
return;
}
// Validaciones básicas antes de añadir
if (newWord.length === 1 && !newWord.match(/[a-zA-Z0-9]/)) {
alert("No se permite agregar un solo carácter que no sea alfanumérico.");
return;
}
if (commonWords.includes(newWord)) {
alert("Esa palabra es muy común y no debe agregarse al diccionario.");
return;
}
if (excludedWords.has(newWord)) {
alert("Esa palabra ya existe en la lista de especiales (excluidas).");
return;
}
if (window.dictionaryWords.has(newWord)) {
alert("La palabra ya existe en el diccionario.");
return;
}
if (!window.dictionaryWords) window.dictionaryWords = new Set();
if (!window.dictionaryIndex) window.dictionaryIndex = {};
window.dictionaryWords.add(newWord); // Añadir al Set
// === AÑADIR AL ÍNDICE ===
const firstChar = newWord.charAt(0).toLowerCase();
if (!window.dictionaryIndex[firstChar]) {
window.dictionaryIndex[firstChar] = [];
}
window.dictionaryIndex[firstChar].push(newWord); // Añadir al índice
input.value = ""; // Limpiar el input
renderDictionaryList(document.getElementById("dictionaryWordsList")); // Re-renderizar la lista
// Guardar en localStorage después de añadir
try
{
localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
}
catch (e)
{
console.error("[WME PLN] Error guardando diccionario en localStorage después de añadir manualmente:", e);
}
}// addWordToDictionary
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址