Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/davidmpizarro/QuipuEco-Hackaton/llms.txt

Use this file to discover all available pages before exploring further.

The VistaMapaPuntos component is a full-screen, dark-themed Mapbox GL JS map that shows Lima residents exactly where to drop off their recyclables based on the material type identified by the AI classifier. Rather than showing every collection point at once, the map filters to the network most relevant to the classified waste — Tambo convenience stores for plastic, paper, and metal; Centro Verde municipal facilities for organic, glass, electronic, peligroso, and general waste. Once the user’s location is known via the browser GeolocateControl, all points are sorted by Haversine distance and a scrollable card tray appears at the bottom. Tapping any card calls the Mapbox Directions API to draw a walking route and display a toast with walking time, driving time, and distance in kilometres.

Map Configuration

VistaMapaPuntos initialises a mapboxgl.Map instance using a CARTO dark raster tile layer instead of a Mapbox style URL. This gives the map its distinctive dark aesthetic while keeping the Mapbox token usage minimal (no vector tile consumption for the base layer).
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_TOKEN;

const map = new mapboxgl.Map({
  container: mapContainer.current,
  style: {
    version: 8,
    sources: {
      "carto-dark": {
        type: "raster",
        tiles: ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png"],
        tileSize: 256,
        attribution: "© CARTO, © OpenStreetMap contributors",
      },
    },
    layers: [{ id: "carto-dark-layer", type: "raster", source: "carto-dark" }],
  },
  center: [-77.03, -12.1],   // Lima
  zoom: 12,
});
Two standard controls are added after map load:
  • mapboxgl.GeolocateControl — triggers automatically on load (geolocate.trigger()), tracks the user’s heading, uses high-accuracy positioning
  • mapboxgl.NavigationControl — zoom in/out and compass
The map requires a valid Mapbox public token in the VITE_MAPBOX_TOKEN environment variable. Without it, the map canvas will fail to load and the component will render blank. See the Mapbox configuration guide for setup instructions.

Recycling Networks

QuipuEco maintains two distinct collection networks. The split is intentional and operationally accurate: Tambo stores accept only clean, dry materials that can be handled at a retail counter, while Centro Verde facilities have the infrastructure for composting, RAEE processing, and certified hazardous-waste management.

🏪 Tambo

Tambo convenience store chain. Accepts plástico, papel, and metal directly at the counter. 6 locations across Lima Este — no appointment required.

♻️ Centro Verde Municipal

Lima municipal recycling centres. Accepts orgánico, vidrio, electrónico, peligroso, and general waste. 5 locations per category across Lima Este.

Tambo Lima Este Locations

All six Tambo locations currently configured in VistaMapaPuntos.jsx via TAMBO_REAL_LIMA_ESTE:
NameDistrictAddressCoordinates
Tambo Corregidor - La MolinaLa MolinaAlameda del Corregidor 1684-12.0815, -76.9398
Tambo Alondras - Santa AnitaSanta AnitaAv. Las Alondras 297-12.0445, -76.9678
Tambo Paracas - AteAteParacas 892-12.0398, -76.9156
Tambo Ayllón - ChaclacayoChaclacayoAv. Nicolás Ayllón 421-11.9821, -76.7701
Tambo Riva Agüero - El AgustinoEl AgustinoAv. Riva Agüero 684-12.0478, -76.9967
Tambo 28 de Julio - ChosicaChosicaProlongación 28 de Julio 384-11.9356, -76.6945

Centro Verde Category Coverage

CategoryNetworkExample location
organicoCentro VerdeCentro Verde Eucaliptos — Santa Anita
vidrioCentro VerdeCentro Verde Separadora Industrial — Ate
electronicoCentro VerdeCentro Verde Javier Prado — La Molina
peligrosoCentro VerdeCentro Verde Separadora Industrial — Ate
generalCentro VerdeCentro Verde Vitarte — Ate

Category Detection

