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);