目录
一、背景
矢量切片技术目前已成为互联网地图的主流技术,无论是Mapbox还是高德地图、百度地图,如今打开F12看到的数据源请求不是当年传统的一张张图片切片,而是一种protobuf格式的压缩的二进制数据,如下图:
矢量切片直接复用了基于XYZ的地图切片技术原理进行数据切片(这一点和栅格切片是一样的思路),切片数据使用谷歌的Protobuf进行数据压缩以优化网络传输效率,然后在客户端结合WebGL技术进行大量地理数据的渲染。Protobuf和WebGL自2015年以来在web地图领域已成为主流技术的核心部分,而在栅格切片的年代,前端还没有进入H5和WebGL的年代,受制于当时的技术用图片直接拼图就成了必然的选择了。
二、矢量切片
矢量切片和栅格切片一样的思路,以金字塔的方式切割矢量数据,只不过切割的不是栅格图片,而是矢量数据的描述性文件,目前矢量切片主要有以下三种格式:
- GeoJSON
- TopoJSON
- MapbBox Vector Tile(MVT)
矢量切片的主要优点有:
- 服务端只关注数据, 无需进行繁琐的配图;
- 网络传输快, 因为只有括矢量数据;
- 客户端渲染, 服务端的一套矢量数据, 在客户端可以有多种的表现形式;
- 充分利用客户端硬件
- 适配客户端屏幕, 根据屏幕解析度进行高精度矢量渲染;
- 利用 OpenGL/WebGL 实现海量空间数据渲染;
目前制作矢量切片的方式主要有:
- ArcGIS 系列产品:生成矢量切片包, 上传到 ArcGIS Portal 和 Server , 这套工具最完善, 但是也最贵;
- 开源的 GeoServer :在2.11beta版中出现了对矢量切片的支持,主要依赖于开源插件geoserver-2.11-SNAPSHOT-vectortiles-plugin以及内嵌的GeoWebcahce完成切片工作。适合熟悉GeoServer的用户,操作还比较简单,缺点是切片的行列号与一般的XYZ编号不同不容易单独部署,且不同geoserver稳定性不一致,笔者曾在某些版本部署崩溃无法应用。
- 基于tippecanoe的矢量切片工具方案,该工具提供了很多高级功能在数据定制化上有很强的优势,但只能部署在Linux,并不是跨平台,只能读取geojson文件,不能直连数据库,不是很好,如果有幸您是c++开发大神,可以改下库的编译绑定平台,使其支持windows,再更改下数据源底层,使其能支持空间数据库,那么该工具会有更多的应用空间。
-
Mapbox,目前已经提出了一套开放的矢量切片标准,并被多个开源团队所接受。
三、Mapbox的矢量切片格式
mvt全称Mapbox Vector Tile,是Mapbox定义的一种矢量瓦片标准,是一种轻量级的数据格式,用于存储地理空间矢量数据,例如点、线和多边形。矢量切片被编码为Google Protobufs (PBF)格式,允许序列化结构化数据。Mapbox 矢量切片使用.mvt
文件后缀。数据解析原理官方文档中介绍的比较详细。这种方式可以实现数据切片渲染。
mvt矢量切片规则:
四、PostGIS生成矢量切片
PostGIS 是关系数据库 PostgreSQL 的空间扩展, 提供了强大的空间数据查询和处理能力, 对矢量切片也提供了支持, 相关的函数有:
- ST_AsMVTGeom 将数据库存储的空间坐标转换为矢量切片坐标;
- ST_AsMVT 将矢量空间坐标聚合为符合矢量切片格式规范的二进制数据;
- ST_TileEnvelope 在 Web墨卡托坐标系 (SRID:3857) 下使用 xyz 切片架构 计算切片切片坐标范围;
ST_Transform
坐标转换函数
通过者上面这三个相关函数, 可以将数据库存储的空间数据快速转换成矢量切片标准的二进制数据。局限性是生成的切片是固定在3857坐标系下的。
ST_AsMVT:
用于将基于MapBox Vector Tile坐标空间的几何图形转换为MapBox VectorTile二进制矢量切片
- row —— 至少具有一个geometry列的行数据。
- name —— 图层名字,默认为"default"。
- extent —— 由MVT规范定义的屏幕空间(MVT坐标空间)中的矢量切片范围。
- geom_name —— row参数的行数据中geometry列的列名,默认是第一个*geometry类型的列。
- feature_id_name —— 行数据中要素ID列的列名。如果未指定或为NULL,则第一个有效数据类型(smallint, integer, bigint)的列将作为要素ID列,其他的列作为要素属性列。
ST_AsMVTGeom:
用于将一个图层中位于参数box2d范围内的一个几何图形的所有坐标转换为MapBox Vector Tile坐标空间里的坐标。
- geom —— 被转换的几何图形信息。
- bounds—— 某个矢量切片的范围对应的空间参考坐标系中的几何矩形框(没有缓冲区)。
- extent—— 是按规范定义的矢量切片坐标空间中的某个矢量切片的范围。如果为NULL,则默认为4096(边长为4096个单位的正方形)。
- buffer—— 矢量坐标空间中缓冲区的距离,位于该缓冲区的几何图形部位根据clip_geom参数被裁剪或保留。如果为NULL,则默认为256。
- clip_geom—— 用于选择位于缓冲区的几何图形部位是被裁剪还是原样保留。如果为NULL,则默认为true。
五、导入试验数据
首先导入试验数据到PostGIS的数据库中
六、编写PostGIS函数
在对应数据库的模式中添加函数
使用上面的三个函数编写实现代码:
CREATE OR REPLACE FUNCTION public.vector_tile_test(z integer, x integer, y integer, tn text, OUT tile bytea)
RETURNS bytea
LANGUAGE plpgsql
STRICT
AS $function$
DECLARE
bound geometry;
extent box2d;
sql text;
BEGIN
--ST_TileEnvelope函数得到的是epsg:3857坐标系,表是4326,需要坐标系转换。
bound:=ST_Transform(ST_TileEnvelope(z,x,y),4326);
extent:=Box2D(bound);
sql:='WITH mvtgeom AS(
SELECT ST_AsMVTGeom(the_geom, $1) AS geom,name FROM ' || tn || ' WHERE ST_Intersects(the_geom, $2)
) SELECT ST_AsMVT(mvtgeom.*,$3) FROM mvtgeom';
execute format(sql) using extent,bound,tn into tile;
RETURN;
END;
$function$
;
函数参数:切片的xy坐标,级别z,图层名tn
函数返回:mvt切片的二进制数组
七:Java后端实现
MapBoxVectorTileVO
@Data
@Entity
public class MapBoxVectorTileVO implements Serializable {
@Id
@Column(name = "num", nullable = false)
private String num;
@Column(name = "tile")
private byte[] tile;
}
MvtService
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.List;
@Service
public class MvtService {
@PersistenceContext
private EntityManager entityManager;
public byte[] vectorTitle(Integer z, Integer x, Integer y,String Tname) {
String sSQL = "select *,row_number() OVER (ORDER BY tile DESC ) as num from vector_tile_test("+z+", "+x+", "+y+",'"+Tname+"')";
Query query = entityManager.createNativeQuery(sSQL, MapBoxVectorTileVO.class);
List<MapBoxVectorTileVO> lstRe = query.getResultList();
System.out.println("获取完成");
return lstRe.get(0).getTile();
}
}
MvtController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@RestController
@CrossOrigin
@RequestMapping("/map/vectortile")
public class MvtController {
@Autowired
MvtService service;
@GetMapping("/{layer}/{z}/{x}/{y}.pbf")
public void vectorTitle2(@PathVariable("layer")String layer, @PathVariable("z")Integer z, @PathVariable("x") Integer x, @PathVariable("y") Integer y, HttpServletResponse response){
response.setContentType("application/x-protobuf;type=mapbox-vector;chartset=UTF-8");
byte[] tile = service.vectorTitle(z, x, y,layer);
// 输出文件流
OutputStream os = null;
InputStream is = null;
try {
is = new ByteArrayInputStream(tile);
os = response.getOutputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
八、Openlayers前端调用
前端使用openlayers进行加载渲染
import './style.css';
import {Map, View} from 'ol';
import TileLayer from 'ol/layer/Tile';
import BingMaps from 'ol/source/BingMaps';
import Control from 'ol/control/Control';
import ZoomSlider from 'ol/control/ZoomSlider.js';
import ScaleLine from 'ol/control/ScaleLine.js';
import VectorLayer from 'ol/layer/Vector';
import * as olSource from 'ol/source';
import * as olFormat from 'ol/format';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import VectorTile from "ol/layer/VectorTile";
import VectorSource from "ol/source/VectorTile";
import {MVT} from "ol/format";
import {Fill,Circle} from "ol/style";
import {createXYZ} from "ol/tilegrid";
const fill = new Fill({
color: 'rgba(255,255,255,0.4)',
});
const stroke = new Stroke({
color: 'rgba(186,30,243,0.88)',
width: 1.25,
});
const styles = [
new Style({
image: new Circle({
fill: fill,
stroke: stroke,
radius: 5,
}),
fill: fill,
stroke: stroke,
}),
];
let mvtLayer = new VectorTile({
source: new VectorSource({
format: new MVT(),
url: 'http://localhost:15106/map/vectortile/osm_places/{z}/{x}/{y}.pbf',
projection: 'EPSG:3857',
tileGrid: createXYZ({
extent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
resolutions: [156543.03392804097, 78271.51696402048, 39135.75848201024, 19567.87924100512, 9783.93962050256, 4891.96981025128, 2445.98490512564, 1222.99245256282, 611.49622628141, 305.748113140705, 152.8740565703525, 76.43702828517625, 38.218514142588125, 19.109257071294063, 9.554628535647031, 4.7773142678235156, 2.3886571339117578, 1.1943285669558789, 0.5971642834779394, 0.2985821417389697, 0.14929107086948485, 0.07464553543474242, 0.03732276771737121, 0.018661383858685606, 0.009330691929342803, 0.0046653459646714015, 0.0023326729823357008]
})
})
})
mvtLayer.setStyle(styles)
let mvtPolygonLayer = new VectorTile({
source: new VectorSource({
format: new MVT(),
url: 'http://localhost:15106/map/vectortile/osm_polygon/{z}/{x}/{y}.pbf',
projection: 'EPSG:3857',
tileGrid: createXYZ({
extent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
resolutions: [156543.03392804097, 78271.51696402048, 39135.75848201024, 19567.87924100512, 9783.93962050256, 4891.96981025128, 2445.98490512564, 1222.99245256282, 611.49622628141, 305.748113140705, 152.8740565703525, 76.43702828517625, 38.218514142588125, 19.109257071294063, 9.554628535647031, 4.7773142678235156, 2.3886571339117578, 1.1943285669558789, 0.5971642834779394, 0.2985821417389697, 0.14929107086948485, 0.07464553543474242, 0.03732276771737121, 0.018661383858685606, 0.009330691929342803, 0.0046653459646714015, 0.0023326729823357008]
})
}),
})
const map = new Map({
target: 'map',
layers: [mvtPolygonLayer],
view: new View({
center: [12970010,2854262], // 设置视图中心点
projection: 'EPSG:3857', // 配置投影坐标系
zoom: 12, // 默认缩放级别
})
});
map.addLayer(mvtLayer)
map.addControl(new ZoomSlider());
map.addControl(new ScaleLine());
调用结果:
附:
在线查看mvt切片文件(将mvt格式或pbf格式文件拖到地图框内即可查看):Custom Drag-and-Drop (MVT preview)
参考链接:
GIS动态矢量切片(MVT——MapBox Vector Tile)
- 后端代码: github.com/lukeSuperCo…
- 前端地图框架:github.com/lukeSuperCo…