Total en dataset
-
Puntos filtrados
-
Punto más cercano
-
Ingresa tu ubicación y presiona "Buscar más cercano".
await (async () => {
const L = await import("https://cdn.jsdelivr.net/npm/leaflet@1.9.4/+esm");
const d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm");
const dataPathCandidates = [
"puntosAtencion_busqueda_completo.csv",
"./puntosAtencion_busqueda_completo.csv",
"data/puntosAtencion_busqueda_completo.csv",
"../data/puntosAtencion_busqueda_completo.csv",
"./data/puntosAtencion_busqueda_completo.csv"
];
const elements = {
departamento: document.getElementById("filtroDepartamento"),
entidad: document.getElementById("filtroEntidad"),
tipo: document.getElementById("filtroTipo"),
texto: document.getElementById("filtroBusqueda"),
soloActivos: document.getElementById("filtroSoloActivos"),
mapaBase: document.getElementById("mapaBase"),
latInput: document.getElementById("latInput"),
lonInput: document.getElementById("lonInput"),
usarUbicacionBtn: document.getElementById("usarUbicacionBtn"),
buscarCercanoBtn: document.getElementById("buscarCercanoBtn"),
centrarFiltrosBtn: document.getElementById("centrarFiltrosBtn"),
estadoUbicacion: document.getElementById("estadoUbicacion"),
statTotal: document.getElementById("statTotal"),
statFiltrados: document.getElementById("statFiltrados"),
statCercano: document.getElementById("statCercano"),
resultadoCercano: document.getElementById("resultadoCercano")
};
const tileLayers = {
osm: L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap contributors"
}),
carto_light: L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
maxZoom: 20,
attribution: "© OpenStreetMap contributors © CARTO"
}),
carto_dark: L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
maxZoom: 20,
attribution: "© OpenStreetMap contributors © CARTO"
})
};
if (window.__asfiLeafletMap) {
window.__asfiLeafletMap.remove();
}
const map = L.map("map", { preferCanvas: true, zoomControl: true }).setView([-16.5, -64.5], 6);
window.__asfiLeafletMap = map;
tileLayers.carto_light.addTo(map);
const markersLayer = L.layerGroup().addTo(map);
const nearestLayer = L.layerGroup().addTo(map);
let allRows = [];
let filteredRows = [];
let lastNearest = null;
function cleanText(value) {
return (value || "").toString().trim();
}
function parseBoolean(value) {
if (typeof value === "boolean") return value;
const v = cleanText(value).toLowerCase();
return v === "true" || v === "1" || v === "si" || v === "sí";
}
function formatNumber(value) {
return new Intl.NumberFormat("es-BO").format(value || 0);
}
async function loadCsvFromCandidates(paths) {
let lastError = null;
for (const path of paths) {
try {
const rows = await d3.csv(path, d3.autoType);
if (rows && rows.length) {
return { rows, path };
}
} catch (error) {
lastError = error;
}
}
throw lastError || new Error("No se pudo cargar el CSV desde ninguna ruta candidata.");
}
function normalizeRow(row) {
const lat = Number(row.latitud);
const lon = Number(row.longitud);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
return null;
}
return {
...row,
latitud: lat,
longitud: lon,
estado: parseBoolean(row.estado),
departamento: cleanText(row.departamento),
entidad: cleanText(row.entidad),
tipoPAF: cleanText(row.tipoPAF),
direccion: cleanText(row.html_direccion),
telefono: cleanText(row.html_telefono),
horarios: cleanText(row.html_horarios_json)
};
}
function uniqueSorted(values) {
return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b, "es"));
}
function fillSelect(select, items, allLabel) {
const current = select.value;
select.innerHTML = "";
const optionAll = document.createElement("option");
optionAll.value = "";
optionAll.textContent = allLabel;
select.appendChild(optionAll);
for (const item of items) {
const option = document.createElement("option");
option.value = item;
option.textContent = item;
select.appendChild(option);
}
if ([...select.options].some((opt) => opt.value === current)) {
select.value = current;
}
}
function updateDependentFilters() {
const selectedDepartment = elements.departamento.value;
const fromDepartment = selectedDepartment
? allRows.filter((row) => row.departamento === selectedDepartment)
: allRows;
fillSelect(elements.entidad, uniqueSorted(fromDepartment.map((r) => r.entidad)), "Todas");
fillSelect(elements.tipo, uniqueSorted(fromDepartment.map((r) => r.tipoPAF)), "Todos");
}
function popupHtml(point, distanceKm) {
const distanceInfo = Number.isFinite(distanceKm)
? `<br><strong>Distancia aproximada:</strong> ${distanceKm.toFixed(2)} km`
: "";
return `
<strong>${point.entidad || "Entidad sin nombre"}</strong><br>
<strong>Tipo:</strong> ${point.desGrupo || point.tipoPAF || "N/D"}<br>
<strong>Departamento:</strong> ${point.departamento || "N/D"}<br>
<strong>Direccion:</strong> ${point.direccion || "N/D"}<br>
<strong>Telefono:</strong> ${point.telefono || "N/D"}
${distanceInfo}
`;
}
function drawPoints(rows, nearest) {
markersLayer.clearLayers();
for (const row of rows) {
const marker = L.circleMarker([row.latitud, row.longitud], {
radius: 5,
color: "#145ea8",
weight: 1,
fillColor: row.estado ? "#1f77d0" : "#a3aeb9",
fillOpacity: 0.75
});
const distance = nearest && nearest.codigoPAF === row.codigoPAF ? nearest.distanceKm : undefined;
marker.bindPopup(popupHtml(row, distance));
marker.addTo(markersLayer);
}
}
function haversineKm(lat1, lon1, lat2, lon2) {
const toRad = (value) => (value * Math.PI) / 180;
const R = 6371;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function getFilteredRows() {
const dpto = elements.departamento.value;
const entidad = elements.entidad.value;
const tipo = elements.tipo.value;
const texto = cleanText(elements.texto.value).toLowerCase();
const soloActivos = elements.soloActivos.checked;
return allRows.filter((row) => {
if (dpto && row.departamento !== dpto) return false;
if (entidad && row.entidad !== entidad) return false;
if (tipo && row.tipoPAF !== tipo) return false;
if (soloActivos && !row.estado) return false;
if (texto) {
const bag = `${row.entidad} ${row.direccion} ${row.telefono}`.toLowerCase();
if (!bag.includes(texto)) return false;
}
return true;
});
}
function fitMapToRows(rows) {
if (!rows.length) return;
const bounds = L.latLngBounds(rows.map((r) => [r.latitud, r.longitud]));
map.fitBounds(bounds.pad(0.08));
}
function updateStats(nearest) {
elements.statTotal.textContent = formatNumber(allRows.length);
elements.statFiltrados.textContent = formatNumber(filteredRows.length);
elements.statCercano.textContent = nearest
? `${nearest.distanceKm.toFixed(2)} km`
: "Sin busqueda";
}
function clearNearestVisual() {
nearestLayer.clearLayers();
elements.statCercano.textContent = "Sin busqueda";
elements.resultadoCercano.textContent = "Ingresa tu ubicacion y presiona \"Buscar mas cercano\".";
lastNearest = null;
}
function applyFilters(resetNearest = true) {
filteredRows = getFilteredRows();
drawPoints(filteredRows);
if (lastNearest && !resetNearest) {
const stillExists = filteredRows.some((row) => row.codigoPAF === lastNearest.codigoPAF);
if (!stillExists) {
clearNearestVisual();
}
}
updateStats(lastNearest);
if (resetNearest) {
clearNearestVisual();
}
}
function findNearestPoint(userLat, userLon) {
if (!filteredRows.length) return null;
let nearest = null;
for (const row of filteredRows) {
const distanceKm = haversineKm(userLat, userLon, row.latitud, row.longitud);
if (!nearest || distanceKm < nearest.distanceKm) {
nearest = { ...row, distanceKm };
}
}
return nearest;
}
function renderNearest(userLat, userLon) {
const nearest = findNearestPoint(userLat, userLon);
nearestLayer.clearLayers();
if (!nearest) {
elements.resultadoCercano.textContent = "No hay puntos que cumplan los filtros actuales.";
lastNearest = null;
updateStats();
return;
}
drawPoints(filteredRows, nearest);
const userMarker = L.circleMarker([userLat, userLon], {
radius: 7,
color: "#0b7a75",
weight: 2,
fillColor: "#16a39c",
fillOpacity: 0.9
}).bindPopup("Tu ubicacion");
const nearestMarker = L.circleMarker([nearest.latitud, nearest.longitud], {
radius: 8,
color: "#b02028",
weight: 2,
fillColor: "#de3e46",
fillOpacity: 0.9
}).bindPopup(popupHtml(nearest, nearest.distanceKm));
const line = L.polyline(
[
[userLat, userLon],
[nearest.latitud, nearest.longitud]
],
{ color: "#d96400", weight: 2, dashArray: "6, 6" }
);
nearestLayer.addLayer(userMarker);
nearestLayer.addLayer(nearestMarker);
nearestLayer.addLayer(line);
map.fitBounds(L.latLngBounds([[userLat, userLon], [nearest.latitud, nearest.longitud]]).pad(0.25));
elements.resultadoCercano.innerHTML = `
<strong>${nearest.entidad || "Entidad sin nombre"}</strong><br>
<strong>Tipo:</strong> ${nearest.desGrupo || nearest.tipoPAF || "N/D"}<br>
<strong>Departamento:</strong> ${nearest.departamento || "N/D"}<br>
<strong>Direccion:</strong> ${nearest.direccion || "N/D"}<br>
<strong>Telefono:</strong> ${nearest.telefono || "N/D"}<br>
<strong>Distancia aproximada:</strong> ${nearest.distanceKm.toFixed(2)} km
`;
lastNearest = nearest;
updateStats(nearest);
}
elements.resultadoCercano.textContent = "Cargando puntos...";
elements.estadoUbicacion.textContent = "Procesando dataset...";
let csvLoadResult;
try {
csvLoadResult = await loadCsvFromCandidates(dataPathCandidates);
} catch (error) {
elements.resultadoCercano.textContent = `Error al cargar CSV: ${error.message}`;
elements.estadoUbicacion.textContent = "Verifica la ruta del dataset en data/.";
return;
}
allRows = csvLoadResult.rows.map(normalizeRow).filter(Boolean);
fillSelect(elements.departamento, uniqueSorted(allRows.map((r) => r.departamento)), "Todos");
updateDependentFilters();
filteredRows = [...allRows];
drawPoints(filteredRows);
fitMapToRows(filteredRows);
updateStats();
elements.resultadoCercano.textContent = "Ingresa tu ubicacion y presiona \"Buscar mas cercano\".";
elements.estadoUbicacion.textContent = `${formatNumber(allRows.length)} puntos cargados desde ${csvLoadResult.path}.`;
elements.mapaBase.addEventListener("change", (event) => {
Object.values(tileLayers).forEach((layer) => {
if (map.hasLayer(layer)) map.removeLayer(layer);
});
tileLayers[event.target.value].addTo(map);
});
elements.departamento.addEventListener("change", () => {
updateDependentFilters();
applyFilters(true);
});
[elements.entidad, elements.tipo, elements.soloActivos].forEach((element) => {
element.addEventListener("change", () => applyFilters(true));
});
elements.texto.addEventListener("input", () => applyFilters(true));
elements.buscarCercanoBtn.addEventListener("click", () => {
const lat = Number(elements.latInput.value);
const lon = Number(elements.lonInput.value);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
elements.estadoUbicacion.textContent = "Ingresa una latitud y longitud validas.";
return;
}
elements.estadoUbicacion.textContent = "";
renderNearest(lat, lon);
});
elements.centrarFiltrosBtn.addEventListener("click", () => {
if (!filteredRows.length) {
elements.estadoUbicacion.textContent = "No hay puntos para centrar con los filtros actuales.";
return;
}
fitMapToRows(filteredRows);
elements.estadoUbicacion.textContent = "Mapa centrado en los puntos filtrados.";
});
elements.usarUbicacionBtn.addEventListener("click", () => {
if (!navigator.geolocation) {
elements.estadoUbicacion.textContent = "Tu navegador no soporta geolocalizacion.";
return;
}
elements.estadoUbicacion.textContent = "Obteniendo ubicacion...";
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
elements.latInput.value = lat.toFixed(6);
elements.lonInput.value = lon.toFixed(6);
elements.estadoUbicacion.textContent = "Ubicacion capturada.";
renderNearest(lat, lon);
},
(error) => {
elements.estadoUbicacion.textContent = `No se pudo obtener la ubicacion: ${error.message}`;
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
});
})();