Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyforks.org/scripts/548860/1657864/WME%20PLN%20Module%20-%20Categories%20Handler.js
// ==UserScript==
// @name WME PLN Module - Categories Handler
// @version 9.0.0
// @description Módulo de categorías para WME Place Normalizer. No funciona por sí solo.
// @author mincho77
// @license MIT
// @grant none
// ==/UserScript==
//Función Para Cargar Categorías Desde Google Sheets
async function loadDynamicCategoriesFromSheet(cfg)
{
const SPREADSHEET_ID = cfg?.spreadsheetId || "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
const API_KEY = cfg?.apiKey || "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
const RANGE = cfg?.range || "Categories!A2:E";
const TTL_MS = Number(cfg?.cacheTTLHours || 24) * 60 * 60 * 1000;
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) =>
{
if (!SPREADSHEET_ID || !API_KEY)
{
plnLog('warn','[WME PLN] No se ha configurado SPREADSHEET_ID o API_KEY. Se omitirá la carga de categorías dinámicas.');
resolve();
return;
}
// Check for cached data first
const cachedData = localStorage.getItem("wme_pln_categories_cache");
if (cachedData)
{
try
{
const { data, timestamp } = JSON.parse(cachedData);
// Use cache if less than TTL_MS old
if (data && timestamp && (Date.now() - timestamp < TTL_MS))
{
plnLog('ui', '[WME PLN] Usando categorías en caché. Reconstruyendo RegExp...');
// Se itera sobre los datos de la caché para reconstruir las expresiones regulares
window.dynamicCategoryRules = data.map(rule =>
{
if (rule.keyword)
{ // Asegurarse de que la regla tenga keywords
const canonical = String(rule.keyword || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
const keywords = canonical.split(';').map(k => k.trim()).filter(k => k.length > 0);
const regexParts = keywords.map(k => `\\b${PLNCore.utils.escapeRegExp(k)}\\b`);
const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
// Devolver la regla con la propiedad compiledRegex correctamente creada
return { ...rule, compiledRegex: combinedRegex };
}
return rule; // Devuelve la regla sin cambios si no tiene keyword
});
window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length);
resolve();
return;
}
}
catch (e)
{
plnLog('warn','[WME PLN] Error al leer caché de categorías:', e);
}
}
PLNCore.net.request(
{
method: "GET",
url: url,
timeout: 10000, // Add timeout
onload: function (response)
{
if (response.status >= 200 && response.status < 300)
{
try
{
const data = JSON.parse(response.responseText);
if (data.values) {
// El procesamiento de los datos de la API ya era correcto
window.dynamicCategoryRules = data.values.map(row =>
{
const keyword = (row[0] || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
const keywords = keyword.split(';').map(k => k.trim()).filter(k => k.length > 0);
const regexParts = keywords.map(k => `\\b${PLNCore.utils.escapeRegExp(k)}\\b`);
const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
return {
keyword: keyword,
categoryKey: row[1] || '',
icon: row[2] || '⚪',
desc_es: row[3] || 'Sin descripción',
desc_en: row[4] || 'No description',
compiledRegex: combinedRegex
};
});
window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length);
// La lógica para guardar en caché también es correcta
try
{
localStorage.setItem("wme_pln_categories_cache", JSON.stringify(
{
data: window.dynamicCategoryRules,
timestamp: Date.now()
}));
}
catch (e)
{
plnLog('warn','[WME PLN] Error al guardar caché de categorías:', e);
}
plnLog('ui', '[WME PLN] Categorías cargadas desde API');
}
}
catch (e)
{
plnLog('error','[WME PLN] Error al procesar datos de categorías:', e);
}
}
else
{
plnLog('warn',`[WME PLN] Error HTTP ${response.status} al cargar categorías`);
}
resolve();
},
onerror: function (error)
{
plnLog('error','[WME PLN] Error de red al cargar categorías:', error);
resolve();
},
ontimeout: function ()
{
plnLog('error','[WME PLN] Timeout al cargar categorías');
resolve();
}
});
});
}//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().normalize("NFD").replace(/[\u0300-\u036f]/g, "");// Convertir el nombre del lugar a minúsculas y normalizar para comparaciones insensibles a mayúsculas y diacríticos
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);
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 = PLNCore.utils.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
}
}
}
}
plnLog('ui', `[WME PLN][DEBUG] findCategoryForPlace para "${placeName}" devolvió: `, allMatchingRules);
return allMatchingRules;
}//findCategoryForPlace
//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 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" },
"THEME_PARK": { icon: "🎢", es: "Parque de atracciones, Parque Temático", en: "Theme Park" },
"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();
plnLog('ui', "[WME_PLN][DEBUG] Buscando ícono para categoría:", categoryName);
plnLog('ui', "[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
plnLog('ui', "[WME_PLN][DEBUG] No se encontró coincidencia, usando ícono por defecto");
return {
icon: "⚪",
title: `${categoryName} (Sin coincidencia / No match)`
};
}// getCategoryIcon
// Crea un dropdown para seleccionar categorías recomendadas
function createRecommendedCategoryDropdown(placeId, currentCategoryKey, dynamicCategorySuggestions)
{
window.tempSelectedCategories = window.tempSelectedCategories || new Map();
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)
{
plnLog('error', 'Lugar no encontrado para actualizar categoría.');
return;
}
try
{
const UpdateObject = (window.require && window.require("Waze/Action/UpdateObject")) || null;
if (!UpdateObject) {
plnLog('error', 'No se pudo cargar Waze/Action/UpdateObject (SDK no listo).');
plnToast('No se pudo aplicar la categoría (SDK no listo).', 3000);
return;
}
const action = new UpdateObject(placeToUpdate, { categories: [suggestion.categoryKey] });
W.model.actionManager.add(action);
// Obtener la celda de la categoría original y aplicar un estilo de opacidad
const row = document.querySelector(`tr[data-place-id="${placeId}"]`); // Obtener la fila
row.dataset.categoryChanged = 'true'; // Marcar fila como modificada
// Habilitar el botón de aplicar sugerencia
const applyButton = row.querySelector('button[title="Aplicar sugerencia"]');
if (applyButton)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
}
//Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, suggestion.categoryKey);
// Asegurarse de que la fila existe antes de intentar acceder a sus celdas
if (row)
{
const originalCategoryCell = row.querySelector('td:nth-child(10)'); // La décima columna es "Categoría"
if (originalCategoryCell)
{
originalCategoryCell.style.opacity = '0.5'; // Atenuar la celda completa
originalCategoryCell.title += ' (Modificada)'; // Opcional, añadir un tooltip
}
}
// : 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
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)
{
plnLog('error', 'Error al actualizar la categoría desde dropdown:', e);
plnToast("Error al actualizar la categoría: " + e.message, 3000); // 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
searchInput.style.opacity = '1.0'; // Opacidad normal para el input
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";
}
});
// Añadir evento click para seleccionar la categoría
optionDiv.addEventListener("click", async () =>
{
const placeToUpdate = W.model.venues.getObjectById(placeId);
if (!placeToUpdate)
{
//console.error("[WME_PLN] Lugar no encontrado para actualizar categoría.");
return;
}
try
{
const UpdateObject = (window.require && window.require("Waze/Action/UpdateObject")) || null;
if (!UpdateObject) {
plnLog('error', 'No se pudo cargar Waze/Action/UpdateObject (SDK no listo).');
plnToast('No se pudo aplicar la categoría (SDK no listo).', 3000);
return;
}
const action = new UpdateObject(placeToUpdate, { categories: [rule.categoryKey] });
W.model.actionManager.add(action);
// ✅ CORRECCIÓN: Se declara 'row' aquí, ANTES de su primer uso.
const row = document.querySelector(`tr[data-place-id="${placeId}"]`);
// Ahora es seguro usar la variable 'row'.
if (row)
{
row.dataset.categoryChanged = 'true'; // Marcar fila como modificada
const applyButton = row.querySelector('button[title="Aplicar sugerencia"]');
// Habilitar el botón de aplicar sugerencia
if (applyButton)
{
applyButton.disabled = false;
applyButton.style.opacity = "1";
}
}
// Actualizar visualmente la celda de Categoría Actual en la tabla
updateCategoryDisplayInTable(placeId, rule.categoryKey);
// Atenuar la celda de la categoría original
if (row)
{
const categoryCell = row.querySelector('td:nth-child(10)');
if (categoryCell)
{
const currentCategoryDiv = categoryCell.querySelector('div');
if (currentCategoryDiv)
{
currentCategoryDiv.style.opacity = '0.5';
currentCategoryDiv.title += ' (Modificada)';
}
}
}
// Actualizar el valor del input con icono y descripción de la selección
searchInput.value = `${rule.icon} ${displayDesc}`;
searchInput.style.setProperty('opacity', '1.0', 'important'); // Usar setProperty para asegurar visibilidad
// Ocultar la lista de opciones
optionsListDiv.style.display = "none";
searchInput.blur();
}
catch (e)
{
plnLog('error', "[WME_PLN] Error al actualizar la categoría desde dropdown:", e);
plnToast("Error al actualizar la categoría: " + e.message, 3000);
}
});
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
// Limpiamos los listeners anteriores y los reescribimos de forma más robusta.
let debounceTimer;
searchInput.addEventListener("input", () =>
{
clearTimeout(debounceTimer);
// Muestra la lista y filtra mientras el usuario escribe.
debounceTimer = setTimeout(() => {
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
}, 200);
});
searchInput.addEventListener("focus", () =>
{
// Al hacer foco, muestra la lista completa.
populateOptions(searchInput.value);
optionsListDiv.style.display = "block";
});
// Usamos 'mousedown' en lugar de 'click' para cerrar el menú.
// Esto evita conflictos con el evento 'click' de las opciones.
document.addEventListener("mousedown", (e) =>
{
if (!wrapperDiv.contains(e.target))
{
optionsListDiv.style.display = "none";
}
});
populateOptions(""); // Cargar las opciones inicialmente (sin filtro)
return wrapperDiv;
}// createRecommendedCategoryDropdown