欢迎光临
我们一直在努力

java通过geotools实现矢量切片生成

📌 本文基于实际项目编写,介绍如何使用 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

    赞(0)
    未经允许不得转载:171主机测评 » java通过geotools实现矢量切片生成
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址