Detta skript bör inte installeras direkt. Det är ett bibliotek för andra skript att inkludera med meta-direktivet // @require https://update.greasyforks.org/scripts/548859/1657862/WME%20PLN%20Module%20-%20Geolocation.js
// ==UserScript==
// @name WME PLN Module - Geolocation
// @version 9.0.0
// @description Módulo de geolocalización para WME Place Normalizer. No funciona por sí solo.
// @author mincho77
// @license MIT
// @grant none
// ==/UserScript==
//Función para obtener coordenadas de un lugar
function getPlaceCoordinates(venueOldModel, venueSDK)
{
let lat = null;
let lon = null;
const placeId = venueOldModel ? venueOldModel.getID() : (venueSDK ? venueSDK.id : 'N/A');
// PRIORIDAD 1: Usar el método recomendado getOLGeometry() del modelo antiguo, es el más estable.
if (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')
{
// La geometría de OpenLayers (OL) está en proyección Mercator (EPSG:3857)
// Necesitamos transformarla a coordenadas geográficas WGS84 (EPSG:4326)
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"),
new OpenLayers.Projection("EPSG:4326")
);
lat = wgs84Point.y;
lon = wgs84Point.x;
// Validar que las coordenadas resultantes sean válidas
if (typeof lat === 'number' && typeof lon === 'number' && Math.abs(lat) <= 90 && Math.abs(lon) <= 180)
{
return { lat, lon };
}
}
}
}
}
catch (e)
{
plnLog('error',`[WME PLN] Error obteniendo coordenadas con getOLGeometry() para ID ${placeId}:`, e);
}
}
// PRIORIDAD 2: Fallback al objeto del SDK si el método anterior falló.
// Esto es menos ideal porque .geometry está obsoleto, pero sirve como respaldo.
if (venueSDK && venueSDK.geometry && Array.isArray(venueSDK.geometry.coordinates)) {
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) {
return { lat, lon };
}
}
// Si todo falló, retornar nulls
plnLog('geo', `[WME PLN] No se pudieron obtener coordenadas válidas para el ID ${placeId}.`);
return { lat: null, lon: null };
}//getPlaceCoordinates
// 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 calcDist = PLNCore?.utils?.calculateDistance;
const distance = typeof calcDist === 'function' ? calcDist(p1.lat, p1.lon, p2.lat, p2.lon) : Infinity;
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><br>
<span style="font-weight: bold; color: #007bff;">Registros: [${lineNumbers.join("],[")}]</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.setAttribute("role", "dialog");
modal.setAttribute("aria-label", "Duplicados cercanos");
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
}
}
}//detectAndAlertDuplicateNames
// Función para aplicar la ciudad seleccionada a un lugar
async function plnApplyCityToVenue(venueId, selectedCityId, selectedCityName)
{
plnLog('geo', 'apply:start', { venueId, selectedCityId, selectedCityName });
if (!wmeSDK?.DataModel?.Venues?.updateAddress)
{
plnLog('geo', 'apply:sdkNotReady');
return;
}
try
{
const venueIdStr = String(venueId);
const cityIdNum = Number(selectedCityId) || 0;
// Intento obtener houseNumber (no bloqueante), es una buena práctica mantenerlo.
let houseNumber = '';
try
{
const v0 = wmeSDK.DataModel.Venues.getById?.({ venueId: venueIdStr });
if (v0?.address?.houseNumber) houseNumber = String(v0.address.houseNumber);
}
catch (_)
{ /* noop */ }
// MODIFICACIÓN CLAVE: Se elimina la lógica de espera y el "Plan B (bridge)".
// Simplemente llamamos a la función que aplica la ciudad y confiamos en que funciona.
const attemptKind = plnApplyCityOnce(venueIdStr, cityIdNum, houseNumber);
if (attemptKind)
{
// Si attemptKind no es nulo, significa que se pudo construir y enviar la solicitud al SDK.
// Asumimos el éxito aquí, ya que la espera en la UI es el punto de fallo.
plnLog('geo', 'apply:doneWithSDK: optimistic success');
// El llamado a plnTryAutoApplyAddressPanel ya está dentro de plnApplyCityOnce,
// por lo que se ejecutará automáticamente.
return;
}
// Si plnApplyCityOnce devuelve null, significa que no pudo encontrar los IDs necesarios.
// Solo en este caso, lanzamos el error.
plnLog('geo', 'apply:noSdkVenueOrAddress', { reason: "Could not resolve IDs for city.", cityIdNum });
}
catch (e)
{
plnLog('error', 'apply:sdkBranchError', e);
}
}//plnApplyCityToVenue
//**************************************************************************
//Nombre: plnExtractAddressIds
//Fecha modificación: 2025-08-10
//Descripción: SDK‑only. Obtiene countryID y stateID desde sdkVenue.address,
// incluso cuando Street/City están vacíos.
//**************************************************************************
function plnExtractAddressIds(venueId, sdkVenue) {
plnLog('geo', 'extractIds:start', { venueId, hasSdkVenue: !!sdkVenue });
const out = { countryID: null, stateID: null, streetName: '', houseNumber: '' };
if (sdkVenue && sdkVenue.address) {
const addr = sdkVenue.address;
out.countryID = addr?.country?.id ?? addr?.countryID ?? addr?.countryId ?? null;
out.stateID = addr?.state?.id ?? addr?.stateID ?? addr?.stateId ?? null;
out.streetName = addr?.street?.name ?? addr?.streetName ?? '';
out.houseNumber = addr?.houseNumber ?? '';
}
plnLog('geo', 'extractIds:fromSDK', out);
return out;
}
//**************************************************************************
//Nombre: plnResolveIdsFromCity
//Fecha modificación: 2025-08-10
//Descripción: SDK-only. A partir de cityId intenta obtener stateID y countryID
// usando los repositorios del SDK (Cities → States → Countries).
//**************************************************************************
function plnResolveIdsFromCity(cityId)
{
const out = { countryID: null, stateID: null };
try {
if (!wmeSDK || !wmeSDK.DataModel) return out;
const cityIdNum = Number(cityId);
let city = null;
try {
if (wmeSDK.DataModel.Cities?.getById) {
city = wmeSDK.DataModel.Cities.getById({ cityId: cityIdNum }); // <-- number
}
} catch(_) {}
plnLog('geo', 'resolveFromCity:city', { requested: cityIdNum, found: !!city });
if (!city) return out;
let stateId = city.state?.id ?? city.stateID ?? city.stateId ?? city.attributes?.state?.attributes?.id ?? city.attributes?.state?.id ?? null;
let countryId = city.country?.id ?? city.countryID ?? city.countryId ?? city.attributes?.country?.attributes?.id ?? city.attributes?.country?.id ?? null;
if (!countryId && stateId && wmeSDK.DataModel.States?.getById) {
try {
const state = wmeSDK.DataModel.States.getById({ stateId: Number(stateId) }); // <-- number
countryId = state?.country?.id ?? state?.countryID ?? state?.countryId ?? null;
} catch(_) {}
}
if (stateId) out.stateID = Number(stateId);
if (countryId) out.countryID = Number(countryId);
plnLog('geo', 'resolveFromCity:result', out);
} catch (e) {
plnLog('error','resolveFromCity:error', e);
}
return out;
}//plnResolveIdsFromCity
//Permite obtener el ID de la calle vacía (empty street) para una ciudad dada.
function plnGetEmptyStreetIdForCity(cityId)
{
const cidNum = Number(cityId);
try
{
if (wmeSDK?.DataModel?.Streets?.getStreet)
{
const st = wmeSDK.DataModel.Streets.getStreet({ cityId: cidNum, streetName: '' }); // <-- number
if (st && st.id != null) { plnLog('geo', 'streets:emptyFound', { cityId: cidNum, streetId: Number(st.id) }); return Number(st.id); }
}
}
catch (_)
{ }
try
{
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const found = all.find(s => Number(s?.city?.id) === cidNum && (s?.isEmpty || s?.name === '' || s?.streetName === ''));
if (found) { plnLog('geo', 'streets:emptyFound', { cityId: cidNum, streetId: Number(found.id) }); return Number(found.id); }
}
catch (_)
{ }
try
{
for (let i = 0; i < 8; i++)
{
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const found = all.find(s => Number(s?.city?.id) === cidNum && (s?.isEmpty || s?.name === '' || s?.streetName === ''));
if (found)
{
plnLog('geo', 'streets:emptyFound', { cityId: cidNum, streetId: Number(found.id) }); return Number(found.id);
}
}
}
catch (_)
{ }
return null;
}//plnGetEmptyStreetIdForCity
//Permite obtener la ciudad asignada a un lugar en este momento (sincrónico).
function plnGetVenueCityIdNow(venueIdStr)
{
try
{
const v = wmeSDK?.DataModel?.Venues?.getById?.({ venueId: String(venueIdStr) });
const cid = v?.address?.city?.id ?? v?.address?.cityID ?? v?.address?.cityId ?? null;
return (cid != null) ? Number(cid) : null;
}
catch (_)
{ return null; }
}
//Permite esperar hasta que un lugar tenga asignada la ciudad esperada (o se agote el tiempo).
function plnWaitVenueCity(venueIdStr, expectedCityId, timeoutMs = 1500)
{
return new Promise(resolve =>
{
const start = Date.now();
const target = Number(expectedCityId);
const tick = setInterval(() =>
{
const cid = plnGetVenueCityIdNow(venueIdStr);
if (cid === target){ clearInterval(tick); return resolve(true); }
if (Date.now() - start > timeoutMs){ clearInterval(tick); return resolve(false); }
}, 120);
});
}
//Permite buscar una ciudad puente (bridge) en un estado dado.
function plnFindBridgeCityIdInState(stateId)
{
try
{
const all = (wmeSDK?.DataModel?.Streets?.getAll?.() || []);
const match = all.find(s =>
(s?.isEmpty || s?.name === '' || s?.streetName === '') &&
Number(s?.city?.state?.id ?? s?.city?.stateID ?? s?.city?.stateId) === Number(stateId)
);
return match?.city?.id != null ? Number(match.city.id) : null;
}
catch (_)
{
return null;
}
}
//Permite aplicar una ciudad a un lugar, una sola vez.
function plnApplyCityOnce(venueIdStr, cityIdNum, houseNumber)
{
// Ruta 1: street vacío específico
const emptyStreetId = plnGetEmptyStreetIdForCity(cityIdNum);
if (emptyStreetId != null)
{
const args = { venueId: venueIdStr, streetId: Number(emptyStreetId) };
if (houseNumber) args.houseNumber = houseNumber;
plnLog('geo', 'apply:updateAddress(args)', args);
wmeSDK.DataModel.Venues.updateAddress(args);
setTimeout(()=>{ try{ plnTryAutoApplyAddressPanel?.(); }catch{} }, 200);
return 'streetId';
}
// Ruta 2: IDs completos con emptyStreet:true
const ids = plnResolveIdsFromCity(cityIdNum);
plnLog('geo', 'apply:fallbackIds', ids);
if (ids.countryID && ids.stateID)
{
const args2 =
{
venueId: venueIdStr,
countryID: Number(ids.countryID),
stateID: Number(ids.stateID),
cityID: Number(cityIdNum),
emptyStreet: true
};
if (houseNumber) args2.houseNumber = houseNumber;
plnLog('geo', 'apply:updateAddress(args2)', args2);
wmeSDK.DataModel.Venues.updateAddress(args2);
setTimeout(()=>{ try{ plnTryAutoApplyAddressPanel?.(); }catch{} }, 200);
return { type:'ids', ids };
}
return null;
}//plnApplyCityOnce