The detectarCategoria() function determines which set of collection points to display. It follows a strict priority order so that the most reliable signal always wins:
function detectarCategoria(resultado, filtroForzado) {
  // 1. Backend-computed category from the chat agent (most reliable)
  if (filtroForzado && CATEGORIAS_VALIDAS.has(filtroForzado)) {
    return filtroForzado;
  }

  // 2. tipo field from the classification JSON
  if (!resultado) return "general";
  const tipo = resultado.tipo?.toLowerCase();
  if (tipo && PUNTOS_LIMA[tipo]) return tipo;

  // 3. Text heuristics on the item name (fallback)
  const texto = (resultado.nombre || "").toLowerCase();
  if (texto.includes("orgán") || texto.includes("vegetal") || texto.includes("aliment")) return "organico";
  if (texto.includes("plást") || texto.includes("plastic") || texto.includes("pet") || texto.includes("botell")) return "plastico";
  if (texto.includes("papel") || texto.includes("cartón") || texto.includes("carton"))  return "papel";
  if (texto.includes("vidrio") || texto.includes("glass"))                               return "vidrio";
  if (texto.includes("metal") || texto.includes("lata") || texto.includes("alumin"))    return "metal";
  if (texto.includes("electrón") || texto.includes("electron") || texto.includes("pila") || texto.includes("bater")) return "electronico";
  if (texto.includes("peligro") || texto.includes("tóxico"))                            return "peligroso";
  return "general";
}
1

filtroForzado (highest priority)

Set when the chat agent returns accion: "mapa" with an accion_data string (e.g. "electronico"). This value is computed by the Python backend from contexto_residuo.tipo, not inferred by the LLM from free text — making it the most accurate signal.
2

resultado.tipo

The tipo field from the /clasificar JSON response. Used when the map is opened directly from ClassificationResult via the “Puntos de acopio en Lima” button.
3

Name heuristics

If resultado.tipo is missing or unrecognised, keyword matching on resultado.nombre is used as a last resort. Falls back to "general" if no keyword matches.

Map Markers

Marker appearance differs by network to make Tambo stores instantly recognisable:

Tambo Markers

White rounded-square (border-radius: 10px) with the Tambo logo image (/images/tambo2.png) fitted inside. Border colour matches the category accent colour. No rotation applied.

Centro Verde Markers

Teardrop shape (border-radius: 50% 50% 50% 0) rotated −45°, filled with the category accent colour, with a centred emoji rotated back to upright (+45°). A coloured box-shadow glow reinforces the category colour.
Each marker has a mapboxgl.Popup that shows the location name, address, district badge, and network badge on click.

Points Card Tray

Once GeolocateControl fires its geolocate event, the component:
  1. Computes Haversine distances from the user’s position to every point in the active category
  2. Sorts the array ascending by distance
  3. Updates puntosOrdenados state, triggering a re-render of the bottom card tray
const ordenados = [...puntos]
  .map((p) => ({
    ...p,
    distancia: calcularDistancia(latitude, longitude, p.lat, p.lng),
  }))
  .sort((a, b) => parseFloat(a.distancia) - parseFloat(b.distancia));
The tray is a horizontal scrollable row of w-48 h-32 cards. Each card shows:
  • Rank number badge (coloured with the category accent)
  • Location name and address
  • Distance badge (e.g. 1.2 km or 850 m)
  • District and network labels
  • A “Ruta” button that calls trazarRuta(punto)

Route Calculation

Tapping any card calls trazarRuta(), which queries the Mapbox Directions API for a walking route:
const url =
  `https://api.mapbox.com/directions/v5/mapbox/walking/` +
  `${userPos.lng},${userPos.lat};${punto.lng},${punto.lat}` +
  `?geometries=geojson&access_token=${mapboxgl.accessToken}`;
The GeoJSON route geometry is added to the map as a line layer styled with the category accent colour at line-width: 4 and line-opacity: 0.9. The map viewport is fitted to the route bounds with 80 px padding. A toast card appears at the bottom-right of the screen showing:
🚗 3 min · 🚶 18 min · 1.2 km
  • Walking timeMath.round(route.duration / 60) minutes
  • Driving timeMath.max(1, Math.round(walkingMinutes / 5)) minutes
  • Distance(route.distance / 1000).toFixed(1) km

Triggering the Map from the Voice Agent

When the voice agent returns accion: "mapa", App.jsx calls handleVerMapa() with two possible puntoDestino forms:
// Agent returned accion_data: "electronico"
// Map opens filtered to Centro Verde electronic points.
// No auto-route — user picks from the card tray.
handleVerMapa(resultado, "electronico");
The VistaMapaPuntos component reads puntoDestino from props:
const filtroForzado  = typeof puntoDestino === "string" ? puntoDestino : null;
const destinoEspecifico = puntoDestino && typeof puntoDestino === "object"
  ? puntoDestino
  : null;
If destinoEspecifico is set, a useEffect watches for userPos and puntosOrdenados to be populated, then calls trazarRuta() automatically on the best-matching point name.
The map can also be opened from the bottom navigation bar (“Mapa” tab) without a prior classification. In that case resultadoMapa is null and a prompt screen is shown asking the user to classify a waste item first. If a classification result already exists, the map opens directly filtered to that result’s category.

Build docs developers (and LLMs) love