🗺️ Leaflet.js - Mapas Interativos com JavaScript

O que é Leaflet.js?

Leaflet = biblioteca JavaScript para criar mapas interativos na web.

Folium (Python)  → Gera HTML com Leaflet
Leaflet.js       → Controle total com JavaScript

Vantagens: - Leve e rápido - Open source - Funciona em mobile - Altamente customizável

Site oficial: https://leafletjs.com


🚀 Setup Básico

HTML com Leaflet

Crie mapa_leaflet.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mapa Leaflet - LAFIC</title>

    <!-- Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />

    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
        }

        #map {
            height: 600px;
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="map"></div>

    <!-- Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

    <script>
        // Criar mapa centrado em Florianópolis
        const map = L.map('map').setView([-27.5969, -48.5495], 12);

        // Adicionar camada de tiles (OpenStreetMap)
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap contributors'
        }).addTo(map);

        // Adicionar marcador
        L.marker([-27.5969, -48.5495])
            .addTo(map)
            .bindPopup('UFSC - Florianópolis')
            .openPopup();
    </script>
</body>
</html>

Abra no navegador! Você verá um mapa interativo! 🗺️


📍 Marcadores (Markers)

Marcador Básico

// Criar marcador
const marcador = L.marker([-27.5969, -48.5495]).addTo(map);

// Com popup
marcador.bindPopup('UFSC');

// Com tooltip (aparece ao passar mouse)
marcador.bindTooltip('Campus Trindade');

Múltiplos Marcadores

const coletas = [
    { lat: -27.4374, lon: -48.3923, nome: "Ingleses Norte", especie: "Ulva lactuca" },
    { lat: -27.4450, lon: -48.3950, nome: "Ingleses Sul", especie: "Gracilaria" },
    { lat: -27.5750, lon: -48.4200, nome: "Barra da Lagoa", especie: "Sargassum" }
];

coletas.forEach(coleta => {
    L.marker([coleta.lat, coleta.lon])
        .addTo(map)
        .bindPopup(`<b>${coleta.nome}</b><br>${coleta.especie}`);
});

Ícones Personalizados

// Criar ícone customizado
const iconeVerde = L.icon({
    iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
    iconSize: [25, 41],
    iconAnchor: [12, 41],
    popupAnchor: [1, -34]
});

// Usar ícone
L.marker([-27.4374, -48.3923], { icon: iconeVerde })
    .addTo(map)
    .bindPopup('Ulva lactuca');

⭕ Círculos e Formas

Circle Markers

// Círculo proporcional a valor
const coletas = [
    { lat: -27.4374, lon: -48.3923, prof: 5.2 },
    { lat: -27.4450, lon: -48.3950, prof: 12.8 },
    { lat: -27.5750, lon: -48.4200, prof: 3.1 }
];

coletas.forEach(c => {
    L.circleMarker([c.lat, c.lon], {
        radius: c.prof * 2,  // Proporcional à profundidade
        color: 'blue',
        fillColor: '#30a3dc',
        fillOpacity: 0.6
    })
    .addTo(map)
    .bindPopup(`Profundidade: ${c.prof}m`);
});

Polígonos

// Área de estudo (polígono)
const areaEstudo = [
    [-27.55, -48.55],
    [-27.55, -48.50],
    [-27.60, -48.50],
    [-27.60, -48.55]
];

L.polygon(areaEstudo, {
    color: 'red',
    fillColor: '#ff0000',
    fillOpacity: 0.2
})
.addTo(map)
.bindPopup('Área de Proteção Marinha');

🎨 Popups HTML Avançados

const coleta = {
    id: 1,
    local: "Ingleses Norte",
    especie: "Ulva lactuca",
    prof: 5.2,
    temp: 22.5,
    sal: 35.0
};

const popupHTML = `
    <div style="font-family: Arial; min-width: 200px;">
        <h3 style="color: #2E86AB; margin-top: 0;">
            📍 ${coleta.local}
        </h3>
        <hr style="margin: 10px 0;">
        <table style="width: 100%; font-size: 14px;">
            <tr>
                <td><strong>ID:</strong></td>
                <td>${coleta.id}</td>
            </tr>
            <tr>
                <td><strong>Espécie:</strong></td>
                <td><em>${coleta.especie}</em></td>
            </tr>
            <tr>
                <td><strong>Profundidade:</strong></td>
                <td>${coleta.prof}m</td>
            </tr>
            <tr>
                <td><strong>Temperatura:</strong></td>
                <td>${coleta.temp}°C</td>
            </tr>
            <tr>
                <td><strong>Salinidade:</strong></td>
                <td>${coleta.sal} PSU</td>
            </tr>
        </table>
        <hr style="margin: 10px 0;">
        <button onclick="verDetalhes(${coleta.id})" 
                style="width: 100%; padding: 8px; background: #2E86AB; color: white; border: none; cursor: pointer; border-radius: 4px;">
            Ver Detalhes
        </button>
    </div>
`;

