Skip to main content

Overview

Tesis Rutas uses Leaflet and React Leaflet to provide interactive maps for displaying tourist destinations (POIs) and routes. This guide covers setup, displaying markers, and route visualization.

Prerequisites

  • React 19.2+
  • Leaflet 1.9.4+
  • React Leaflet 5.0+
  • Node.js environment

Installation

The required packages are already configured in the project:
package.json
{
  "dependencies": {
    "leaflet": "^1.9.4",
    "react-leaflet": "^5.0.0",
    "@turf/turf": "^7.3.2"
  }
}
Install dependencies:
npm install leaflet react-leaflet @turf/turf

Basic Map Setup

1

Import Leaflet CSS

Always import Leaflet styles before using map components:
import "leaflet/dist/leaflet.css";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import L from "leaflet";
2

Fix Default Marker Icons

Leaflet icons require a fix when using Vite/Webpack bundlers:
src/components/Mapa/MapaBase.jsx
import L from "leaflet";

/* Fix iconos Leaflet (Vite) */
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl:
    "https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png",
  iconUrl:
    "https://unpkg.com/[email protected]/dist/images/marker-icon.png",
  shadowUrl:
    "https://unpkg.com/[email protected]/dist/images/marker-shadow.png",
});
This fix ensures marker icons load correctly in production builds.
3

Create Base Map Component

src/components/Mapa/MapaBase.jsx
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";

export default function MapaBase({ rutas }) {
  // Centro inicial UCE (hardcode SOLO para init)
  const center = [-0.201, -78.502];

  // Aplanamos POIs de todas las rutas
  const pois = rutas.flatMap((ruta) => ruta.pois);

  return (
    <MapContainer
      center={center}
      zoom={17}
      className="h-full w-full"
    >
      <TileLayer
        attribution='&copy; OpenStreetMap contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />

      {pois.map((poi) => (
        <Marker
          key={poi.id}
          position={[poi.lat, poi.lng]}
        >
          <Popup>
            <strong>{poi.nombre}</strong>
            <br />
            {poi.ubicacion}
          </Popup>
        </Marker>
      ))}
    </MapContainer>
  );
}

Displaying POIs (Points of Interest)

Single POI Map

Display a single destination on a map with conditional rendering:
src/components/Mapa/MapaPOI.jsx
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

export default function MapaPOI({ nombre, latitud, longitud }) {
  // Validate coordinates
  if (!latitud || !longitud) {
    return (
      <div className="h-[300px] flex items-center justify-center text-sm text-muted-foreground bg-muted rounded-lg">
        No hay coordenadas para mostrar.
      </div>
    );
  }

  const position = [latitud, longitud];

  return (
    <div className="h-[300px] w-full rounded-lg overflow-hidden">
      <MapContainer
        center={position}
        zoom={16}
        scrollWheelZoom={false}
        className="h-full w-full"
      >
        <TileLayer
          attribution="&copy; OpenStreetMap contributors"
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <Marker position={position}>
          <Popup>{nombre}</Popup>
        </Marker>
      </MapContainer>
    </div>
  );
}
The scrollWheelZoom={false} property prevents accidental zooming when scrolling the page.

Multiple POIs Map

Display all destinations from context:
src/features/mapa/pages/MapaPage.jsx
import { useEffect, useMemo } from "react";
import { useAuth } from "../../../context/AuthContext";
import MapView from "../../../components/Mapa/MapView";
import { useMapaDestinos } from "../hooks/useMapaDestinos";
import { useDestinosContext } from "../../../context/DestinosContext/useDestinosContext";
import { useRutas } from "../../../hooks/useRutas/useRutas";

export default function MapaPage() {
  const { user } = useAuth();

  const { destinos, loading: loadingDestinos, error: errorDestinos } = useDestinosContext();
  const { pois } = useMapaDestinos(destinos);

  const { rutas, fetchRutas, loading: loadingRutas, error: errorRutas } = useRutas();

  useEffect(() => {
    fetchRutas();
  }, []);

  const routes = useMemo(() => {
    return (rutas || [])
      .map((r) => {
        const path = (r.puntos || [])
          .map((p) => p.position)
          .filter((pos) => Array.isArray(pos) && pos.length === 2);
        if (path.length < 2) return null;
        return { id: r.id, nombre: r.nombre, categoria: r.categoria ?? null, path };
      })
      .filter(Boolean);
  }, [rutas]);

  if (loadingDestinos || loadingRutas) return <p className="p-4">Cargando mapa...</p>;

  if (errorDestinos || errorRutas) {
    return <p className="p-4 text-red-500">Error al cargar información del mapa</p>;
  }

  return (
    <div className="p-4 flex flex-col h-[calc(100vh-120px)]">
      <header className="mb-4 flex items-center justify-between">
        <h1 className="text-2xl font-semibold">Mapa Turístico</h1>

        {user?.rol === "administrador" && (
          <span className="text-sm text-gray-500">Modo administrador</span>
        )}
      </header>

      <div className="flex-1 rounded-lg overflow-hidden border">
        <MapView pois={pois} routes={routes} showRoutes={false} />
      </div>
    </div>
  );
}

