Guia Avançado de Mapas Interativos e Rastreamento em Tempo Real com Leaflet

Introdução ao Leaflet

O Leaflet é uma biblioteca JavaScript de código aberto, altamente otimizada e focada na compatibilidade com dispositivos móveis para a criação de mapas interativos. Com um tamanho minúsculo de aproximadamente 42 KB, ele entrega um conjunto robusto de funcionalidades exigidas pela maioria das aplicações de geolocalização. Sua arquitetura modular permite a integração de inúmeros plugins, garantindo alta performance tanto em navegadores de desktop quanto em plataformas móveis.

Configuração de Serviodr de Tiles Local

Para ambientes sem conexão com a internet ou para utilizar dados proprietários, é possível configurar o Leaflet para consumir tiles de um servidor local. A estrutura de URL deve seguir o padrão de coordenadas {z}/{x}/{y}.


const container = document.getElementById('mapa-base');
const mapaLocal = L.map(container).setView([-23.5505, -46.6333], 11);

L.tileLayer('http://localhost:8080/servidor-tiles/{z}/{x}/{y}.png', {
  minZoom: 5,
  maxZoom: 18,
  attribution: 'Dados Geográficos Locais'
}).addTo(mapaLocal);

Interação com Marcadroes: Eventos, Popups e Tooltips

Os marcadores no Leaflet podem ser enriquecidos com metadados e eventos personalizados. É importante distinguir entre Popups (janelas flutuantes que suportam HTML complexo e links, mas são exclusivas por vez) e Tooltips (rótulos leves que podem ser exibidos simultaneamente em múltiplos marcadores, mas com interatividade limitada).


const coordenadasSensor = [-23.5505, -46.6333];
const sensorMarcador = L.marker(coordenadasSensor).addTo(mapaLocal);

// Injeção de dados personalizados
sensorMarcador.dadosInternos = { idSensor: 'X-99', status: 'ativo' };

sensorMarcador.on('click', (evento) => {
  console.log('Informações do alvo:', evento.target.dadosInternos);
});

// Configuração de Popup com link
sensorMarcador.bindPopup(
  '<h4>Sensor X-99</h4><a href="https://exemplo.com/painel" target="_blank">Acessar Painel</a>'
).openPopup();

// Configuração de Tooltip permanente
sensorMarcador.bindTooltip('Operação Normal', { 
  permanent: true, 
  direction: 'top', 
  offset: [0, -15] 
});

Integração com React e TypeScript

Ao utilizar React, o ciclo de vida do mapa deve ser gerenciado dentro do useEffect. O exemplo abaixo demonstra a renderização de polígonos de risco sobrepostos ao mapa.


import React, { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

const MapaSetores: React.FC = () => {
  const referenciaMapa = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!referenciaMapa.current) return;

    const instanciaMapa = L.map(referenciaMapa.current).setView([-22.9068, -43.1729], 12);

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

    const verticesSetor = [
      [-22.91, -43.17], [-22.92, -43.18], [-22.915, -43.19]
    ];

    L.polygon(verticesSetor, {
      color: '#E91E63',
      fillColor: '#F48FB1',
      fillOpacity: 0.6,
      weight: 2
    }).addTo(instanciaMapa)
     .bindPopup('Área de Monitoramento');

    return () => {
      instanciaMapa.remove();
    };
  }, []);

  return <div ref={referenciaMapa} style={{ height: '100vh' }} />;
};

export default MapaSetores;

Simulação de Trajetórias Dinâmicas Múltiplas

Para rastreamento em tempo real, é necessário calcular o ângulo de direção (bearing) para rotacionar os ícones adequadamente. A classe abaixo encapsula a lógica de desenho de linhas animadas (usando o plugin leaflet-ant-path) e atualização de marcadores.