L.marker([coleta.lat, coleta.lon])
    .addTo(map)
    .bindPopup(popupHTML);

🗂️ Controle de Camadas

// Criar grupos de camadas
const grupoUlva = L.layerGroup();
const grupoGracilaria = L.layerGroup();
const grupoSargassum = L.layerGroup();

// Adicionar marcadores aos grupos
L.marker([-27.4374, -48.3923])
    .bindPopup('Ulva lactuca')
    .addTo(grupoUlva);

L.marker([-27.4450, -48.3950])
    .bindPopup('Gracilaria')
    .addTo(grupoGracilaria);

// Criar controle de camadas
const overlays = {
    "Ulva lactuca": grupoUlva,
    "Gracilaria": grupoGracilaria,
    "Sargassum": grupoSargassum
};

L.control.layers(null, overlays).addTo(map);

// Adicionar grupos ao mapa por padrão
grupoUlva.addTo(map);
grupoGracilaria.addTo(map);

🎯 Exemplo Completo: Dashboard Interativo

Crie dashboard_completo.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard LAFIC</title>

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />

    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        header {
            background: linear-gradient(135deg, #2E86AB 0%, #1a5c7a 100%);
            color: white;
            padding: 20px;
            text-align: center;
        }

        .container {
            display: flex;
            height: calc(100vh - 80px);
        }

        .sidebar {
            width: 300px;
            background: #f8f9fa;
            padding: 20px;
            overflow-y: auto;
            border-right: 1px solid #dee2e6;
        }

        #map {
            flex: 1;
        }

        .stat-card {
            background: white;
            padding: 15px;
            margin-bottom: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }

        .stat-card h3 {
            color: #2E86AB;
            font-size: 14px;
            margin-bottom: 10px;
        }

        .stat-number {
            font-size: 32px;
            font-weight: bold;
            color: #333;
        }

        .species-list {
            list-style: none;
            margin-top: 10px;
        }

        .species-list li {
            padding: 8px;
            margin-bottom: 5px;
            background: white;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.3s;
        }

        .species-list li:hover {
            background: #e9ecef;
        }

        .badge {
            display: inline-block;
            padding: 3px 8px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: bold;
        }

        .badge-green { background: #28a745; color: white; }
        .badge-red { background: #dc3545; color: white; }
        .badge-blue { background: #007bff; color: white; }
        .badge-yellow { background: #ffc107; color: white; }
    </style>
</head>
<body>
    <header>
        <h1>🌊 Dashboard de Coletas - LAFIC/UFSC</h1>
        <p>Monitoramento de Macroalgas na Costa de Santa Catarina</p>
    </header>

    <div class="container">
        <div class="sidebar">
            <div class="stat-card">
                <h3>📊 ESTATÍSTICAS GERAIS</h3>
                <div style="text-align: center;">
                    <div class="stat-number" id="totalColetas">0</div>
                    <div>Total de Coletas</div>
                </div>
            </div>

            <div class="stat-card">
                <h3>🌿 ESPÉCIES</h3>
                <ul class="species-list" id="especiesList"></ul>
            </div>

            <div class="stat-card">
                <h3>📈 MÉDIAS</h3>
                <div id="medias"></div>
            </div>
        </div>

        <div id="map"></div>
    </div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

    <script>
        // Dados de coleta
        const coletas = [
            { id: 1, lat: -27.4374, lon: -48.3923, local: "Ingleses N", especie: "Ulva lactuca", prof: 5.2, temp: 22.5, sal: 35.0 },
            { id: 2, lat: -27.4450, lon: -48.3950, local: "Ingleses S", especie: "Gracilaria", prof: 7.8, temp: 23.1, sal: 34.8 },
            { id: 3, lat: -27.5750, lon: -48.4200, local: "Barra Lagoa", especie: "Sargassum", prof: 3.1, temp: 22.8, sal: 35.1 },
            { id: 4, lat: -28.4833, lon: -48.7833, local: "Laguna", especie: "Laminaria", prof: 10.5, temp: 21.2, sal: 35.0 },
            { id: 5, lat: -28.0200, lon: -48.6200, local: "Garopaba", especie: "Gracilaria", prof: 6.0, temp: 22.0, sal: 34.9 },
            { id: 6, lat: -27.7500, lon: -48.5100, local: "Armação", especie: "Ulva lactuca", prof: 4.5, temp: 23.5, sal: 35.1 }
        ];

        // Cores por espécie
        const coresEspecies = {
            "Ulva lactuca": "#28a745",
            "Gracilaria": "#dc3545",
            "Sargassum": "#007bff",
            "Laminaria": "#6f42c1"
        };

        // Criar mapa
        const map = L.map('map').setView([-27.8, -48.5], 9);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap'
        }).addTo(map);

        // Adicionar marcadores
        coletas.forEach(coleta => {
            const cor = coresEspecies[coleta.especie];
            const valida = (coleta.temp >= 20 && coleta.temp <= 25 && coleta.prof < 10);

            // Criar ícone SVG personalizado
            const iconeHTML = `
                <div style="background-color: ${cor}; width: 30px; height: 30px; 
                            border-radius: 50%; border: 3px solid white; 
                            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
                            display: flex; align-items: center; justify-content: center;
                            color: white; font-weight: bold; font-size: 12px;">
                    ${coleta.id}
                </div>
            `;

            const icone = L.divIcon({
                html: iconeHTML,
                iconSize: [30, 30],
                className: ''
            });

            // Popup
            const popupHTML = `
                <div style="font-family: Arial; min-width: 220px;">
                    <h3 style="color: ${cor}; margin: 0 0 10px 0;">
                        📍 ${coleta.local}
                    </h3>
                    <hr style="margin: 10px 0; border: none; border-top: 1px solid #ddd;">
                    <table style="width: 100%; font-size: 13px;">
                        <tr><td><strong>ID:</strong></td><td>#${coleta.id}</td></tr>
                        <tr><td><strong>Espécie:</strong></td><td><em>${coleta.especie}</em></td></tr>
                        <tr><td><strong>Profundidade:</strong></td><td>${coleta.prof}m</td></tr>
                        <tr><td><strong>Temperatura:</strong></td><td>${coleta.temp}°C</td></tr>
                        <tr><td><strong>Salinidade:</strong></td><td>${coleta.sal} PSU</td></tr>
                    </table>
                    <hr style="margin: 10px 0; border: none; border-top: 1px solid #ddd;">
                    <div style="text-align: center; padding: 8px; background: ${valida ? '#d4edda' : '#fff3cd'}; border-radius: 4px;">
                        <strong>${valida ? '✅ VÁLIDA' : '⚠️ ATENÇÃO'}</strong>
                    </div>
                </div>
            `;

            L.marker([coleta.lat, coleta.lon], { icon: icone })
                .addTo(map)
                .bindPopup(popupHTML);
        });

        // Atualizar sidebar
        function atualizarEstatisticas() {
            // Total
            document.getElementById('totalColetas').textContent = coletas.length;

            // Espécies
            const especiesCount = {};
            coletas.forEach(c => {
                especiesCount[c.especie] = (especiesCount[c.especie] || 0) + 1;
            });

            const especiesList = document.getElementById('especiesList');
            especiesList.innerHTML = '';
            Object.entries(especiesCount).forEach(([especie, count]) => {
                const cor = coresEspecies[especie];
                const li = document.createElement('li');
                li.innerHTML = `
                    <span class="badge" style="background: ${cor};">${count}</span>
                    <em style="margin-left: 10px;">${especie}</em>
                `;
                especiesList.appendChild(li);
            });

            // Médias
            const tempMedia = (coletas.reduce((sum, c) => sum + c.temp, 0) / coletas.length).toFixed(1);
            const profMedia = (coletas.reduce((sum, c) => sum + c.prof, 0) / coletas.length).toFixed(1);
            const salMedia = (coletas.reduce((sum, c) => sum + c.sal, 0) / coletas.length).toFixed(1);

            document.getElementById('medias').innerHTML = `
                <p><strong>Temperatura:</strong> ${tempMedia}°C</p>
                <p><strong>Profundidade:</strong> ${profMedia}m</p>
                <p><strong>Salinidade:</strong> ${salMedia} PSU</p>
            `;
        }

        atualizarEstatisticas();
    </script>
</body>
</html>

Abra no navegador! Dashboard completo e interativo! 📊🗺️


🎓 Integrar com GeoJSON

// Carregar GeoJSON
fetch('coletas.geojson')
    .then(response => response.json())
    .then(data => {
        L.geoJSON(data, {
            onEachFeature: function(feature, layer) {
                const props = feature.properties;
                layer.bindPopup(`
                    <h3>${props.nome}</h3>
                    <p><strong>Espécie:</strong> ${props.especie}</p>
                `);
            }
        }).addTo(map);
    });

🎓 Checklist desta Lição

Se marcou tudo, você completou Visualização Web! 🎉


➡️ Próximos Passos

Parabéns! Você domina desenvolvimento web para visualização de dados! 🌐

Próximo módulo: - 4-Casos-Praticos (Projetos completos de Oceanografia)


📝 Resumo de Funções Leaflet

Função Uso
L.map() Criar mapa
L.tileLayer() Adicionar camada base
L.marker() Adicionar marcador
L.circleMarker() Círculo no mapa
L.polygon() Polígono
L.layerGroup() Agrupar camadas
L.control.layers() Controle de camadas

Você é um desenvolvedor web completo! 🌐✨

Pronto para criar dashboards profissionais para pesquisa! 🚀