📌 本文基于实际项目编写,介绍如何使用 GeoTools 库将 Shapefile 生成为 PBF 文件保存到本地。整套流程为:读取本地 SHP → 获取矢量要素属性 → 计算切片地理范围 → 生成 PBF 保存在本地。
序言
在地理信息系统(GIS)开发中,矢量切片(Vector Tiles)因其数据量小、渲染效率高、样式灵活等优点,逐渐成为 Web 地图的主流方案。
PBF(Protocol Buffers Binary Format) 是矢量切片的常用格式,相比 GeoJSON,它具有:
-
✅ 文件体积更小(通常只有 GeoJSON 的 1/5)
-
✅ 解析速度更快(二进制格式)
-
✅ 支持大规模数据切片
本文基于 若依框架 + GeoTools 实战项目,手把手教你实现从 Shapefile 到 PBF 矢量切片的完整流程。
一、Web 墨卡托投影与切片方案
在生成矢量切片之前,我们需要了解常见的地图切片方案。
1. 谷歌 XYZ 方案(本项目采用)
| 原点位置 | 左上角 |
| X 轴方向 | 从左向右递增 |
| Y 轴方向 | 从上向下递增 |
| Z 轴 | 缩放层级(0-18+) |
典型应用:高德地图、谷歌地图、OpenStreetMap
瓦片编号规则:
z = 缩放级别
x = 横向瓦片索引(从左到右)
y = 纵向瓦片索引(从上到下)
2. TMS 方案
| 原点位置 | 左下角 |
| X 轴方向 | 从左向右递增 |
| Y 轴方向 | 从下向上递增 |
| Z 轴 | 缩放层级 |
典型应用:腾讯地图、OSGeo TMS 标准
与 XYZ 的区别:Y 轴方向相反,转换公式:
y_tms = (1 << z) – 1 – y_xyz
3. QuadTree 方案
| 编码方式 | 四叉树编码 |
| 瓦片表示 | 同一级别的瓦片用整数表示 |
| 存储方式 | X/Y 转二进制编码 |
典型应用:Bing 地图
二、准备环境
项目技术栈
-
JDK:17+
-
GeoTools:28.0
-
JTS:1.18.1 / 1.13
-
Vector Tile:java-vector-tile 1.2.1
-
数据库:PostgreSQL + PostGIS(可选)
Maven 依赖
在pom.xml 中添加以下核心依赖:
<properties>
<geotools.version>28.0</geotools.version>
</properties>
<dependencies>
<!– GeoTools 核心 –>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
<!– GeoTools 投影支持 –>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-referencing</artifactId>
<version>${geotools.version}</version>
</dependency>
<!– GeoTools EPSG 数据库 –>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
<!– GeoTools GeoJSON 支持 –>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>${geotools.version}</version>
</dependency>
<!– JTS 几何库 –>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.1</version>
</dependency>
<!– Vector Tiles (PBF) 编码 –>
<dependency>
<groupId>no.ecc.vectortile</groupId>
<artifactId>java-vector-tile</artifactId>
<version>1.2.1</version>
</dependency>
<!– Protocol Buffers –>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.21.9</version>
</dependency>
</dependencies>
<!– GeoTools 仓库(必须配置) –>
<repositories>
<repository>
<id>osgeo</id>
<name>OSGeo Release Repository</name>
<url>https://repo.osgeo.org/repository/release/</url>
</repository>
</repositories>
三、核心代码实现
3.1 项目结构
gis/
├── src/main/java/com/gis/
│ ├── controller/
│ │ └── SpatialController.java # 切片生成控制器
│ ├── service/
│ │ ├── IShapefileService.java # Shapefile 服务接口
│ │ └── impl/
│ │ └── IShapefileServiceImpl.java # Shapefile 服务实现
│ ├── domain/
│ │ ├── ShapefileInfo.java # Shapefile 参数实体
│ │ └── VectorTile.java # 矢量瓦片参数实体
│ ├── mercator/
│ │ └── TileUtils.java # 墨卡托投影工具类
│ └── utils/
│ ├── ShapefileReader.java # Shapefile 读取工具
│ └── VectorTileEncoder.java # 矢量切片编码工具
3.2 核心实体类
ShapefileInfo(切片参数)
@Data
@Schema(name = "矢量数据请求参数", description = "请求接口支持参数")
public class ShapefileInfo implements java.io.Serializable {
@Schema(name = "图层名称", example = "test")
private String name;
@Schema(name = "地图切片低级级别", example = "0")
private Integer lowLevel;
@Schema(name = "地图切片高级级别", example = "6")
private Integer highLevel;
@Schema(name = "文件路径", example = "D:\\\\data\\\\qgis_data\\\\shp\\\\test.shp")
private String path;
@Schema(name = "文件保存路径", example = "D:\\\\data\\\\qgis_data\\\\tile\\\\testss\\\\")
private String savePath;
}
VectorTile(瓦片参数)
@Data
@Schema(description = "postGIS 接口支持参数")
public class VectorTile {
@Schema(description = "要素表名称")
private String name;
@Schema(description = "瓦片坐标 x")
private Integer x;
@Schema(description = "瓦片坐标 y")
private Integer y;
@Schema(description = "瓦片坐标 z")
private Integer z;
}
3.3 墨卡托投影工具类
public class TileUtils {
/**
* 瓦片 X 坐标转经度
*/
public static double tileXToLongitude(long tileX, byte zoom) {
return (tileX / Math.pow(2, zoom) * 360) – 180;
}
/**
* 瓦片 Y 坐标转纬度
*/
public static double tileYToLatitude(long tileY, byte zoom) {
double n = Math.PI – (2 * Math.PI * tileY) / Math.pow(2, zoom);
return Math.toDegrees(Math.atan(Math.sinh(n)));
}
/**
* 获取瓦片的地理边界范围
*/
public static ReferencedEnvelope getTileBounds(int x, int y, int z) {
double minLon = tileXToLongitude(x, (byte) z);
double maxLon = tileXToLongitude(x + 1, (byte) z);
double minLat = tileYToLatitude(y + 1, (byte) z);
double maxLat = tileYToLatitude(y, (byte) z);
CoordinateReferenceSystem crs = DefaultGeographicCRS.WGS84;
return new ReferencedEnvelope(minLon, maxLon, minLat, maxLat, crs);
}
/**
* 将边界框转换为瓦片坐标列表
*/
public static List<int[]> parseBound2Xyz(double minLon, double minLat,
double maxLon, double maxLat,
int zoomLevel) {
List<int[]> tiles = new ArrayList<>();
for (int x = lonToTile(minLon, zoomLevel); x <= lonToTile(maxLon, zoomLevel); x++) {
for (int y = latToTile(maxLat, zoomLevel); y <= latToTile(minLat, zoomLevel); y++) {
tiles.add(new int[]{x, y, zoomLevel});
}
}
return tiles;
}
private static int lonToTile(double lon, int zoom) {
return (int) Math.floor((lon + 180) / 360 * (1 << zoom));
}
private static int latToTile(double lat, int zoom) {
return (int) Math.floor(
(1 – Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI)
/ 2 * (1 << zoom)
);
}
/**
* 将几何对象转换为瓦片内像素坐标
*/
public static void convert2Piexl(int x, int y, int z, Geometry geom) {
double px = MercatorProjection.tileXToPixelX(x);
double py = MercatorProjection.tileYToPixelY(y);
Coordinate[] cs = geom.getCoordinates();
byte zoom = (byte) z;
for (Coordinate c : cs) {
c.x = (int) (((MercatorProjection.longitudeToPixelX(c.x, zoom)) – px) * 16);
c.y = (int) (((MercatorProjection.latitudeToPixelY(c.y, zoom)) – py) * 16);
}
}
}
3.4 切片生成服务实现
@Service
@Slf4j
public class IShapefileServiceImpl extends ServiceImpl<ShapefileMapper, ShapefileInfo>
implements IShapefileService {
@Override
public boolean getShapefile(ShapefileInfo shapefileInfo) throws RuntimeException, IOException {
boolean status = false;
// 获取 Shapefile 边界范围
String bound = ShapefileReader.bounds(shapefileInfo.getPath());
// 遍历每个缩放级别
for (int i = shapefileInfo.getLowLevel(); i <= shapefileInfo.getHighLevel(); i++) {
// 计算该级别的所有瓦片
List<int[]> tiles = getTilesFromBound(bound, i);
for (int[] tile : tiles) {
System.out.println("Generating tile: " + Arrays.toString(tile));
int x = tile[0];
int y = tile[1];
int z = tile[2];
// 获取瓦片的地理边界
ReferencedEnvelope tileBounds = TileUtils.getTileBounds(x, y, z);
// 读取当前瓦片范围内的矢量要素
ShapefileReader.ShapefileResult results = ShapefileReader.getShapefileReader(
shapefileInfo.getPath(), tileBounds
);
// 如果没有要素,跳过此瓦片
if (results != null) {
try {
// 创建矢量切片编码器(精度 4096,缓冲 16,简化 false)
VectorTileEncoder vte = new VectorTileEncoder(4096, 16, false);
// 遍历每个矢量要素
results.getFeatureList().stream()
.filter(featureAttributes -> featureAttributes.get("the_geom") != null)
.forEach(featureAttributes -> {
// 转换为 WKT 格式
WKTWriter wktWriter = new WKTWriter();
String wkt = wktWriter.write(
(org.locationtech.jts.geom.Geometry) featureAttributes.get("the_geom")
);
// 解析 WKT 为 Geometry 对象
Geometry geom = new WKTReader().read(wkt);
// 转换为瓦片像素坐标
TileUtils.convert2Piexl(x, y, z, geom);
// 移除几何字段,保留属性
featureAttributes.remove("the_geom");
// 添加要素到编码器
vte.addFeature(shapefileInfo.getName(), featureAttributes, geom);
});
// 编码为 PBF 数据
byte[] pbfData = vte.encode();
// 构建文件路径:savePath/name/z/x/y.pbf
String filePath = shapefileInfo.getSavePath() + "\\\\"
+ shapefileInfo.getName() + "\\\\" + z + "\\\\" + x + "\\\\" + y + ".pbf";
Path path = Paths.get(filePath);
// 确保父目录存在
Files.createDirectories(path.getParent());
// 写入文件
try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
fos.write(pbfData);
}
System.out.println("PBF file saved to: " + filePath);
} catch (IOException e) {
log.error("Error while processing shapefile: {}", e.getMessage());
return false;
}
} else {
System.out.println("跳过不存在的切片!");
}
}
}
return true;
}
}
3.5 控制器接口
@RestController
@Tag(name = "本地切片生成", description = "本地切片生成")
@RequestMapping("/spatial")
@Slf4j
public class SpatialController {
@Autowired
private IShapefileService iShapefileService;
/**
* 本地矢量切片生成接口
*
* @param shapefileInfo 切片参数(图层名称、级别范围、保存路径)
* @param file 上传的 ZIP 压缩包(包含.shp .shx .dbf .prj 等文件)
* @return 切片生成结果
*/
@PostMapping(value = "/acquireVectorTiles")
@Operation(summary = "本地矢量切片生成", description = "本地矢量切片生成")
public AjaxResult vectorTile(
@RequestParam(name = "shapefileInfo") ShapefileInfo shapefileInfo,
@RequestParam(name = "file") MultipartFile file) throws IOException {
log.info("上传文件:{}", shapefileInfo);
if (shapefileInfo == null) {
return AjaxResult.error("未上传切片级别信息");
}
// 如果 savePath 为空,使用若依默认上传路径
if (shapefileInfo.getSavePath() == null || shapefileInfo.getSavePath().isEmpty()) {
shapefileInfo.setSavePath(RuoYiConfig.getMapPath());
}
// 创建临时目录解压 ZIP
Path tempDir = Files.createTempDirectory("shapefile_");
String shapefilePath = null;
try (ZipInputStream zipInputStream = new ZipInputStream(
new BufferedInputStream(file.getInputStream()))) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
// 跳过非 shp 组件文件
if (!isShapefileComponent(entry.getName())) {
continue;
}
String simpleName = Paths.get(entry.getName()).getFileName().toString();
Path tempFile = tempDir.resolve(simpleName);
try (OutputStream out = Files.newOutputStream(tempFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = zipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
if (entry.getName().endsWith(".shp")) {
shapefilePath = tempFile.toString();
}
}
if (shapefilePath == null) {
return AjaxResult.error("未找到有效的 Shapefile (.shp)");
}
shapefileInfo.setPath(shapefilePath);
// 调用服务生成切片
boolean status = iShapefileService.getShapefile(shapefileInfo);
if (status) {
return AjaxResult.success(
"已获取 " + shapefileInfo.getName() + "=="
+ shapefileInfo.getLowLevel() + "~" + shapefileInfo.getHighLevel() + " 级切片",
shapefileInfo.getSavePath()
);
}
return AjaxResult.error("获取失败");
} finally {
// 清理临时目录
Files.walk(tempDir)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try { Files.delete(path); } catch (IOException ignored) {}
});
}
}
/**
* 动态获取矢量瓦片接口(实时计算)
*/
@GetMapping("/vectorTile/{z}/{x}/{y}.pbf")
@Operation(summary = "java 计算获取矢量瓦片", description = "根据 x,y,z 获取对应矢量瓦片")
@Anonymous
public ResponseEntity<byte[]> getVectorTile(
@PathVariable int z,
@PathVariable int x,
@PathVariable int y,
@RequestParam(name = "name", defaultValue = "roads") String name) throws Exception {
VectorTile vectorTile = new VectorTile(name, x, y, z);
byte[] pbfData = iShapefileService.getFeaturesInTile(vectorTile);
if (pbfData == null || pbfData.length == 0) {
return ResponseEntity.ok().build();
}
System.out.println(x + "." + y + "." + z + " PBF 数据大小:"
+ String.format("%.2f", (double) pbfData.length / (1024 * 1024)) + " MB");
return ResponseEntity.ok()
.header("Content-Type", "application/x-protobuf; proto=mapbox-vector-tile")
.header("Content-Disposition", "inline; filename=" + y + ".pbf")
.body(pbfData);
}
}
四、API 接口说明
4.1 生成矢量切片
接口:POST /spatial/acquireVectorTiles
请求参数:
| shapefileInfo | JSON | 切片参数对象 | 见下方 |
| file | MultipartFile | ZIP 压缩包(包含.shp 等文件) | – |
shapefileInfo 示例:
{
"name": "roads",
"lowLevel": 10,
"highLevel": 14,
"path": "",
"savePath": "D:/data/qgis_data/tile/"
}
响应示例:
{
"code": 200,
"msg": "已获取 roads==10~14 级切片",
"data": "D:/data/qgis_data/tile/"
}
4.2 获取单个瓦片
接口:GET /spatial/vectorTile/{z}/{x}/{y}.pbf
请求参数:
| z | path | 缩放级别 |
| x | path | 瓦片 X 坐标 |
| y | path | 瓦片 Y 坐标 |
| name | query | 图层名称 |
请求示例:
GET /spatial/vectorTile/12/1024/690.pbf?name=roads
响应头:
Content-Type: application/x-protobuf; proto=mapbox-vector-tile
Content-Disposition: inline; filename=690.pbf
五、输出目录结构
D:/data/qgis_data/tile/
└── roads/
├── 10/
│ ├── 512/
│ │ └── 345.pbf
│ └── …
├── 11/
│ ├── 1024/
│ │ ├── 690.pbf
│ │ └── 691.pbf
│ └── …
├── 12/
├── 13/
└── 14/
├── 2048/
│ ├── 1380.pbf
│ ├── 1381.pbf
│ ├── 1382.pbf
│ └── 1383.pbf
└── …
六、性能优化建议
1. 多线程生成
// 使用线程池并行生成瓦片
ExecutorService executor = Executors.newFixedThreadPool(8);
List<Future<?>> futures = new ArrayList<>();
for (int[] tile : tiles) {
futures.add(executor.submit(() -> {
generateTilePbf(tile);
}));
}
// 等待所有任务完成
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
2. 空间索引优化
在 Shapefile 读取时添加空间索引:
// 使用 QuadTree 空间索引加速查询
Quadtree spatialIndex = new Quadtree();
for (Feature feature : features) {
Envelope envelope = feature.getBounds();
spatialIndex.insert(envelope, feature);
}
// 查询时只获取相交要素
List<Feature> candidates = spatialIndex.query(tileBounds);
3. 几何简化
// 对小比例尺瓦片进行几何简化
if (z < 8) {
geom = TopologyPreservingSimplifier.simplify(geom, 0.01);
}
七、常见问题
1. 中文属性乱码
解决:设置 Shapefile 编码
// 在读取 Shapefile 时指定编码
Charset charset = Charset.forName("GBK");
dataStore.setCharset(charset);
2. 坐标系不匹配
解决:统一转换为 WGS84
CoordinateReferenceSystem sourceCRS = shapefile.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = DefaultGeographicCRS.WGS84;
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true);
Geometry transformed = JTS.transform(geometry, transform);
3. 瓦片边界要素缺失
解决:添加缓冲区
// 在查询时添加缓冲区
ReferencedEnvelope expandedBounds = new ReferencedEnvelope(tileBounds);
expandedBounds.expandBy(0.001); // 添加约 100 米缓冲区
4. 内存溢出
解决:
-
分批处理瓦片
-
增加 JVM 堆内存:-Xmx4G
-
及时释放资源:使用 try-with-resources
八、总结
本文完整介绍了基于 若依框架 + GeoTools 生成矢量切片 (PBF) 的全流程:
✅ 上传 ZIP 压缩包(包含 Shapefile 组件)
✅ 解压并读取 Shapefile
✅ 计算切片地理范围(XYZ 方案)
✅ 坐标投影转换(WGS84 → 像素坐标)
✅ 编码生成 PBF 文件
✅ 保存到本地目录
核心技术点:
-
墨卡托投影与瓦片坐标计算
-
GeoTools Shapefile 读取
-
java-vector-tile 编码
-
若依框架集成
应用场景:
-
Web 地图矢量数据展示
-
大规模矢量数据可视化
-
前端 Mapbox GL / OpenLayers 集成
📢 欢迎关注我的公众号
如果你觉得这篇文章对你有帮助,欢迎关注我的微信公众号 地信 Learner
公众号内容:
-
🗺️ GIS 开发技术干货
-
💻 Java/Python 实战教程
-
📊 空间数据处理技巧
-
🎯 行业案例分享
📚 参考资料
-
GeoTools 官方文档
-
Mapbox Vector Tile 规范
-
OpenStreetMap Wiki – Slippy Map

