一、环境安装
pip install mapbox-vector-tile[proj] shapely mercantile
mapbox-vector-tile--- MVT encode/decode(依赖 protobuf + shapely)
shapely--- 构造 Polygon 几何
mercantile--- 经纬度 ↔ 瓦片 xyz 换算
二、核心思路
在指定经纬度范围内随机生成 20W 个小矩形 Polygon
每个要素附带随机 color(0--255 整数三元组或 CSS 色值)
用 mercantile计算目标 zoom 下各瓦片包围盒,按瓦片分组
对每个瓦片内的要素,将 WGS84 坐标量化到 tile 局部坐标 (0--4096) 后调用 mapbox_vector_tile.encode()
写出 {z}/{x}/{y}.mvt
⚠️ 单张 MVT 建议 ≤ 数万要素,20W 需分散到多个 z=14~16 的瓦片中。
三、完整示例代码
import os
import random
import math
import mercantile
from shapely.geometry import Polygon
import mapbox_vector_tile
# ── 参数 ──────────────────────────────────────────────
NUM_FEATURES = 200_000 # 20万个"立方体"(小方块)
ZOOM = 14 # 切片层级
TILE_EXTENT = 4096 # MVT标准范围
CENTER_LNG, CENTER_LAT = 116.39, 39.90 # 北京附近
RADIUS_DEG = 0.15 # 约16km
BOX_SIZE_DEG = 0.0003 # 每个小方块大小
OUTPUT_DIR = "./mvt_output"
LAYER_NAME = "colored_boxes"
# ────────────────────────────────────────────────────────
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ① 随机生成 (minx, miny, maxx, maxy) 的矩形 + 随机颜色
features_world = []
for fid in range(NUM_FEATURES):
cx = CENTER_LNG + random.uniform(-RADIUS_DEG, RADIUS_DEG)
cy = CENTER_LAT + random.uniform(-RADIUS_DEG, RADIUS_DEG)
half = BOX_SIZE_DEG / 2
poly = Polygon([
(cx - half, cy - half),
(cx - half, cy + half),
(cx + half, cy + half),
(cx + half, cy - half),
(cx - half, cy - half),
])
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
features_world.append({
"poly": poly,
"props": {"id": fid, "r": r, "g": g, "b": b,
"color": f"#{r:02x}{g:02x}{b:02x}"}
})
# ② 按瓦片分组
tile_bins = {}
for feat in features_world:
centroid = feat["poly"].centroid
t = mercantile.tile(centroid.x, centroid.y, ZOOM)
tile_bins.setdefault(t, []).append(feat)
# ③ 逐瓦片编码
MVT_EXTENT = TILE_EXTENT
for (tx, ty, tz), feats in tile_bins.items():
if not feats:
continue
# 瓦片WGS84边界 → 量化用
tb = mercantile.bounds(tx, ty, tz)
bbox = (tb.west, tb.south, tb.east, tb.north) # minx, miny, maxx, maxy
layer_features = []
for feat in feats:
# 用 WKT + quantize_bounds 让编码器自动做坐标变换
layer_features.append({
"geometry": feat["poly"].wkt,
"properties": feat["props"]
})
pbf = mapbox_vector_tile.encode(
[{
"name": LAYER_NAME,
"features": layer_features
}],
default_options={
"quantize_bounds": bbox,
"extents": MVT_EXTENT,
"y_coord_down": False # MVT规范Y轴向下,encoder内部处理
}
)
z_dir = os.path.join(OUTPUT_DIR, str(tz))
x_dir = os.path.join(z_dir, str(tx))
os.makedirs(x_dir, exist_ok=True)
with open(os.path.join(x_dir, f"{ty}.mvt"), "wb") as f:
f.write(pbf)
print(f"✓ tile {tz}/{tx}/{ty} → {len(feats)} features, {len(pbf)} bytes")
print("Done! 输出目录:", OUTPUT_DIR)
四、前端渲染
<style>html,body,#map{margin:0;padding:0;width:100%;height:100%}</style>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script>
mapboxgl.accessToken = "accessToken";
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/standard', // ✅ 关键
center: [116.39, 39.90],
zoom: 14
});
map.on('style.load', () => {
map.addSource('coloredBoxes', {
type: 'vector',
tiles: ['./mvt_output/{z}/{x}/{y}.mvt'],
minzoom: 14,
maxzoom: 15
});
map.addLayer({
id: 'colored-boxes-fill',
type: 'fill',
source: 'coloredBoxes',
'source-layer': 'colored_boxes',
slot: 'middle',
paint: {
'fill-color': ['get', 'color'],
'fill-opacity': 0.85
}
});
});
</script>