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 && 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没啥流量。