class GerenciadorDeRotas {
  static calcularDirecao(lat1, lon1, lat2, lon2) {
    const deltaLon = (lon2 - lon1) * (Math.PI / 180);
    const y = Math.sin(deltaLon) * Math.cos(lat2 * (Math.PI / 180));
    const x = Math.cos(lat1 * (Math.PI / 180)) * Math.sin(lat2 * (Math.PI / 180)) -
              Math.sin(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.cos(deltaLon);
    let angulo = Math.atan2(y, x) * (180 / Math.PI);
    return (angulo + 360) % 360;
  }

  atualizarRota(mapa, pontos, idCamada, estiloLinha, iconeVeiculo) {
    if (pontos.length === 0) return;

    // Limpeza da camada anterior
    if (mapa.hasLayer(this.camadas[idCamada])) {
      mapa.removeLayer(this.camadas[idCamada]);
    }
    this.camadas[idCamada] = L.layerGroup().addTo(mapa);

    L.polyline.antPath(pontos, {
      ...estiloLinha,
      delay: 8000,
      dashArray: [10, 20]
    }).addTo(this.camadas[idCamada]);

    if (pontos.length > 1) {
      const ultimoPonto = pontos[pontos.length - 1];
      const pontoAnterior = pontos[pontos.length - 2];
      
      L.marker(ultimoPonto, {
        icon: L.icon({ iconUrl: iconeVeiculo, iconSize: [32, 32] }),
        rotationAngle: GerenciadorDeRotas.calcularDirecao(
          pontoAnterior[0], pontoAnterior[1], ultimoPonto[0], ultimoPonto[1]
        )
      }).addTo(this.camadas[idCamada]);
    }
  }
}

Implementação de Rastreamento no Vue 3

No ecossistema Vue 3, a API Composition facilita a orquestração de intervalos de tempo para atualiazr as coordenadas do mapa dinamicamente.


<template>
  <div id="mapa-vue" class="container-mapa"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

onMounted(() => {
  const mapa = L.map('mapa-vue').setView([40.4168, -3.7038], 13);
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mapa);
  
  const historicoRota: [number, number][] = [[40.4168, -3.7038]];
  const camadaDinamica = L.layerGroup().addTo(mapa);

  const cronometro = setInterval(() => {
    const novaLat = historicoRota[historicoRota.length - 1][0] + (Math.random() * 0.005);
    const novaLon = historicoRota[historicoRota.length - 1][1] + (Math.random() * 0.005);
    historicoRota.push([novaLat, novaLon]);
    
    camadaDinamica.clearLayers();
    L.polyline(historicoRota, { color: '#4CAF50', weight: 4 }).addTo(camadaDinamica);
    L.circleMarker(historicoRota[historicoRota.length - 1], { radius: 6, color: '#F44336' }).addTo(camadaDinamica);
  }, 1000);

  onUnmounted(() => clearInterval(cronometro));
});
</script>

Reprodução e Controle de Trajetórias Gravadas

O plugin leaflet-trackplayer permite simular a reprodução de uma rota histórica, incluindo controles de velocidade e animações suaves de câmera.


const mapaPlayer = L.map('mapa-reproducao').setView([4.7110, -74.0721], 14);
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}').addTo(mapaPlayer);

const dadosRota = [
  { lat: 4.7110, lng: -74.0721 },
  { lat: 4.7125, lng: -74.0735 },
  { lat: 4.7140, lng: -74.0750 }
];

const reprodutor = new L.TrackPlayer(dadosRota, {
  markerIcon: L.icon({ iconUrl: 'ativo.svg', iconSize: [28, 28] }),
  speed: 300,
  weight: 5,
  passedLineColor: '#2196F3',
  notPassedLineColor: '#90CAF9',
  panTo: true,
  polylineDecoratorOptions: {
    patterns: [{
      offset: 0,
      repeat: 60,
      symbol: L.Symbol.arrowHead({ pixelSize: 7, pathOptions: { color: '#FFFFFF', weight: 1 } })
    }]
  },
  markerRotation: true
}).addTo(mapaPlayer);

// Controles de Interface
document.getElementById('btn-iniciar').onclick = () => reprodutor.start();
document.getElementById('btn-pausar').onclick = () => reprodutor.pause();
document.getElementById('btn-acelerar').onclick = () => reprodutor.setSpeed(reprodutor.getSpeed() * 2);

Renderização de Fronteiras Administrativas via GeoJSON

A API nativa fetch pode ser utilizada para carregar arquivos GeoJSON e renderizar polígonos regionais diretamente sobre a camada base.


const mapaGeoJson = L.map('mapa-regioes').setView([-12.0464, -77.0428], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mapaGeoJson);

fetch('./arquivos/limites-municipais.json')
  .then(resposta => resposta.json())
  .then(dadosGeo => {
    L.geoJSON(dadosGeo, {
      style: {
        color: '#673AB7',
        weight: 2,
        opacity: 0.8,
        fillColor: '#D1C4E9',
        fillOpacity: 0.4
      },
      onEachFeature: (feature, layer) => {
        if (feature.properties && feature.properties.nome) {
          layer.bindTooltip(feature.properties.nome);
        }
      }
    }).addTo(mapaGeoJson);
  });

Controle de Rotação de Ícones

Utilizando o plugin Leaflet.RotatedMarker, é possível definir manualmente o ângulo de orientação de qualquer marcador, ideal para representar a direção de veículos ou drones.


const mapaRotacao = L.map('mapa-drone').setView([-33.4489, -70.6693], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mapaRotacao);

L.marker([-33.4489, -70.6693], {
  rotationAngle: 135, // Ângulo em graus
  rotationOrigin: 'center center',
  icon: L.icon({
    iconUrl: 'drone-icone.png',
    iconSize: [45, 45],
    iconAnchor: [22, 22]
  })
}).addTo(mapaRotacao);

Tags: Leaflet javascript TypeScript React vue3

Publicado em 8-26 16:54