矢量數(shù)據(jù)是包含空間幾何字段的數(shù)據(jù)译红,矢量數(shù)據(jù)可視化就是將數(shù)據(jù)庫中存儲(chǔ)的矢量數(shù)據(jù)請求到前端霜瘪,渲染成電子地圖的過程站宗。
為了滿足用戶對不同比例尺的地圖的瀏覽需求秧骑,通常需要繪制多個(gè)比例尺級別的地圖罪郊,根據(jù)用戶需求加載對應(yīng)的級別蠕蚜。但是顯示器屏幕有限,當(dāng)比例尺很大時(shí)悔橄,只能顯示地圖的局部靶累,所以會(huì)對每個(gè)級別的地圖進(jìn)行分片,形成一個(gè)個(gè)小的正方形像素范圍癣疟,稱為瓦片挣柬,每張瓦片都有一個(gè)唯一的坐標(biāo)(z, x, y),z表示瓦片所在的層級睛挚,x表示瓦片的列號(hào)邪蛔,y表示瓦片的行號(hào)。所有層級的瓦片形成了如圖1所示的金字塔結(jié)構(gòu)扎狱。
渲染時(shí)侧到,前端會(huì)根據(jù)地圖當(dāng)前的縮放級別勃教,確定z值,然后根據(jù)設(shè)定的顯示窗口大小床牧,確定需要加載z級的哪些瓦片荣回,即確定多個(gè)(x, y),形成成多個(gè)瓦片坐標(biāo)(z,x,y)戈咳,然后根據(jù)瓦片坐標(biāo)向后端請求瓦片中的數(shù)據(jù),并渲染在窗口中壕吹。
1. 矢量數(shù)據(jù)存儲(chǔ)
PostGIS是基于關(guān)系型數(shù)據(jù)庫PostgreSQL開發(fā)的插件著蛙,用于在關(guān)系型數(shù)據(jù)庫中支持空間數(shù)據(jù)的存儲(chǔ)管理。文章MyBatisPlus+PostGIS實(shí)現(xiàn)Geometry數(shù)據(jù)的讀寫介紹了SpringBoot項(xiàng)目中整合矢量數(shù)據(jù)的方法耳贬。
本文中踏堡,我們以創(chuàng)建一張興趣點(diǎn)(POI)的矢量表為例,SQL建表語句如下:
CREATE TABLE public.t_poi
(
id varchar(36) primary key, # ID
name varchar(255) not null, # 興趣點(diǎn)名稱
geom geometry(Point, 4326) null # 興趣點(diǎn)空間位置
);
興趣點(diǎn)數(shù)據(jù)存儲(chǔ)在t_poi表里面咒劲。
2. 動(dòng)態(tài)矢量切片原理
矢量瓦片是一份位于瓦片中的矢量數(shù)據(jù)的集合顷蟆,只是在矢量瓦片中,這些矢量數(shù)據(jù)的空間幾何字段不再是空間坐標(biāo)腐魂,而是以瓦片左上角為原點(diǎn)的像素坐標(biāo)帐偎。將原始的空間坐標(biāo)系中的矢量數(shù)據(jù)映射到瓦片中,并將其空間坐標(biāo)轉(zhuǎn)換成瓦片中的像素坐標(biāo)的過程稱為矢量切片蛔屹。
有兩種典型的矢量切片策略:靜態(tài)矢量切片和動(dòng)態(tài)矢量切片削樊。
靜態(tài)矢量切片是預(yù)先將所有級別的矢量瓦片都切好,存儲(chǔ)在文件系統(tǒng)中兔毒,前端請求的時(shí)候直接從文件系統(tǒng)中讀取并返回漫贞,但是這種策略無法渲染實(shí)時(shí)更新的矢量數(shù)據(jù),所以有了動(dòng)態(tài)矢量切片策略育叁,動(dòng)態(tài)矢量切片在前端發(fā)起請求時(shí)觸發(fā)的迅脐,如圖2所示,根據(jù)前端傳入的瓦片坐標(biāo)(z, x, y)生成PostGIS的SQL語句豪嗽,用PostGIS的矢量瓦片生成功能將存儲(chǔ)的矢量數(shù)據(jù)進(jìn)行坐標(biāo)轉(zhuǎn)換并編碼成矢量瓦片谴蔑。
接下來我們詳細(xì)了解這個(gè)SQL語句,如下所示:
WITH mvtgeom AS (
SELECT id,
name,
ST_AsMVTGeom(
ST_Transform(position, 3857),
ST_TileEnvelope(z, x, y), extent => 512, buffer => 8) AS geom
FROM t_gas_station
WHERE position && ST_Transform(ST_TileEnvelope(z, x, y), 4326)
)
SELECT ST_AsMVT(mvtgeom.*) as mvt
FROM mvtgeom
SQL語句中涉及到兩個(gè)坐標(biāo)系4326和3857昵骤,其中4326是POI表中存儲(chǔ)的矢量數(shù)據(jù)的空間坐標(biāo)系(和建表語句對應(yīng))树碱,而3857是用于地圖可視化的Web墨卡托平面投影坐標(biāo)系,如圖3所示变秦。
SQL語句中蹦玫,通過一系列的函數(shù)調(diào)用赎婚,將一張瓦片對應(yīng)的矢量數(shù)據(jù)查詢出來刘绣、將地理坐標(biāo)轉(zhuǎn)投影坐標(biāo)、將投影坐標(biāo)轉(zhuǎn)像素坐標(biāo)挣输,然后將像素坐標(biāo)表示的矢量數(shù)據(jù)編碼便得到一張矢量瓦片纬凤,下面是每個(gè)函數(shù)的功能:
ST_Transform:該函數(shù)用于坐標(biāo)轉(zhuǎn)換,此處主要用于在基于球面的地理坐標(biāo)系(EPSG代碼為4326)和基于平面的投影坐標(biāo)系(EPSG代碼為3857)之間做轉(zhuǎn)換撩嚼。
ST_TileEnvelope(z, x, y):該函數(shù)用于生成瓦片在Web墨卡托平面上的正方形范圍停士。
ST_AsMVTGeom:該函數(shù)用于將Web墨卡托坐標(biāo)系下的矢量數(shù)據(jù)投影到瓦片的像素坐標(biāo)系中,即將矢量數(shù)據(jù)從平面坐標(biāo)系換到像素坐標(biāo)系中完丽,并且只保留與落在瓦片中的部分(如圖4中長方形矢量數(shù)據(jù)的陰影部分)恋技,extent參數(shù)是瓦片的像素寬高,buffer參數(shù)是瓦片向外擴(kuò)展的像素?cái)?shù)逻族,如圖4所示蜻底。向外擴(kuò)展的原因是為了避免相鄰?fù)咂丛谝黄饡r(shí),交界處的矢量數(shù)據(jù)在可視化效果上出現(xiàn)裂痕聘鳞,如果兩個(gè)相鄰?fù)咂衎uffer的重疊薄辅,則會(huì)消除裂痕。
ST_AsMVT:該函數(shù)將查詢出來的要素編碼成二進(jìn)制格式的矢量瓦片抠璃,編碼規(guī)則請參考MapBox定義的矢量瓦片標(biāo)準(zhǔn)站楚。
3. 動(dòng)態(tài)矢量切片服務(wù)開發(fā)
下面我們給出基于SpringBoot和Mybatis開發(fā)的動(dòng)態(tài)矢量切片的服務(wù)端代碼:
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.spring.accumulator.entity.handler.PointTypeHandler;
import lombok.Data;
import org.locationtech.jts.geom.Point;
/**
* 興趣點(diǎn)PO
*
* @author wangrubin
*/
@Data
@TableName(value = "t_poi", autoResultMap = true)
public class PoiPO {
/**
* ID
*/
private Integer id;
/**
* POI名稱
*/
private String name;
/**
* POI空間位置
*/
@JsonIgnore
@TableField(typeHandler = PointTypeHandler.class)
private Point geom;
}
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.spring.accumulator.entity.PoiPO;
import com.spring.accumulator.model.vo.VectorTile;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
/**
* POI表數(shù)據(jù)庫訪問層
*
* @author wangrubin
*/
@Mapper
public interface PoiMapper extends BaseMapper<PoiPO> {
@Select({"WITH mvtgeom AS (\n" +
" SELECT id, name, ST_AsMVTGeom(\n" +
" ST_Transform(geom, 3857),\n" +
" ST_TileEnvelope(#{z}, #{x}, #{y}), extent => 4096, buffer => 8) AS geom\n" +
" FROM t_region_poi\n" +
" WHERE geom && ST_Transform(ST_TileEnvelope(#{z}, #{x}, #{y}), 4326)\n" +
")\n" +
"SELECT ST_AsMVT(mvtgeom.*) as mvt\n" +
"FROM mvtgeom"})
VectorTile selectVectorTile(Integer z, Integer x, Integer y);
}
import com.spring.accumulator.dao.PoiMapper;
import com.spring.accumulator.model.vo.VectorTile;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/vector-tile")
public class VectorTileController {
@ApiOperation(value = "動(dòng)態(tài)矢量切片請求")
@ApiImplicitParams(value = {
@ApiImplicitParam(name = "z", value = "縮放等級", required = true),
@ApiImplicitParam(name = "y", value = "瓦片行號(hào)", required = true),
@ApiImplicitParam(name = "x", value = "瓦片列號(hào)", required = true)
})
@GetMapping("/tile/{z}/{y}/{x}.pbf")
public void listPerson(@PathVariable Integer z,
@PathVariable Integer y,
@PathVariable Integer x,
HttpServletResponse response) {
try {
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");
VectorTile vectorTile = poiMapper.selectVectorTile(z, x, y);
response.getOutputStream().write(vectorTile.getMvt());
System.out.println(z + "-" + y + "-" + x + ":" + vectorTile.getMvt().length);
} catch (Exception e) {
// 重置response
log.error("文件下載失敗" + e.getMessage());
throw new RuntimeException("下載文件失敗", e);
}
}
@Resource
private PoiMapper poiMapper;
}
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class VectorTile {
byte[] mvt;
}
4. QGIS演示
在本地啟動(dòng)服務(wù),端口設(shè)為8080鸡典。QGIS提供了利用矢量瓦片來渲染電子地圖的功能源请,如圖5所示,配置url為:http://localhost:8080/vector-tile/tile/{z}/{y}/{x}.pbf