基于 PostGIS 生成MVT矢量瓦片

1、本文的“矢量瓦片”特指:基于Mapbox标准的矢量瓦片的 Mapbox Vector Tiles(MVT)。与矢量生成的栅格瓦片无关(有些人也称之为矢量瓦片)。

2、本文并未过多涉及瓦片的优化,如需优化:可以考虑创建空间索引、减少瓦片属性字段、缓存瓦片、多子域(多个域名)等手段进行优化。

(1)减少属性的解决方法:只保留一个唯一标识或前端标注使用的属性,其他属性可以通过查询获取到。在数据的字段比较多时,减少瓦片返回的字段数,这个对瓦片优化是非常可观的。

(2)缓存瓦片:基于瓦片生成原理,我们可以写一个程序获取到数据的范围,根据数据的范围计算对应的xyz行列号,再通过xyz行列号算出瓦片的范围从而得到矢量瓦片;当数据更新时,也可以获取到数据的对应范围,再通过范围计算出xyz行列号进行更新。

3、本文暂不涉及矢量瓦片的生成原理,因为笔者也不是非常理解其底层实现转换原理,属于应用层级的矢量瓦片生成。

4、本文主要是生成动态的矢量瓦片,目前生成矢量瓦片比较慢的层级为7、8、9、10级,特别是7、8级,本人在测试百万土地利用类型图斑加载时,这个是调优的关键点。

基于 PostGIS 生成MVT矢量瓦片

基于java(Springboot+MyBaits实现)+ PostgreSQL 13.x(PostGIS 3.4.2)实现。其他编程语言主要是进行对Web开发后端的模块进行改造即可。

1 PostGIS基础函数

1.1 矢量瓦片函数

(1)ST_AsMvtGeom:将几何图形从地理坐标系或投影坐标系转换为Mapbox矢量切片瓦片坐标空间的数据(目前尝试地理坐标或投影坐标系【4326、3857、4490】都可以转化成功并且成功在前端加载)。简单描述为:将地理坐标或投影坐标系下的数据转换为MVT矢量瓦片坐标系下的数据

参考:https://postgis.net/docs/manual-3.5/zh_Hans/ST_AsMVTGeom.html

(2)ST_AsMVT:将Mapbox矢量瓦片下的几何图形(通常是ST_AsMvtGeom的转换结果)转换为二进制 Mapbox 矢量瓦片表示。简单描述为:将MVT矢量瓦片坐标系下的数据转换为二进制表示,用于传输到前端

参考:https://postgis.net/docs/manual-dev/zh_Hans/ST_AsMVT.html

1.2 辅助函数

(1)ST_Transform:进行空间坐标系的转换,可以将地理数据转换到指定的SRID下。

(2)ST_TileEnvelope:生成一个范围。根据3857的XYZ切片方案中的 缩放级别 Z 和该级别的网格中图块的 XY 索引生成一个矩形多边形范围。这个主要是用于计算每一张瓦片的范围。

 SELECT ST_AsText( ST_TileEnvelope(2, 1, 1) );

2 使用PostGIS生成MVT基础

2.1 准备导入数据到PostGIS

2.1.1 通过PostGIS导入数据到PostGIS
1、连接到PostGIS

2、添写数据库连接信息

3、进入数据库管理器

4、选择导入图层

5、选择并导入数据

2.2 基础PostGIS SQL

1、将几何字段转换为WKT字符串形式
 -- 1、将geom转为wkt字符串显示
 SELECT
     ST_ASTEXT (GEOM)
 FROM
     PUBLIC."China_province";
2、查询的范围:ST_TILEENVELOPE
 -- 2、查看查询的范围
 SELECT
     ST_ASTEXT (ST_TILEENVELOPE (1, 1, 0));

3、将查询范围(3857坐标系)转换为4326:ST_TRANSFORM

 -- 3、将查询范围(3857坐标系)转换为4326
 SELECT
     ST_ASTEXT (ST_TRANSFORM (ST_TILEENVELOPE (1, 1, 0), 4326));

