最近由于在工作中涉及到了海量图形渲染的问题,因此我开始研究相关的解决方案。在咨询了许多朋友之后发现矢量切片似乎是行业内最常用的一种解决方案,于是我便开始研究它该如何使用。
一、什么是矢量切片
矢量切片按照我的理解就是用栅格切片的方式把矢量数据也切成金字塔,只不过切割的不是栅格图片,而是矢量数据的描述性文件。
矢量切片的特点
因此矢量切片它就兼具矢量数据与栅格数据的优点,比如说:
- 矢量瓦片相比于栅格图片更加灵活,可以直接访问矢量要素,因为矢量数据是以要素为单位的,而栅格数据它就是一个图片那就难以直接访问到具体的要素了。
- 可直接在客户端获取请求指定地物的信息,无须再次请求服务器。因为空间数据和属性数据一起被请求到客户端了,无需再次请求了。
- 样式可改变和定制。因为我们拿到的只是矢量数据,因此就可以在客户端自由的给它设置样式。
- 相比于原始矢量数据,矢量瓦片更小巧,进行了重新编码并切分,在被请求时可以只返回请求区域和相应级别的数据。这个优点也就是我想使用它的理由,我期望矢量切片可以帮助我解决海量图形渲染的问题。
- 数据更新快,甚至可以说是实时的,当数据库中的空间数据变化后,再次请求的数据是更新后的数据。
二、发布矢量切片服务
如何发布矢量瓦片服务,具体要看你使用的是什么WebGIS服务器,不同的WebGIS服务器操作的步骤可能都不一样,需要专门去查阅相关资料。我在实验的过程中是使用GeoServer来发布矢量切片的。
GeoServer中发布矢量切片
1.下载Vector Tiles 插件
GeoServer中无法直接实现矢量切片,需要下载对应的插件。
插件在官网中就可以下载(Download - GeoServer),但是注意要到自己所安装的对应geoserver版本的下载页面中去下载插件,如果下载了其它版本下的插件,可能会在启动geoserver时导致报错。
由于我安装的是2.21.5版本的geoserver,因此就进入这个版本的下载页面。
然后下滑到下面的 Extensions 部分就可以找到对应的插件。
2.安装Vector Tiles插件
将下载的插件解压之后得到如下的这些文件,复制其中的.jar
文件(jar包)。
将复制好的文件放到geoserver的 WEB-INF/lib
目录下,这个目录中都是各种.jar
文件,比较好辨认。
3.检查Vector Tiles插件是否安装成功
安装插件成功后重新启动GeoServer,然后打开GeoServer的图层页面
然后随便选择一个矢量图层
然后进入Tile Caching页面
滚动页面到 "Tile Image Formats" 部分,除了标准的GIF/PNG/JPEG格式之外,如果你好看到以下的内容就表示插件安装成功了。
4.发布矢量切片
发布矢量切片的过程也很简单,首先将自己准备的数据创建为一个矢量图层(具体的步骤我就不多说了)。
这里我就随便选择了一个GeoServer中的美国人口的矢量面图
点击进入"图层编辑"页面,然后再进入"Tile Caching"选项卡
滚动页面到 "Tile Image Formats" 部分,勾选application/json;type=geojson
、application/json;type=topojson
和application/vnd.mapbox-vector-tile
这几个选项(注意:这三个选项对应了三种不同格式的矢量切片数据,我们每勾选一种就会GeoServer就会制作发布一种对应格式的矢量切片)
最后点击保存,一个带有矢量切片的图层就准备好了。
三、OpenLayers中加载矢量切片
1.加载TMS服务的矢量切片
网上绝大多数的文章中都是加载的TMS服务的矢量切片,这种加载方式看起来还是比较简单的,但是我在实际使用的时候却发现"困难重重"实际上用起来比较麻烦。
获取TMS服务地址
首先第一步我们要搞清楚我们所发布的矢量瓦片的TMS服务的url地址是什么。可以打开Goeserver去查看。
点击左上角的logo进入首页
在首页点击TMS的链接
进入的这个页面中就记录了目前我的这台GeoServer服务器上发布的所有TMS服务的url。
找到之前设置了矢量切片的图层的链接(例如我下面的这个美国人口的图层)
这其中以geojson
、topojson
和pbf
结尾的url就是矢量切片的url。
然后就可以拿到一个类似于http://localhost:8080/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@geojson
这样的url。这个url由几个部分组成:
http://localhost:8080/geoserver/gwc/service/tms/1.0.0
这部分在同一个GeoServer服务器上是固定的。/topp%3Astates
这部分表示图层名,其中的%3A
是冒号的URL编码形式,所以我这里的图层就叫做topp:states
@EPSG%3A4326
这部分表示投影,我这使用的是 EPSG:4326@geojson
这部分表示数据的格式
一个完整的TMS的url还不止上面的这些,它应该还要包括切片的具体信息,例如:http://localhost:8080/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@geojson/3/2/1.geojson
后面的/3/2/1
分别代表切片的层级、列号和行号。
加载TMS矢量切片的方式
OpenLayers中加载矢量切片都需要使用VectorTile
图层 + VectorTile
数据源的方式来实现。
创建VectorTile
图层时可以通过style
属性给请求到的矢量瓦片数据设置样式。
创建VectorTile
数据源时则主要是要设置三个属性:
url
,即矢量瓦片服务的地址,由于OpenLayers中不支持直接请求TMS服务,所以需要通过请求XYZ服务的方式请求TMS,因此url要写成"http://localhost:8080/geoserver/gwc/service/tms/1.0.0/BeiJiang%3Abj@EPSG%3A4326@geojson/{z}/{x}/{-y}.geojson"
的形式,也就是用占位符{z}
、{x}
、{y}
来表示切片的层级、列号和行号。但是这里有一点特殊的地方在于使用的是{-y}
,之所以这样写是因为XYZ瓦片的y坐标从顶部开始向下递增,TMS瓦片的y坐标从底部开始向上递增,也就是说他们的y轴方向相反,所以url中要写成{-y}
进行转换。format
,这个属性是用来将矢量数据转换为Feature的,如果矢量切片的数据类型是geojson就使用GeoJSON
转换器,如果矢量切片的数据类型是pbf
就使用MVT
转换器(这些转换器都是OpenLayers封装好的的都在ol/format
目录下面)。tileGrid
,这个属性应该用来定义切片规则的,具体怎么定义的我也搞不懂。这里由于我们使用的是加载XYZ服务的方式,所以可以直接使用OpenLayers内置的createXYZ
函数来直接创建适合XYZ服务的tileGrid。但是在使用createXYZ
函数时特别要注意,其中的extent
属性默认为 EPSG:3857 投影的范围,如果你像我一样使用的是其它的投影(例如 EPSG:4326),那就必需要手动将这个属性设置为你所使用的投影的范围,否则请求的url将会404(不要问我是怎么知道的😭)
JavaScript
import { VectorTile as VectorTileLayer } from "ol/layer";
import { VectorTile as VectorTileSource } from "ol/source";
import { Style, Stroke, Circle as CircleStyle } from "ol/style";
import { GeoJSON} from "ol/format";
import { createXYZ } from "ol/tilegrid";
// 加载矢量切片
const riverVectorTileLayer = new VectorTileLayer({
source: new VectorTileSource({
url: "http://localhost:8080/geoserver/gwc/service/tms/1.0.0/BeiJiang%3Abj@EPSG%3A4326@geojson/{z}/{x}/{-y}.geojson",
format: new GeoJSON(),
tileGrid: new createXYZ({
extent: getProjection("EPSG:4326").getExtent(),
maxZoom: 18,
}),
}),
style: function (feature) {
const style = new Style({
stroke: new Stroke({
color: "blue",
width: 1,
}),
});
return style;
},
});
map.addLayer(riverVectorTileLayer);
但是我使用上面的这套代码进行加载却失败了,请求到的数据都是空的,这个就很奇怪,我看到很多的博客中都是这么写的,他们都能加载出来,我却加载不出来 ╭(╯^╰)╮。
之后,我历经了千辛万苦终于找到了原因出在哪里,我发现只要将层级z
减一就能够请求到正确的矢量切片数据,这就说明XYZ的层级跟TMS的层级好像是不匹配的,有可能XYZ切片的层级是从1开始计数的,而TMS切片的层级是从0开始计数的。
想要将z减一,就需要使用tileUrlFunction
属性,这个属性接收一个函数,然后XYZ切片的坐标就会作为参数传入到函数中,之后就可以根据XYZ切片的坐标来换算出TMS切片的坐标。
要换算的地方有两个,一是将z减一这个比较简单,二是要将y进行转换(因为它们的y轴是相反的)。
Bash
# 进行y轴转换的公式
y_tms = 最大行号 - y_xyz
# 因为
最大行号 = 总行数 - 1
# 所以
y_tms = 总行数 - 1 - y_xyz
# 又因为
总行数 = 2 ^ z_tms # 2的z次方
z_tms = z_xyz - 1
# 所以最终的公式为
y_tms = (2 ^ (z_xyz - 1)) - 1 - y_xyz
JavaScript
import { VectorTile as VectorTileLayer } from "ol/layer";
import { VectorTile as VectorTileSource } from "ol/source";
import { Style, Stroke, Circle as CircleStyle } from "ol/style";
import { GeoJSON} from "ol/format";
import { createXYZ } from "ol/tilegrid";
// 加载矢量切片
const riverVectorTileLayer = new VectorTileLayer({
source: new VectorTileSource({
tileUrlFunction: function (tileCoord) {
const [z, x, y] = tileCoord;
const url =
"http://localhost:8080/geoserver/gwc/service/tms/1.0.0/BeiJiang%3Abj@EPSG%3A4326@geojson/" +
(z - 1) +
"/" +
x +
"/" +
(Math.pow(2, z - 1) - 1 - y) +
".geojson";
return url;
},
format: new GeoJSON(),
tileGrid: new createXYZ({
extent: getProjection("EPSG:4326").getExtent(),
maxZoom: 18,
}),
}),
style: function (feature) {
const style = new Style({
stroke: new Stroke({
color: "blue",
width: 1,
}),
});
return style;
},
});
map.addLayer(riverVectorTileLayer);
通过上面的方式我最终才将我自己的矢量切片加载出来了
2.加载WMTS服务的矢量切片
之后我又看到了另一种加载方式,通过WMTS进行加载。WMTS加载起来那就比TMS要复杂很多了,但是其实是有一条"捷径"的。
在GeoServer中,进入切片图层页面。
选择一个带有矢量切片的矢量图层,在预览的下拉菜单中选择带有geojson
、topojson
或pbf
的选项就可以预览矢量切片
然后F12打开调试工具查看页面的源代码,在demo文件夹中就可以找到通过WMTS加载矢量切片的代码,直接照抄就可以了。
JavaScript
// 通过WMTS加载矢量切片
function addRiver_vectorTileWMTS() {
const baseUrl = "http://localhost:8080/geoserver/gwc/service/wmts";
const layerName = "BeiJiang:bj";
const style = "";
const gridsetName = "EPSG:4326";
const format = "application/json;type=geojson";
const resolutions = [
0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125, 0.02197265625,
0.010986328125, 0.0054931640625, 0.00274658203125, 0.001373291015625,
6.866455078125e-4, 3.4332275390625e-4, 1.71661376953125e-4,
8.58306884765625e-5, 4.291534423828125e-5, 2.1457672119140625e-5,
1.0728836059570312e-5, 5.364418029785156e-6, 2.682209014892578e-6,
1.341104507446289e-6, 6.705522537231445e-7, 3.3527612686157227e-7,
];
const params = {
REQUEST: "GetTile",
SERVICE: "WMTS",
VERSION: "1.0.0",
LAYER: layerName,
STYLE: style,
TILEMATRIX: gridsetName + ":{z}",
TILEMATRIXSET: gridsetName,
FORMAT: format,
TILECOL: "{x}",
TILEROW: "{y}",
};
let url = baseUrl + "?";
for (var param in params) {
url = url + param + "=" + params[param] + "&";
}
url = url.slice(0, -1);
riverVectorTileLayer = new VectorTileLayer({
source: new VectorTileSource({
url,
format: new GeoJSON(),
projection: "EPSG:4326",
tileGrid: new WMTSTileGrid({
extent: [-180, -90, 180, 90],
resolutions: resolutions,
matrixIds: [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
],
}),
}),
style: function (feature) {
const style = new Style({
stroke: new Stroke({
color: "blue",
width: 1,
}),
});
return style;
},
});
map.addLayer(riverVectorTileLayer);
}