PostGIS動(dòng)態(tài)矢量切片(原理+實(shí)現(xiàn))

矢量數(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)扎狱。


圖1 瓦片金字塔

渲染時(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)換并編碼成矢量瓦片谴蔑。


圖2 動(dòng)態(tài)矢量瓦片請求示意圖

接下來我們詳細(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所示变秦。


圖3 地理坐標(biāo)系成榜、Web墨卡托投影坐標(biāo)系、與像素坐標(biāo)系

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)站楚。

圖4 矢量瓦片的參數(shù)示意圖

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

圖5 QGIS請求矢量瓦片渲染電子地圖的配置窗口

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彻况,一起剝皮案震驚了整個(gè)濱河市谁尸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纽甘,老刑警劉巖良蛮,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異悍赢,居然都是意外死亡决瞳,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門左权,熙熙樓的掌柜王于貴愁眉苦臉地迎上來皮胡,“玉大人,你說我怎么就攤上這事赏迟÷藕兀” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甩栈。 經(jīng)常有香客問我泻仙,道長,這世上最難降的妖魔是什么量没? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任玉转,我火速辦了婚禮,結(jié)果婚禮上殴蹄,老公的妹妹穿的比我還像新娘究抓。我一直安慰自己,他們只是感情好袭灯,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布漩蟆。 她就那樣靜靜地躺著,像睡著了一般妓蛮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上圾叼,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天蛤克,我揣著相機(jī)與錄音,去河邊找鬼夷蚊。 笑死构挤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惕鼓。 我是一名探鬼主播筋现,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼箱歧!你這毒婦竟也來了矾飞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤呀邢,失蹤者是張志新(化名)和其女友劉穎洒沦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體价淌,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡申眼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蝉衣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片括尸。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖病毡,靈堂內(nèi)的尸體忽然破棺而出濒翻,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布肴焊,位于F島的核電站前联,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏娶眷。R本人自食惡果不足惜似嗤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望届宠。 院中可真熱鬧烁落,春花似錦、人聲如沸豌注。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轧铁。三九已至每聪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間齿风,已是汗流浹背药薯。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留救斑,地道東北人童本。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像脸候,于是被迫代替她去往敵國和親穷娱。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容