线宽不随缩放变化,放大后像一根头发丝
12 级时道路像主干道,17 级时同样的道路细得几乎看不见。
现象
道路线样式 weight: 3。在 12 级,3 像素约等于实地 40 米,看起来像条主干道;
到 17 级,3 像素只相当于 2.4 米,细到几乎不可见,且与两侧建筑比例完全失调。
反过来,如果为了在高倍看清而把 weight 设成 12,
12 级时道路会粗成一条色带,把整个街区盖住。
原因
Leaflet 的 weight 单位是 屏幕像素,不随 zoom 变化。
而在墨卡托投影下,每像素对应的实地长度随级别减半:
17 级:156543.03 / 2^17 ≈ 1.19 米/像素
12 级:156543.03 / 2^12 ≈ 38.22 米/像素
因此同样的 3 像素,在 12 级代表 115 米,在 17 级只代表 3.6 米。 想让线宽在视觉上稳定,必须让像素宽度随 zoom 递减。
| 缩放级别 | 米/像素 | 3 px 对应实地 |
|---|---|---|
| 10 | 152.87 | 458 m |
| 12 | 38.22 | 115 m |
| 15 | 4.78 | 14 m |
| 18 | 0.60 | 1.8 m |
排查过程
- 在不同级别截图,量同一段道路的像素宽度,确认是否为常数。
- 看样式配置里
weight是否写死。 - 检查是否有
zoomend监听去重置 weight,若有则确认公式是否正确。 - 对比底图道路宽度,判断目标比例:通常道路在图上的宽度应占实际宽度的 1/5 左右。
console.log(156543.03392 / Math.pow(2, 12)); // 38.22
console.log(156543.03392 / Math.pow(2, 17)); // 1.19
解决方案
Leaflet 用 zoomend 动态设置 weight,或用 SVG 的
vector-effect: non-scaling-stroke 保持实地宽度(不常用)。
Mapbox GL 直接写 interpolate。
// Leaflet:按级别给权重,越放大越细
function weightFor(z) {
if (z <= 10) return 1;
if (z <= 13) return 2;
if (z <= 16) return 3;
return 5;
}
const roads = L.geoJSON(roadData, {
style: f => ({ color: '#e8590c', weight: weightFor(map.getZoom()) })
}).addTo(map);
map.on('zoomend', () => roads.setStyle(
f => ({ color: '#e8590c', weight: weightFor(map.getZoom()) })
));
// Mapbox GL:表达式插值,逐级过渡
map.addLayer({
id: 'roads',
type: 'line',
source: 'roads',
paint: {
'line-color': '#e8590c',
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'],
8, 0.8,
12, 1.6,
16, 4,
18, 7
]
}
});
验证
在 12 级与 17 级各截一张图,道路屏幕宽度分别为约 2 像素与 4 像素, 与底图道路的实际宽度比例接近;缩小时不出现色带糊满街区。
console.assert(weightFor(12) < weightFor(17), '线宽未随级别增加');
// 实地宽度换算
[12, 17].forEach(z => console.log(z, (156543.03392 / 2 ** z * weightFor(z)).toFixed(1) + ' m'));
适用版本
| 库 | 版本 | 单位 |
|---|---|---|
| Leaflet | 1.9.x | weight 为屏幕像素 |
| Mapbox GL JS | 2.15 / 3.x | line-width 为屏幕像素,支持 interpolate |
| OpenLayers | 9.x | stroke.width 为像素,随分辨率变化需自算 |
经纬志(2026)。《线宽不随缩放变化,放大后像一根头发丝》。https://www.jingweizhi.com/cookbook/line-width-zoom-interpolate.html(访问日期 2026-11-03)。