Route Visualization

Route Data Structure

Routes are represented as arrays of coordinate pairs:
const routes = [
  {
    id: "route_1",
    nombre: "Ruta Histórica",
    categoria: "cultural",
    path: [
      [-0.201, -78.502],  // [lat, lng]
      [-0.202, -78.503],
      [-0.203, -78.504]
    ]
  }
];

Processing Routes from API

const routes = useMemo(() => {
  return (rutas || [])
    .map((r) => {
      // Extract positions from route points
      const path = (r.puntos || [])
        .map((p) => p.position)
        .filter((pos) => Array.isArray(pos) && pos.length === 2);
      
      // Skip routes with less than 2 points
      if (path.length < 2) return null;
      
      return {
        id: r.id,
        nombre: r.nombre,
        categoria: r.categoria ?? null,
        path
      };
    })
    .filter(Boolean);  // Remove null entries
}, [rutas]);

Drawing Routes with Polyline

import { Polyline } from "react-leaflet";

function RouteLayer({ routes }) {
  return (
    <>
      {routes.map((route) => (
        <Polyline
          key={route.id}
          positions={route.path}
          color="blue"
          weight={4}
          opacity={0.7}
        >
          <Popup>
            <strong>{route.nombre}</strong>
            <br />
            Categoría: {route.categoria}
          </Popup>
        </Polyline>
      ))}
    </>
  );
}

Map Configuration Options

MapContainer Props

PropertyTypeDescriptionExample
center[lat, lng]Initial map center[-0.201, -78.502]
zoomnumberInitial zoom level (1-18)16
scrollWheelZoombooleanEnable scroll zoomfalse
classNamestringCSS classes"h-full w-full"

TileLayer Configuration

The platform uses OpenStreetMap tiles:
<TileLayer
  attribution='&copy; OpenStreetMap contributors'
  url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
You can use different map styles by changing the tile URL:
// Satellite imagery (requires API key)
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"

// Dark mode
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"

// Terrain
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"

Custom Markers

Create custom marker icons for different POI types:
import L from "leaflet";

const customIcon = L.icon({
  iconUrl: '/marker-custom.png',
  iconSize: [32, 32],
  iconAnchor: [16, 32],
  popupAnchor: [0, -32]
});

<Marker position={position} icon={customIcon}>
  <Popup>{nombre}</Popup>
</Marker>

Coordinate Formats

Always use [latitude, longitude] format. This is opposite to many other mapping libraries that use [lng, lat].
Correct format:
const position = [-0.201, -78.502];  // [lat, lng]
Incorrect format:
const position = [-78.502, -0.201];  // ❌ Wrong!

Performance Optimization

Use useMemo for Route Processing

const routes = useMemo(() => {
  return processRoutes(rutas);
}, [rutas]);

Limit Visible Markers

For maps with many POIs, only show markers within viewport:
import { useMap } from "react-leaflet";

function FilteredMarkers({ pois }) {
  const map = useMap();
  const bounds = map.getBounds();
  
  const visiblePois = pois.filter(poi => 
    bounds.contains([poi.lat, poi.lng])
  );
  
  return visiblePois.map(poi => (
    <Marker key={poi.id} position={[poi.lat, poi.lng]} />
  ));
}

Common Issues

Ensure you’ve imported the Leaflet CSS:
import "leaflet/dist/leaflet.css";
And set a height on your map container:
<div className="h-[400px]">
  <MapContainer ...>
</div>
Apply the icon fix at the top of your component:
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: "https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png",
  iconUrl: "https://unpkg.com/[email protected]/dist/images/marker-icon.png",
  shadowUrl: "https://unpkg.com/[email protected]/dist/images/marker-shadow.png",
});
Verify your path array has at least 2 coordinate pairs:
if (path.length < 2) return null;
And coordinates are in [lat, lng] format, not [lng, lat].

Next Steps

API Reference

Fetch destinations for map display

Authentication

Protect map routes with JWT

Build docs developers (and LLMs) love