4、将数据从空间坐标系转为MVT瓦片坐标系:ST_TRANSFORM

 -- 4、将查询范围的几何结果从地理坐标系转为像素坐标系中
 SELECT
     ST_ASTEXT (
         ST_ASMVTGEOM (
             GEOM,
             ST_TRANSFORM (ST_TILEENVELOPE (1, 1, 0), 4326)
         )
     ) AS GEOM
 FROM
     PUBLIC."China_province";

5、将查询范围的几何转为MVT瓦片:ST_ASMVT、ST_ASMVTGEOM

 -- 5、将查询范围的几何转为MVT瓦片
 SELECT
     ST_ASMVT (MVTGEOM.*) AS MVT
 FROM
     (
         SELECT
             ST_ASMVTGEOM (
                 GEOM,
                 ST_TRANSFORM (ST_TILEENVELOPE (1, 1, 0), 4326)
             )
         FROM
             PUBLIC."China_province"
     ) MVTGEOM;

6、使用范围查询矢量瓦片:ST_MAKEENVELOPE

-- 6、查询矢量瓦片:使用范围查询
 WITH
     MVTGEOM AS (
         SELECT
             ID,
             ST_ASMVTGEOM (
                 GEOM,
                 ST_MAKEENVELOPE (106.875, -67.5, 112.5, -61.875, 4490),
                 4096,
                 64,
                 FALSE
             ) AS GEOM
         FROM
             PUBLIC."China_province"
         WHERE
             -- 判断几何是否与范围相交,或者使用:ST_INTERSECTS()函数
             GEOM && ST_MAKEENVELOPE (106.875, -67.5, 112.5, -61.875, 4490)
     )
 SELECT
     ST_ASMVT (MVTGEOM.*, 'China_province') AS MVT
 FROM
     MVTGEOM;

3 基于Springboot+MyBaits构建矢量瓦片服务

3.1 搭建Java工程

3.1.1 创建Maven工程

3.1.2 添加Maven依赖

需要添加Springboot、MyBatis、PostgreSQL连接依赖。

pom.xml

 <?xml version="1.0" encoding="UTF-8"?>
 <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://maven.apache.org/POM/4.0.0"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     
     <groupId>org.xyzgis</groupId>
     <artifactId>opengis-postgis</artifactId>
     <version>1.0-SNAPSHOT</version>
     <build>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
                     <source>9</source>
                     <target>9</target>
                 </configuration>
             </plugin>
         </plugins>
     </build>
     
     <!--Springboot版本信息-->
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
         <version>2.7.0</version>
         <!--设定一个空值将始终从仓库中获取,不从本地路径获取-->
         <relativePath/>
     </parent>
     
     <properties>
         <maven.compiler.source>8</maven.compiler.source>
         <maven.compiler.target>8</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
     
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         
         <!--mybatis-plus数据对接依赖-->
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <version>3.5.1</version>
         </dependency>
         
         <!--postgresql数据库依赖-->
         <dependency>
             <groupId>org.postgresql</groupId>
             <artifactId>postgresql</artifactId>
             <version>42.2.22</version>
         </dependency>
         
         <!--Knife4接口文档-->
         <dependency>
             <groupId>com.github.xiaoymin</groupId>
             <artifactId>knife4j-spring-boot-starter</artifactId>
             <version>2.0.9</version>
         </dependency>
         
         <!--测试依赖-->
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
 ​
 </project>
3.1.3 实体类

VectorTile.java 

package org.xyzgis.dto;
 ​
 /**
  * @ClassName VectorTile
  * @Description 矢量瓦片数据类
  * @Author xuyizhuo
  * @Date 2024/8/31 13:08
  */
 public class VectorTile {
     byte[] mvt; // Mapbox标注矢量瓦片数据
 ​
     public byte[] getMvt() {
         return mvt;
     }
 ​
     public void setMvt(byte[] mvt) {
         this.mvt = mvt;
     }
 }
