本文所涉及技术与Geotrellis并无太大关系,仅是矢量瓦片前端渲染和加载技术,但是其实我这是在为Geotrellis的矢量瓦片做铺垫。很多人可能会说,Geotrellis为什么要搞矢量瓦片,这不就是前端展示吗。其实不然,首先Geotrellis可以用分布式技术进行快速矢量瓦片切割,当然这不是主要的,因为单台服务器基本也能很快处理矢量瓦片的切割,重要的是Geotrellis可以使用矢量瓦片进行空间计算,这样可以矢栅一体化,矢量瓦片和栅格瓦片同时进行计算,这个东西就厉害了,将大大的提高空间数据分析的可能性。当然这只是我个人的看法,有待后续研究,并且Geotrellis的矢量瓦片还并在测试当中。本文仅介绍前端矢量瓦片技术。
目前高德、百度等互联网地图基本都使用了矢量瓦片技术。先来看一下Wiki中的介绍:
Vector tiles, tiled vectors or vectiles are packets of geographic data, packaged into pre-defined roughly-square shaped "tiles" for transfer over the web. This is an emerging method for delivering styled web maps, combining certain benefits of pre-rendered raster map tiles with vector map data. As with the widely used raster tiled web maps, map data is requested by a client as a set of "tiles" corresponding to square areas of land of a pre-defined size and location. Unlike raster tiled web maps, however, the server returns vector map data, which has been clipped to the boundaries of each tile, instead of a pre-rendered map image.
There are several major advantages of this hybrid approach. Compared to an un-tiled vector map, the data transfer is reduced,because only data within the current viewport, and at the current zoom level needs to be transferred. The GIS clipping operations can all be performed in advance, as the tile boundaries are pre-defined. This in turn means that tiled vector data can be packaged up and distributed, without needing any kind of GIS system available to serve data.
Compared to a tiled raster map, data transfer is also greatly reduced, as vector data is typically much smaller than a rendered bitmap. Also, styling can be applied later in the process, or even in the browser itself, allowing much greater flexibility in how data is presented. It is also easy to provide interactivity with map features, as their vector representation already exists within the client. Yet another benefit is that less centralised server processing power is required, since rasterisation can be performed directly in the client. This has been described as making "rendering ... a last-mile problem, with fast, high-quality GPU[s] in everyone’s pocket".
简单的说就是将矢量直接切割成如栅格瓦片一样大小的块,这种切割同样是按照空间来进行的。优势就是在于继承了栅格瓦片的所有优点后,还不需要事先定义样式进行矢量数据栅格化,能够在用户浏览器随意配置显示样式,减轻服务器端计算压力,缩小服务端存储空间(栅格图片占用大量存储空间),并且可以实现用户交互。
这些就是矢量瓦片的优势,当然不是说矢量瓦片绝对是个好东西,任何事情都要辩证的区看待,对待任何问题都要深入研究,找出最优解。如栅格数据(遥感影像等)永远需要使用栅格瓦片,某些不需要交互、不怎么变化等情况的矢量数据也可以使用栅格瓦片。
矢量瓦片的生成还未研究,本文只是调用OSM公开发布的矢量瓦片进行前端展示试验。
目前开源中矢量瓦片做的比较好的是Mapbox,各种渲染技术也基本以Mapbox定义的矢量瓦片标准为标准。Leaflet有多款插件支持矢量瓦片,Leaftlet是一款开源的前端地图渲染引擎,主要支持的是栅格瓦片。综合分析之后我选用了Leaflet.VectorGrid插件进行矢量瓦片的渲染,Github地址https://github.com/IvanSanchez/Leaflet.VectorGrid。
除了正常的Leftlet所需的js以及css文件外(具体请自行搜索),还需添加一下语句引入vectorgrid的js文件。
<script src="https://unpkg.com/leaflet.vectorgrid@1.2.0"></script>
当然你可以直接将此文件下载到本地引入。在Github中也有相应的示例可以参考。
OSM有一套可以直接调用的矢量瓦片,在这里我们以此数据为演示,将其添加到地图中,并实现交互。
var map = L.map('map');
var openmaptilesUrl = "https://free-{s}.tilehosting.com/data/v3/{z}/{x}/{y}.pbf.pict?key={key}";
var openmaptilesVectorTileOptions = {
rendererFactory: L.canvas.tile,
attribution: '<a href="https://openmaptiles.org/">© OpenMapTiles</a>, <a href="http://www.openstreetmap.org/copyright">© OpenStreetMap</a> contributors',
vectorTileLayerStyles: osm_poi_style,
subdomains: '0123',
interactive: true, // Make sure that this VectorGrid fires mouse/pointer events
key: '5iCgspbpUIw5lEYGLbGj',
maxZoom: 16
};
var openmaptilesPbfLayer = L.vectorGrid.protobuf(openmaptilesUrl, openmaptilesVectorTileOptions).addTo(map)
.on('click', function(e) { // The .on method attaches an event handler
L.popup()
.setContent((e.layer.properties.name || e.layer.properties.type) + "<br/>" + e.layer.properties.class)
.setLatLng(e.latlng)
.openOn(map);
L.DomEvent.stop(e);
});
openmaptilesUrl为OSM矢量瓦片请求地址,openmaptilesVectorTileOptions为矢量瓦片的相应配置,其中最重要的就是vectorTileLayerStyles,其表示矢量瓦片的渲染规则,矢量瓦片传送的只是矢量数,那么渲染就要由前端完成,这个变量定义的就是渲染规则,如点线面显示成什么颜色以及不同的要素渲染成什么形状颜色以及如何交互等,均在此变量中设置。osm_poi_style定义如下:
var osm_poi_style= {
poi: {icon: new L.Icon.Default()},
water: {
fill: true,
weight: 1,
fillColor: '#06cccc',
color: '#06cccc',
fillOpacity: 0.2,
opacity: 0.4,
},
admin: {
weight: 1,
fillColor: 'pink',
color: 'pink',
fillOpacity: 0.2,
opacity: 0.4
},
waterway: {
weight: 1,
fillColor: '#2375e0',
color: '#2375e0',
fillOpacity: 0.2,
opacity: 0.4
},
landcover: {
fill: true,
weight: 1,
fillColor: '#53e033',
color: '#53e033',
fillOpacity: 0.2,
opacity: 0.4,
},
landuse: {
fill: true,
weight: 1,
fillColor: '#e5b404',
color: '#e5b404',
fillOpacity: 0.2,
opacity: 0.4
},
park: {
fill: true,
weight: 1,
fillColor: '#84ea5b',
color: '#84ea5b',
fillOpacity: 0.2,
opacity: 0.4
},
boundary: {
weight: 1,
fillColor: '#c545d3',
color: '#054b96',
fillOpacity: 0.2,
opacity: 0.4
},
aeroway: {
weight: 1,
fillColor: '#51aeb5',
color: '#51aeb5',
fillOpacity: 0.2,
opacity: 0.4
},
road: { // mapbox & mapzen only
weight: 1,
fillColor: '#f2b648',
color: '#f2b648',
fillOpacity: 0.2,
opacity: 0.4
},
tunnel: { // mapbox only
weight: 0.5,
fillColor: '#f2b648',
color: '#f2b648',
fillOpacity: 0.2,
opacity: 0.4,
// dashArray: [4, 4]
},
bridge: { // mapbox only
weight: 0.5,
fillColor: '#f2b648',
color: '#f2b648',
fillOpacity: 0.2,
opacity: 0.4,
// dashArray: [4, 4]
},
transportation: { // openmaptiles only
weight: 0.5,
fillColor: '#f2b648',
color: '#f2b648',
fillOpacity: 0.2,
opacity: 0.4,
// dashArray: [4, 4]
},
transit: { // mapzen only
weight: 0.5,
fillColor: '#f2b648',
color: '#f2b648',
fillOpacity: 0.2,
opacity: 0.4,
// dashArray: [4, 4]
},
building: {
fill: true,
weight: 1,
fillColor: '#2b2b2b',
color: '#2b2b2b',
fillOpacity: 0.2,
opacity: 0.4
},
water_name: {
weight: 1,
fillColor: '#022c5b',
color: '#022c5b',
fillOpacity: 0.2,
opacity: 0.4
},
transportation_name: {
weight: 1,
fillColor: '#bc6b38',
color: '#bc6b38',
fillOpacity: 0.2,
opacity: 0.4
},
place: {
weight: 1,
fillColor: '#f20e93',
color: '#f20e93',
fillOpacity: 0.2,
opacity: 0.4
},
housenumber: {
weight: 1,
fillColor: '#ef4c8b',
color: '#ef4c8b',
fillOpacity: 0.2,
opacity: 0.4
},
poi: {
weight: 1,
fillColor: '#3bb50a',
color: '#3bb50a',
fillOpacity: 0.2,
opacity: 0.4
},
earth: { // mapzen only
fill: true,
weight: 1,
fillColor: '#c0c0c0',
color: '#c0c0c0',
fillOpacity: 0.2,
opacity: 0.4
},
// Do not symbolize some stuff for mapbox
country_label: [],
marine_label: [],
state_label: [],
place_label: [],
waterway_label: [],
poi_label: [],
road_label: [],
housenum_label: [],
// Do not symbolize some stuff for openmaptiles
country_name: [],
marine_name: [],
state_name: [],
place_name: [],
waterway_name: [],
poi_name: [],
road_name: [],
housenum_name: []
};
其中不同的对象有不同的渲染规则,而第一行的poi: {icon: new L.Icon.Default()}表示对poi这个属性进行特别渲染,渲染成一个Icon图标,当用户点击此图标的时候即可根据上面定义的on方法中的内容来进行交互。再来看一下on方法中的内容:
L.popup()
.setContent((e.layer.properties.name || e.layer.properties.type) + "<br/>" + e.layer.properties.class)
.setLatLng(e.latlng)
.openOn(map);
L.DomEvent.stop(e);
L.popup表示弹出一个提示框,setContent表示提示框中的内容,这个根据矢量瓦片中的数据内容和自己的业务需求具体修改。setLatLng表示提示框显示的位置,此处表示当前点的位置,也可以修改。当然其实我们也完全可以在on函数中实现更复杂的逻辑,如查询数据库获取更多信息进行显示等,具体根据自己的业务而定。来看一下显示的具体效果。
可以看到交互的图标以及交互信息,当然后面的数据也都是矢量瓦片在前端时时渲染的。矢量瓦片显示很流畅,交互也都很顺利。总之此插件效果不错。
我们知道了如何在前端进行矢量瓦片渲染,下面来看一下矢量瓦片的具体内容,当我们下载一幅矢量瓦片时可以看到其中都是二进制数据,这是为了减小传输压力进行的压缩,也有一些开源的软件可以进行解压缩,如https://github.com/bertt/mapbox-vector-tile-cs。
解析后的部分数据内容如下(只取出了属性等数据):
water
----Polygon
--------class lake
----Polygon
--------class lake
----Polygon
--------class lake
----Polygon
--------class lake
waterway
----LineString
--------class stream
--------name Molly Ann Brook
--------name:latin Molly Ann Brook
--------name_de Molly Ann Brook
--------name_en Molly Ann Brook
--------name_int Molly Ann Brook
landcover
----MultiPolygon
--------class wood
--------subclass wood
mountain_peak
----Point
--------ele 268
--------ele_ft 879
--------name High Mountain
--------name:latin High Mountain
--------name_de High Mountain
--------name_en High Mountain
--------name_int High Mountain
--------osm_id 357723234
--------rank 1
boundary
----LineString
--------admin_level 8
--------disputed 0
--------maritime 0
place
----Point
--------class village
--------name North Haledon
--------name:latin North Haledon
--------name_de North Haledon
--------name_en North Haledon
--------name_int North Haledon
--------rank 11
housenumber
----Point
--------housenumber 558
----Point
--------housenumber 65
poi
----Point
--------class school
--------name Memorial Elementary School
--------name:latin Memorial Elementary School
--------name_de Memorial Elementary School
--------name_en Memorial Elementary School
--------name_int Memorial Elementary School
--------rank 1
--------subclass school
----Point
--------class grocery
--------name Super Foodtown
--------name:latin Super Foodtown
--------name_de Super Foodtown
--------name_en Super Foodtown
--------name_int Super Foodtown
--------rank 1
--------subclass supermarket
----Point
--------class place_of_worship
--------name Temple Emanuel of North Jersey
--------name:latin Temple Emanuel of North Jersey
--------name_de Temple Emanuel of North Jersey
--------name_en Temple Emanuel of North Jersey
--------name_int Temple Emanuel of North Jersey
--------rank 2
--------subclass place_of_worship
可以看出其中确实包含了多种数据类型,water、boundary、poi等,各种类型下面有空间属性也有一些class、name等属性。主要来看一下poi,可以看出下面有多个点,每个点有分类以及name等,刚刚我在提示框中显示的正是class和name信息。
本文简单讲述了矢量瓦片技术,期待Geotrellis的矢量瓦片早日上线,这样就能验证我矢栅一体化的猜想,真正的统合所有空间数据,进行统一基准下的空间运算。
Geotrellis系列文章链接地址http://www.cnblogs.com/shoufengwei/p/5619419.html