Puntos de Atención ASFI - Bolivia
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
    }
  );
});

})();
Filtros
Mapa base
Punto más cercano