3.1.4 添加Mapper层(数据访问层)
Mapper.java
package org.xyzgis.mapper;
 ​
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.xyzgis.dto.VectorTile;
 ​
 @Mapper
 public interface VectorTileMapper extends BaseMapper<VectorTile> {
 }
Mapper.xml

位置:resources/mapper/Mapper.java

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="org.xyzgis.mapper.VectorTileMapper">
 </mapper>
3.1.5 添加Service层(业务逻辑层)
package org.xyzgis.service;
 ​
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.xyzgis.dto.VectorTile;
 import org.xyzgis.mapper.VectorTileMapper;
 import org.xyzgis.utils.SimplifyUtil;
 import org.xyzgis.utils.Tile4326Util;
 ​
 /**
  * @ClassName VectorTileService
  * @Description 矢量瓦片服务
  * @Author xuyizhuo
  * @Date 2024/8/31 13:09
  */
 @Service
 public class VectorTileService {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 ​
     @Autowired
     private VectorTileMapper vectorTileMapper;
 }
3.1.6 添加Controller(控制层)
package org.xyzgis.controller;
 ​
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
 import org.springframework.web.bind.annotation.*;
 import org.xyzgis.dto.VectorTile;
 import org.xyzgis.service.VectorTileService;
 import org.xyzgis.utils.Tile4326Util;
 ​
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 ​
 /**
  * @ClassName VectorTilePostGISController
  * @Description 获取矢量瓦片接口类
  * @Author xuyizhuo
  * @Date 2024/8/31 13:04
  */
 @CrossOrigin
 @RestController
 @Api(tags = "获取矢量瓦片服务")
 @RequestMapping("/services/map")
 public class VectorTilePostGISController {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
     @Autowired
     private VectorTileService vectorTileService;
 }
3.1.7 配置启动端口、访问根路径(按需配置)  
server:
     port: 9761 # 服务端口
     servlet:
         context-path: /xyzgis # 服务根路径
 spring:
     datasource:
         driver-class-name: org.postgresql.Driver
         username: postgres # PostgreSQL数据库用户
         password: XyzGIS520 # PostgreSQL数据库用户密码
         url: jdbc:postgresql://127.0.0.1:5432/chinavector # PostgreSQL数据库地址,chinavector为数据库名称
 logging:
     level:
         org:
             xyzgis: debug # 开发阶段设置日志级别debug
3.1.8 启动类
package org.xyzgis;
 ​
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 ​
 /**
  * @ClassName XyzgisServerStarter
  * @Description 启动器
  * @Author xuyizhuo
  * @Date 2024/08/31 13:02
  */
 @SpringBootApplication
 @MapperScan("org.xyzgis.mapper") // Mybaits mapper配置文件的扫描目录
 public class XyzgisServerStarter {
     public static void main(String[] args) {
         SpringApplication.run(XyzgisServerStarter.class, args);
     }
 }

3.2 生成3857坐标系下的矢量瓦片

3.2.1 Mapper层实现
Mapper.java
package org.xyzgis.mapper;
 ​
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.xyzgis.dto.VectorTile;
 ​
 @Mapper
 public interface VectorTileMapper extends BaseMapper<VectorTile> {
 ​
     /**
      * 获取指定行列号的矢量瓦片
      * @param z 缩放等级
      * @param x 瓦片行号
      * @param y 瓦片列号
      * @return 矢量瓦片
      */
     VectorTile selectTile(String dataSourceName, Integer z, Integer x, Integer y);
 ​
     /***
      * 根据边界获取矢量瓦片
      * @param dataSourceName
      * @param bound
      * @return
      */
     VectorTile selectTileByBound(String dataSourceName, String bound);
 }
