Graticule
Runnable Leaflet example: Graticule. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:经纬网格
要点:按缩放级别选择网格步长、按视野范围生成经纬线、边缘标注 N/E、moveend 重绘
依赖:Leaflet 1.9.4 · jsDelivr CDN + Esri 瓦片
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>经纬网格</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<style>
html,body{height:100%;margin:0}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
.bar{flex:0 0 auto;padding:8px 12px;background:#f6f7f9;border-bottom:1px solid #e3e6ea;
display:flex;gap:12px;align-items:center;font-size:12px;color:#58606e}
.bar .spacer{flex:1 1 auto}
.bar b{color:#16181d;font-variant-numeric:tabular-nums}
#map{flex:1 1 auto;min-height:0}
.glab{font-size:10px;line-height:14px;color:#0e7490;background:rgba(255,255,255,.88);
padding:0 3px;border-radius:2px;white-space:nowrap;border:1px solid rgba(14,116,144,.25)}
</style>
</head>
<body>
<div class="bar">
<span>网格步长 <b id="step">—</b></span>
<span class="spacer"></span>
<span>拖动或缩放后自动重算</span>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
'use strict';
var map = L.map('map', {
center: [39.907, 116.391],
zoom: 12,
zoomControl: true
});
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19,
attribution: 'Tiles © Esri'
}).addTo(map);
/* 网格与标注放在同一组,便于整体清除 */
var grid = L.layerGroup().addTo(map);
var stepEl = document.getElementById('step');
function stepForZoom(z) {
if (z <= 3) return 30;
if (z <= 5) return 10;
if (z <= 7) return 5;
if (z <= 9) return 1;
if (z <= 11) return 0.2;
if (z <= 13) return 0.05;
return 0.01;
}
function fmtLat(v, step) {
var digits = step < 0.1 ? 2 : step < 1 ? 1 : 0;
return Math.abs(v).toFixed(digits) + '°' + (v >= 0 ? 'N' : 'S');
}
function fmtLng(v, step) {
var digits = step < 0.1 ? 2 : step < 1 ? 1 : 0;
return Math.abs(v).toFixed(digits) + '°' + (v >= 0 ? 'E' : 'W');
}
function label(latlng, text, anchor) {
return L.marker(latlng, {
interactive: false,
icon: L.divIcon({
className: '',
html: '<span class="glab">' + text + '</span>',
iconSize: [1, 1],
iconAnchor: anchor
})
});
}
function draw() {
grid.clearLayers();
var bounds = map.getBounds();
var step = stepForZoom(map.getZoom());
stepEl.textContent = step + '°';
var west = bounds.getWest();
var east = bounds.getEast();
var south = bounds.getSouth();
var north = bounds.getNorth();
var latStart = Math.ceil(south / step) * step;
var lngStart = Math.ceil(west / step) * step;
var lineStyle = {
color: '#0e7490',
weight: 1,
opacity: 0.35,
dashArray: '4 4',
interactive: false
};
var lat, lng, count = 0;
for (lat = latStart; lat <= north && count < 60; lat += step, count++) {
var la = Number(lat.toFixed(6));
grid.addLayer(L.polyline([[la, west], [la, east]], lineStyle));
grid.addLayer(label(
L.latLng(la, west + (east - west) * 0.015),
fmtLat(la, step),
[0, 8]
));
}
count = 0;
for (lng = lngStart; lng <= east && count < 60; lng += step, count++) {
var ln = Number(lng.toFixed(6));
grid.addLayer(L.polyline([[south, ln], [north, ln]], lineStyle));
grid.addLayer(label(
L.latLng(north - (north - south) * 0.03, ln),
fmtLng(ln, step),
[0, 8]
));
}
}
/* move 期间每帧触发,合并到 moveend 与 zoomend 即可 */
map.on('moveend', draw);
map.on('zoomend', draw);
draw();
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.