Leaflet.js é a principal biblioteca JavaScript open-source para mapas interativos otimizados para dispositivos móveis. Ao contrário do Google Maps ou do Mapbox, o Leaflet é leve (~42KB em gzip), funciona com qualquer fornecedor de tiles e oferece controlo total sobre o estilo e o comportamento. Este guia aborda a criação de aplicações de cartografia prontas para produção, na perspetiva de um programador sénior.
Porquê Leaflet
O Leaflet oferece vantagens convincentes:
- Leve: Apenas 42KB, carrega rapidamente em qualquer lugar
- Agnóstico quanto ao fornecedor: Funciona com OpenStreetMap, Mapbox, tiles personalizados
- Mobile-first: Compatível com toque, responsivo por defeito
- Extensível: Ecossistema rico de plugins para qualquer funcionalidade
- Gratuito: Não são necessárias chaves de API para utilização básica
Primeiros passos
Instalação
<!--CSS-->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<!--JavaScript-->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<!--Contentor-->
<div id="map" style="height: 500px;"></div>
Ou com npm:
npm install leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
Mapa básico
// Inicializar o mapa centrado nas coordenadas
const map = L.map('map').setView([51.505, -0.09], 13);
// Adicionar tiles do OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Adicionar um marcador
L.marker([51.5, -0.09])
.addTo(map)
.bindPopup('Hello from London!')
.openPopup();
Fornecedores de tiles
OpenStreetMap (gratuito)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
Mapbox (estilo personalizado)
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: '© Mapbox © OpenStreetMap',
id: 'mapbox/streets-v11',
accessToken: 'your.mapbox.access.token'
}).addTo(map);
Imagens de satélite
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri, Maxar, Earthstar Geographics'
}).addTo(map);
Alternância de camadas
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri'
});
const map = L.map('map', {
center: [51.505, -0.09],
zoom: 13,
layers: [osm] // Camada por defeito
});
L.control.layers({
'Street Map': osm,
'Satellite': satellite
}).addTo(map);
Marcadores e popups
Marcadores básicos
// Marcador simples
const marker = L.marker([51.5, -0.09]).addTo(map);
// Com popup
marker.bindPopup('<b>Location</b><br>Description here');
// Abrir o popup imediatamente
marker.openPopup();
// Com tooltip (mostra ao passar o rato)
marker.bindTooltip('Quick info');
Ícones personalizados
const customIcon = L.icon({
iconUrl: '/images/marker.png',
iconSize: [32, 32],
iconAnchor: [16, 32], // Ponto que corresponde à posição do marcador
popupAnchor: [0, -32] // Ponto onde o popup abre relativamente ao iconAnchor
});
L.marker([51.5, -0.09], { icon: customIcon }).addTo(map);
Clusters de marcadores
Para muitos marcadores, utilize o plugin MarkerCluster:
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
const markers = L.markerClusterGroup();
locations.forEach(loc => {
const marker = L.marker([loc.lat, loc.lng])
.bindPopup(loc.name);
markers.addLayer(marker);
});
map.addLayer(markers);
Desenhar formas
Círculos e polígonos
// Círculo (raio em metros)
L.circle([51.508, -0.11], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 500
}).addTo(map);
// Polígono
L.polygon([
[51.509, -0.08],
[51.503, -0.06],
[51.51, -0.047]
], {
color: 'blue',
fillOpacity: 0.3
}).addTo(map);
// Retângulo
L.rectangle([
[51.49, -0.08],
[51.5, -0.06]
], {
color: '#ff7800',
weight: 1
}).addTo(map);
// Polilinha (sem preenchimento)
L.polyline([
[51.505, -0.09],
[51.51, -0.1],
[51.51, -0.12]
], {
color: 'green',
weight: 3,
dashArray: '5, 10'
}).addTo(map);
Trabalhar com GeoJSON
GeoJSON é o formato padrão para dados geográficos:
const geojsonData = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Central Park",
"category": "park"
},
"geometry": {
"type": "Point",
"coordinates": [-73.965355, 40.782865]
}
},
{
"type": "Feature",
"properties": {
"name": "Manhattan",
"population": 1628706
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-74.047285, 40.679548],
[-73.907000, 40.679548],
[-73.907000, 40.882214],
[-74.047285, 40.882214],
[-74.047285, 40.679548]
]]
}
}
]
};
// Adicionar GeoJSON com estilo
L.geoJSON(geojsonData, {
style: function(feature) {
return {
color: feature.properties.category === 'park' ? 'green' : 'blue',
fillOpacity: 0.5
};
},
pointToLayer: function(feature, latlng) {
return L.circleMarker(latlng, {
radius: 8,
fillColor: '#ff7800',
color: '#000',
weight: 1,
fillOpacity: 0.8
});
},
onEachFeature: function(feature, layer) {
layer.bindPopup(`<b>${feature.properties.name}</b>`);
}
}).addTo(map);
Carregar GeoJSON a partir de URL
fetch('/api/locations.geojson')
.then(response => response.json())
.then(data => {
L.geoJSON(data).addTo(map);
});
Eventos do mapa
Eventos de clique
// Clique no mapa
map.on('click', function(e) {
console.log('Clicked at:', e.latlng);
L.popup()
.setLatLng(e.latlng)
.setContent(`Coordinates: ${e.latlng.lat.toFixed(4)}, ${e.latlng.lng.toFixed(4)}`)
.openOn(map);
});
// Clique no marcador
marker.on('click', function(e) {
console.log('Marker clicked');
});
Movimento do mapa
map.on('moveend', function() {
const center = map.getCenter();
const zoom = map.getZoom();
const bounds = map.getBounds();
console.log('Center:', center);
console.log('Zoom:', zoom);
console.log('Bounds:', bounds);
// Carregar dados para a área visível
loadMarkersInBounds(bounds);
});
map.on('zoomend', function() {
const zoom = map.getZoom();
// Mostrar/ocultar camadas com base no zoom
if (zoom > 15) {
detailLayer.addTo(map);
} else {
map.removeLayer(detailLayer);
}
});
Localização do utilizador
// Localizar o utilizador
map.locate({ setView: true, maxZoom: 16 });
map.on('locationfound', function(e) {
const radius = e.accuracy / 2;
L.marker(e.latlng)
.addTo(map)
.bindPopup(`You are within ${radius} meters`)
.openPopup();
L.circle(e.latlng, { radius: radius }).addTo(map);
});
map.on('locationerror', function(e) {
alert('Location access denied');
});
Controlos personalizados
Criar controlo personalizado
const InfoControl = L.Control.extend({
options: {
position: 'bottomright'
},
onAdd: function(map) {
const container = L.DomUtil.create('div', 'info-control');
container.innerHTML = '<h4>Map Info</h4><div id="info-content"></div>';
// Impedir interações no mapa ao clicar no controlo
L.DomEvent.disableClickPropagation(container);
return container;
},
update: function(content) {
document.getElementById('info-content').innerHTML = content;
}
});
const infoControl = new InfoControl();
map.addControl(infoControl);
// Atualizar ao passar o rato sobre a feature
layer.on('mouseover', function(e) {
infoControl.update(`<b>${e.target.feature.properties.name}</b>`);
});
Integração com Vue.js
<template>
<div id="map" ref="mapContainer"></div>
</template>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
export default {
props: {
center: {
type: Array,
default: () => [51.505, -0.09]
},
markers: {
type: Array,
default: () => []
}
},
data() {
return {
map: null,
markerLayer: null
};
},
mounted() {
this.initMap();
},
watch: {
markers: {
handler(newMarkers) {
this.updateMarkers(newMarkers);
},
deep: true
}
},
methods: {
initMap() {
this.map = L.map(this.$refs.mapContainer).setView(this.center, 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this.map);
this.markerLayer = L.layerGroup().addTo(this.map);
this.updateMarkers(this.markers);
},
updateMarkers(markers) {
this.markerLayer.clearLayers();
markers.forEach(m => {
L.marker([m.lat, m.lng])
.bindPopup(m.popup)
.addTo(this.markerLayer);
});
}
},
beforeDestroy() {
if (this.map) {
this.map.remove();
}
}
};
</script>
<style>
#map {
height: 400px;
width: 100%;
}
</style>
Dicas de desempenho
Grandes conjuntos de dados
// Usar o renderer Canvas para milhares de marcadores
const map = L.map('map', {
renderer: L.canvas()
});
// Ou por camada
L.circleMarker([lat, lng], {
renderer: L.canvas()
}).addTo(map);
// Simplificar GeoJSON no zoom
L.geoJSON(complexData, {
style: { weight: 2 },
smoothFactor: 1.5 // Simplificar paths
});
Lazy loading
// Carregar marcadores apenas dentro dos limites visíveis
map.on('moveend', debounce(async function() {
const bounds = map.getBounds();
const response = await fetch(`/api/markers?bbox=${bounds.toBBoxString()}`);
const data = await response.json();
markerLayer.clearLayers();
L.geoJSON(data).addTo(markerLayer);
}, 300));
Principais conclusões
- Escolha o fornecedor de tiles certo: OSM é gratuito, Mapbox para estilos personalizados
- Agrupe marcadores em clusters: Essencial para conjuntos de dados acima de ~100 marcadores
- Use GeoJSON: O formato padrão funciona em todo o lado
- Trate os eventos corretamente: Faça debounce de operações dispendiosas
- Canvas para desempenho: Mude o renderer para grandes conjuntos de dados
- Mobile-first: Teste cuidadosamente as interações por toque
O Leaflet fornece tudo o que é necessário para aplicações de cartografia profissionais, mantendo-se leve e flexível — perfeito para projetos que precisam de mapas sem dependência de fornecedor.