Mapper.xml
<!--查询矢量瓦片-->
     <select id="selectTile" resultType="org.xyzgis.dto.VectorTile">
         -- 动态获取图层的坐标系
         WITH
             -- 计算数据的坐标系
             T_SRID AS (SELECT ST_SRID(GEOM)                     AS SRID,
                               ST_TILEENVELOPE(#{z}, #{x}, #{y}) as BOUNDS
                        FROM "${dataSourceName}"
                        WHERE GEOM IS NOT NULL
                        LIMIT 1),
             -- 计算瓦片的范围
             T_BOUNDS AS (SELECT T_SRID.BOUNDS                            as BOUNDS,
                                 -- 在bound计算中,使用前面计算得到的坐标系代码
                                 ST_TRANSFORM(T_SRID.BOUNDS, T_SRID.SRID) as BOUNDS_geom
                          FROM T_SRID),
             -- 查询矢量瓦片
             MVTGEOM AS (SELECT ID,
                                ST_ASMVTGEOM(
                                        ST_TRANSFORM(GEOM, 3857),
                                    -- ST_TRANSFORM(ST_Simplify(geom, 0.2), 3857),
                                        T_BOUNDS.BOUNDS,
                                        4096
                                    ) AS GEOM
                         FROM "${dataSourceName}",
                              T_BOUNDS
                         WHERE ST_INTERSECTS(
                                       GEOM,
                                       T_BOUNDS.BOUNDS_geom
                                   ))
         SELECT ST_ASMVT(MVTGEOM.*, #{dataSourceName}) AS MVT
         FROM MVTGEOM;
     </select>
 ​
     <!-- 根据范围获取矢量瓦片:3857 -->
     <select id="selectTileByBound" resultType="org.xyzgis.dto.VectorTile">
         WITH MVTGEOM AS (SELECT ID,
                                 ST_ASMVTGEOM(
                                         geom,
                                         ST_MakeEnvelope(${bound}, 4490),
                                         4096
                                     ) AS GEOM
                          FROM "${dataSourceName}"
                          WHERE GEOM &amp;&amp; ST_MakeEnvelope(${bound}, 4490))
         SELECT ST_ASMVT(MVTGEOM.*, #{dataSourceName}) AS MVT
         FROM MVTGEOM;
     </select>
3.2.2 Service层实现
@Service
 public class VectorTileService {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 ​
     @Autowired
     private VectorTileMapper vectorTileMapper;
 ​
     /**
      * 获取指定行列号的矢量瓦片
      *
      * @param z 缩放等级
      * @param x 瓦片行号
      * @param y 瓦片列号
      * @return 矢量瓦片
      */
     public VectorTile getTile(String dataSourceName, Integer z, Integer x, Integer y) {
         return this.vectorTileMapper.selectTile(dataSourceName, z, x, y);
     }
 }
3.2.3 Controller层实现
package org.xyzgis.controller;
 ​
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
 import org.springframework.web.bind.annotation.*;
 import org.xyzgis.dto.VectorTile;
 import org.xyzgis.service.VectorTileService;
 import org.xyzgis.utils.Tile4326Util;
 ​
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 ​
 /**
  * @ClassName VectorTilePostGISController
  * @Description 获取矢量瓦片接口类
  * @Author xuyizhuo
  * @Date 2024/8/31 13:04
  */
 @CrossOrigin
 @RestController
 @Api(tags = "获取矢量瓦片服务")
 @RequestMapping("/services/map")
 public class VectorTilePostGISController {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
     @Autowired
     private VectorTileService vectorTileService;
 ​
     @ApiOperation(value = "动态矢量切片")
     @ApiImplicitParams(value = {
             @ApiImplicitParam(name = "dataSourceName", value = "数据源名称(对应数据库的表名)", required = true),
             @ApiImplicitParam(name = "z", value = "缩放等级", required = true),
             @ApiImplicitParam(name = "y", value = "瓦片行号", required = true),
             @ApiImplicitParam(name = "x", value = "瓦片列号", required = true)
     })
     @GetMapping("{dataSourceName}/vector/tile/{z}/{x}/{y}.pbf")
     public void getTile(@PathVariable String dataSourceName, @PathVariable Integer z,
                         @PathVariable Integer x,
                         @PathVariable Integer y,
                         HttpServletResponse response) {
         if (dataSourceName == null || dataSourceName.isEmpty()) {
             throw new RuntimeException("数据源名称不能为空");
         }
         try {
 ​
             VectorTile vectorTile = vectorTileService.getTile(dataSourceName, z, x, y);
             logger.debug("{}\t,瓦片:{}/{}/{}.pbf,length:{}", dataSourceName, z, x, y, vectorTile.getMvt().length);
 ​
             // 设置响应数据
             response.setContentType("application/x-protobuf");
             response.setCharacterEncoding("utf-8");
             // 这里URLEncoder.encode可以防止中文乱码
             String encodedFileName = URLEncoder.encode(x.toString(), StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
             response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=utf-8''" + encodedFileName + ".pbf");
             ServletOutputStream outputStream = response.getOutputStream();
             try {
                 outputStream.write(vectorTile.getMvt());
             } catch (IOException e) {
                 logger.debug("请求取消:" + e.getMessage());
                 // e.printStackTrace();
             }
 ​
         } catch (Exception e) {
             // 重置response
             logger.error("获取矢量瓦片失败:" + e.getMessage());
             throw new RuntimeException("获取矢量瓦片失败", e);
         }
     }
 }
3.2.4 Maplibre(Mapbox)加载示例

Mapbox1.x是开源的,开源协议比较宽松,高版本会有一定的商业限制。而Maplibre是基于Mapbox1.x的分支版本,目前暂不存在商业限制问题,在使用上两者相差不大。

 <!DOCTYPE html>
 <html lang="en">
 ​
 <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>加载PostGIS矢量瓦片服务</title>
     <!-- Maplibre依赖 -->
     <link href="../../lib/maplibre-gl-js-4.5.0/maplibre-gl.css" rel="stylesheet" />
     <script src="../../lib/maplibre-gl-js-4.5.0/maplibre-gl.js"></script>
 ​
     <style>
         * {
             margin: 0;
             padding: 0;
         }
 ​
         html,
         body,
         #map {
             width: 100%;
             height: 100%;
         }
     </style>
 </head>
 ​
 <body>
     <div id="map"></div>
     <script>
         // 初始化MapBox地图
         const map = new maplibregl.Map({
             container: "map",
             // 初始化的地图Style
             style: {
                 version: 8,
                 sources: {
                     "tiandtu-raster": {
                         type: "raster",
                         tileSize: 256,
                         // 加载xyz瓦片
                         tiles: [
 "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png"
                         ],
                     },
                     "China_county": {
                         type: 'vector',
                         tiles: [`http://localhost:9761/xyzgis/services/map/China_county/vector/tile/{z}/{x}/{y}.pbf`],
                         minzoom: 0,
                         maxzoom: 18,
                     },
                 },
                 layers: [
                     {
                         id: "tiandtu",
                         type: "raster",
                         source: "tiandtu-raster",
                         // minzoom: 0,
                         // maxzoom: 22,
                     },
                     {
                         id: "China_county",
                         "source-layer": "China_county",
                         type: "fill",
                         source: "China_county",
                         paint: {
                             "fill-antialias": true, // 填充时是否反锯齿
                             "fill-color": "#f5f4ee", // 填充的颜色
                             "fill-outline-color": "#b7b7a1", // 描边的颜色
                             // "fill-opacity": 0.9, // 填充的不透明度
                         },
                     }
                 ],
             },
             minzoom: 0,
             center: [92.91032639969171, 57.18390324160433],
             zoom: 2,
         });
         console.log("初始化地图成功", map);
 ​
         map.on("load", function () {
             addClickEvent();
         });
     </script>
 </body>
 ​
 </html>

3.3 生成4326、4490坐标系下的矢量瓦片

PostGIS使用范围的方式获取矢量瓦片,需要将XYZ瓦片矩阵转换为瓦片的范围。

3.3.1 XYZ瓦片转范围工具

Tile4326Util.java

package org.xyzgis.utils;
 ​
 /**
  * @ClassName Tile4326Util
  * @Description 4326坐标系xyz行列号转换工具类
  * @Author xuyizhuo
  * @Date 2024/9/1 21:13
  */
 public class Tile4326Util {
     /**
      * 将xyz行列号转换为经纬度范围
      * @param z
      * @param x
      * @param y
      * @return
      */
     public static String xyz2prjBound(int z, int x, int y) {
 ​
         double xTileCount = Math.pow(2, z + 1); // 0级别2张瓦片,1级别4张瓦片
         double yTileCount = Math.pow(2, z);
 ​
         double xMin = (x / xTileCount) * 360 - 180;
         double yMin = 90 - ((y + 1) / yTileCount) * 180;
         double xMax = ((x + 1) / xTileCount) * 360 - 180;
         double yMax = 90 - (y / yTileCount) * 180;
         return String.format("%s,%s,%s,%s", xMin, yMin, xMax, yMax);
     }
 }
3.3.2 Mapper层实现
Mapper.java
 Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="org.xyzgis.mapper.VectorTileMapper">
     <!--根据范围获取矢量瓦片:支持以数据坐标返回,如4326、4490的地理坐标系-->
     <select id="selectGeographyTile" resultType="org.xyzgis.dto.VectorTile">
         WITH
             -- 计算数据的坐标系
             T_SRID AS (
                 SELECT
                     -- ST_SRID (GEOM) AS SRID,
                     ST_MAKEENVELOPE (${bound}, ST_SRID (GEOM)) AS BOUNDS_GEOM
                 FROM
                     "${dataSourceName}"
                 WHERE
                     GEOM IS NOT NULL
                 LIMIT
                     1
             ),
             -- 查询矢量瓦片
             MVTGEOM AS (
                 SELECT
                     ID,
                     ST_ASMVTGEOM (
                             GEOM,
                             BOUNDS_GEOM,
                             4096
                         ) AS GEOM
                 FROM
                     "${dataSourceName}", T_SRID
                 WHERE
                     ST_INTERSECTS (GEOM, BOUNDS_GEOM)
             )
         SELECT
             ST_ASMVT (MVTGEOM.*, #{dataSourceName}) AS MVT
         FROM
             MVTGEOM;
     </select>
 </mapper>
3.3.3 Service层实现
package org.xyzgis.service;
 ​
 import org.xyzgis.utils.Tile4326Util;
 ​
 /**
  * @ClassName VectorTileService
  * @Description 矢量瓦片服务
  * @Author xuyizhuo
  * @Date 2024/8/31 13:09
  */
 @Service
 public class VectorTileService {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 ​
     @Autowired
     private VectorTileMapper vectorTileMapper;
 ​
     /**
      * 以地理坐标的形式获取指定行列号的矢量瓦片
      *
      * @param dataSourceName
      * @param z
      * @param x
      * @param y
      * @return
      */
     public VectorTile getGeographyTile(String dataSourceName, Integer z, Integer x, Integer y) {
         logger.debug("获取地理坐标的瓦片 = {}, {}, {}, 范围==> {}", z, x, y, Tile4326Util.xyz2prjBound(z, x, y));
         return this.vectorTileMapper.selectGeographyTile(dataSourceName, Tile4326Util.xyz2prjBound(z, x, y));
     }
 }
3.3.4 Controller层实现
package org.xyzgis.controller;
 /**
  * @ClassName VectorTilePostGISController
  * @Description 获取矢量瓦片接口类
  * @Author xuyizhuo
  * @Date 2024/8/31 13:04
  */
 @CrossOrigin
 @RestController
 @Api(tags = "获取矢量瓦片服务")
 @RequestMapping("/services/map")
 public class VectorTilePostGISController {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
     @Autowired
     private VectorTileService vectorTileService;
 ​
     /**
      * 获取地理坐标系下的矢量瓦片,使用EPSG:4326投影
      */
     @GetMapping("{dataSourceName}/vector/tile/geography/{z}/{x}/{y}.pbf")
     public void getGeographyTile(@PathVariable String dataSourceName, @PathVariable Integer z,
                                  @PathVariable Integer x,
                                  @PathVariable Integer y,
                                  Integer zoomOffset,
                                  HttpServletResponse response) {
         if (dataSourceName == null || dataSourceName.isEmpty()) {
             throw new RuntimeException("数据源名称不能为空");
         }
         if (zoomOffset != null) {
             z += zoomOffset;
         }
         try {
 ​
             VectorTile vectorTile = vectorTileService.getGeographyTile(dataSourceName, z, x, y);
             logger.debug("{}\t,zoomOffset = {}, 瓦片:{}/{}/{}.pbf,length:{}", dataSourceName, zoomOffset, z, x, y,
                     vectorTile.getMvt().length);
             logger.debug("获取指定行列号的范围 = {}", Tile4326Util.xyz2prjBound(z, x, y));
 ​
             // 设置响应数据
             response.setContentType("application/x-protobuf");
             response.setCharacterEncoding("utf-8");
             // 这里URLEncoder.encode可以防止中文乱码
             String encodedFileName = URLEncoder.encode(x.toString(), StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
             response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=utf-8''" + encodedFileName + ".pbf");
             ServletOutputStream outputStream = response.getOutputStream();
             try {
                 outputStream.write(vectorTile.getMvt());
             } catch (IOException e) {
                 logger.debug("请求取消:" + e.getMessage());
             }
 ​
         } catch (Exception e) {
             // 重置response
             logger.error("获取矢量瓦片失败:" + e.getMessage());
             throw new RuntimeException("获取矢量瓦片失败", e);
         }
     }
 }
3.3.5 Maplibre(Mapbox)加载示例(基于超图)

目前市场上的Maplibre(Mapbox)官方版本仅仅支持3857投影的矢量瓦片加载,如需加载4490的坐标系,需要经过改造后的。目前市场上主要是有两款改造好的:(1)cgcs2000 (2)超图定制后的Maplibre(Mapbox),超图从底层去改造的,不是插件模式,也不支持npm的安装模式。以下示例是基于超图定制后的Maplibre加载示例(可以在超图的iClient包中下载获取到):

<!DOCTYPE html>
 <html lang="en">
 ​
 <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>加载PostGIS矢量瓦片_4326坐标系_SuperMap</title>
 ​
     <!-- 超图定制的Maplibre,下载路径:https://iclient.supermap.io/download/download.html -->
     <link href="../../lib/maplibre-gl-js-enhance/4.3.0-1/maplibre-gl-enhance.css" rel="stylesheet" />
     <script src="../../lib/maplibre-gl-js-enhance/4.3.0-1/maplibre-gl-enhance.js"></script>
     <style>
         * {
             margin: 0;
             padding: 0;
         }
 ​
         html,
         body,
         #map {
             width: 100%;
             height: 100%;
         }
     </style>
 </head>
 ​
 <body>
     <div id="map"></div>
     <script>
         // 初始化MapBox地图
         const map = new maplibregl.Map({
             container: "map",
             // 一定要设置style对象,sources取值为对象,layers取值为数组
             style: {
                 version: 8,
                 // 自定义字体,如果要动态设置标注,必须设置glyphs,否则无法显示文本(包含数字英文)
                 // glyphs: "../lib/font/{fontstack}/{range}.pbf",
                 sources: {
                     "tiandtu-raster": {
                         type: "raster",
                         tileSize: 256,
                         // 加载xyz瓦片,加载天地图经纬度瓦片
                         tiles: [
                             "https://t0.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t1.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t2.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t3.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t4.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t5.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t6.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                             "https://t7.tianditu.gov.cn/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=0b018552994f71a9467d24461a8f8238",
                         ],
                     },
                     "China_province": {
                         type: 'vector',
                         minzoom: 0,
                         maxzoom: 5,
                         tiles: ["http://localhost:9761/xyzgis/services/map/China_province/vector/tile/geography/{z}/{x}/{y}.pbf?zoomOffset=-1"],
                     },
                 },
                 layers: [
                     {
                         id: "tiandtu",
                         type: "raster",
                         source: "tiandtu-raster",
                         minzoom: 0,
                         maxzoom: 22,
                     },
                     {
                         id: "China_province",
                         "source-layer": "China_province",
                         type: "fill",
                         source: "China_province",
                         paint: {
                             "fill-antialias": true, // 填充时是否反锯齿
                             "fill-color": "#f5f4ee", // 填充的颜色
                             "fill-outline-color": "#b7b7a1", // 描边的颜色
                             // "fill-opacity": 0.9, // 填充的不透明度
                         },
                     },
                 ],
             },
             center: [103.57418476087491, 28.00749934867109],
             zoom: 3.5,
             crs: "EPSG:4326", // crs: "EPSG:4490", // 这里暂不考虑4326与4490的差别。在某一个精度下可以认为是一致的。
             // crs: maplibregl.CRS.EPSG4326
         });
 ​
         // 地图加载完成
         map.on("load", function () {
             console.log("初始化地图成功", map);
         });
     </script>
 </body>
 ​
 </html>

获取源代码

基于Java+PostGIS生成矢量瓦片

百度网盘
 

如果有硬币可以点击链接:https://download.csdn.net/download/xuyizhuo/89922755

看在阿里云盘的提示,不支持压缩文件分享。本来想放到gitee的,结果前面分享到gitee没啥流量。

### 使用 PostGIS 导出 MVT (Mapbox Vector Tiles) 为了实现从 PostGIS 数据库导出 MVT 格式的文件,可以利用 `ST_AsMVT` 函数来创建矢量瓦片。此方法允许直接查询地理空间数据并将其转换成适合前端渲染的地图切片。 #### 创建测试表 假设有一个名为 `public.buildings` 的表格存储建筑物几何信息: ```sql CREATE TABLE public.buildings ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, geom GEOMETRY(Polygon, 4326) NOT NULL ); ``` #### 查询并生成 MVT 切片 下面是一个 SQL 查询例子,展示如何使用 `ST_AsMVTGeom` 和 `ST_AsMVT` 来构建单个 MVT 瓦片[^1]: ```sql WITH mvtgeom AS ( SELECT ST_AsMVTGeom(geom, TileBBox(zoom_level, tile_x, tile_y)) as geom, name FROM buildings WHERE geom && ST_Transform(TileBBox(zoom_level, tile_x, tile_y), 4326) ) SELECT ST_AsMVT(mvtgeom.*, 'buildings') FROM mvtgeom; ``` 在这个查询里, - `TileBBox()` 计算给定缩放级别 (`zoom_level`) 及其对应的 X (`tile_x`) Y (`tile_y`) 坐标的边界框; - `ST_AsMVTGeom()` 负责调整原始几何对象使之适应于当前瓦片范围; - 最终通过 `ST_AsMVT()` 把处理后的特征集合打包成二进制形式的 MVT 文件; 请注意上述代码片段中的变量 `zoom_level`, `tile_x`, 和 `tile_y` 应该被实际数值替换以便执行具体请求。 对于更复杂的场景,可能还需要考虑分层缓存机制以及优化性能等问题。此外,如果希望进一步简化操作流程,则可借助第三方工具如 `postgis-mvt-server` 或者编写自定义的应用程序接口(API),这些API能够自动接收 HTTP 请求参数(比如 z/x/y),进而返回相应的 MVT 数据流。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值