动态矢量瓦片缓存库方案
目录
前言
二、实现步骤
1.将数据写入postgis数据库
2.将矢量瓦片数据写入缓存库
3.瓦片接口实现
4.瓦片局部更新接口实现
总结
前言
矢量瓦片作为webgis目前最优秀的数据格式,其主要特点就是解决了大批量数据在前端渲染时出现加载缓慢、卡顿的问题,能够环境前端设备的计算压力。动态矢量瓦片技术,解决了矢量存储在数据库中的实时动态更新,不再需要使用离线工具对矢量进行本地切片发布的问题。但是动态矢量瓦片技术的缺陷也很大,就是因为其运行逻辑是通过对数据库矢量实时切片,那么当用户访问并发数过多的时候,pg库就会超负荷运行,会出现访问超时的情况。为解决这一问题,搭建矢量瓦片缓存库就非常重要。
一、缓存库的意义
为解决多用户访问时pg库切片压力过大的问题,首先对数据库所有数据进行切片,然后将X,Y,Z和瓦片信息储存在缓存库中。当用户访问矢量瓦片接口时,优先判断缓存库内有无对应的瓦片数据,如果没有,则调用pg函数实时切片,切片完成后再缓存库插入数据,如果缓存库有数据,则直接返回缓存库储存的瓦片数据。如果需要对数据库的矢量进行增删改查操作,则计算更改矢量对应的瓦片范围,对缓存库做对应的局部更新即可。
二、实现步骤
1.将数据写入postgis数据库
对各大空间数据库读写最强工具,非FME莫属,直接写模块即可,需要注意一点就是如果。pg的表是MultiPolygon,那么我们需要加入aggregator把要素变为聚合体格式写入,还有就是坐标系要和表一致。
2.将矢量瓦片数据写入缓存库
将矢量数据写入一个临时的postgis数据表,然后用fme生成对应层级的瓦片范围,最后用python调用postgis函数对数据进行切片,最后过滤一下空白瓦片,最后将数据写入缓存库。
获得了各类层级的二进制矢量瓦片数据
3.瓦片接口实现
后端框架采用python的geodjango,首先造一个x,y,z转换为wg84坐标范围的函数
import math
def xyz2lonlat(x,y,z):
n = math.pow(2, z)
lon_deg = (x / n) * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - (2 * y) / n)));
lat_deg = (180 * lat_rad) / math.pi
return [lon_deg, lat_deg]
同时造一个mvt生成器,并实现空间库和缓存库信息判定,有则直接调用缓存库数据,不调用pg函数切片,无则调用pg函数切片,切片完成后更新缓存库并同时返回瓦片数据。
def make_mvt(model,temp_model,x,y,z):
"""temp_model为矢量瓦片缓存库模型类
model为pg矢量库模型要素
x,y,z为前端请求参数
"""
temp_mvt = temp_model.objects.filter(x=x,y=y,z=z)
if len(temp_mvt) == 1:
return HttpResponse(temp_mvt[0].byte, content_type="application/x-protobuf")
if len(temp_mvt) > 1:
for i in range(0, len(temp_mvt) - 1):
temp_mvt[i].delete()
return HttpResponse(temp_mvt[len(temp_mvt) - 1].byte, content_type="application/x-protobuf")
tablename = model._meta.db_table
boundbox_min = xyz2lonlat(x, y, z)
boundbox_max = xyz2lonlat(x + 1, y + 1, z)
sql = """SELECT
ST_AsMVT ( P,'polygon', 4096, 'geom' ) AS "mvt" FROM (SELECT
ST_AsMVTGeom (ST_Transform (st_simplify(geom,0.0), 3857 ), ST_Transform (ST_MakeEnvelope
( %s,%s, %s,%s, 4326 ),3857),
4096, 64,TRUE ) geom FROM "%s" ) AS P""" % (
boundbox_min[0], boundbox_min[1], boundbox_max[0], boundbox_max[1], tablename)
cursor = connection.cursor()
cursor.execute(sql)
tile = bytes(cursor.fetchone()[0])
temp_model.objects.create(
x=x,
y=y,
z=z,
byte=tile,
)
if not len(tile):
return False
else:
return tile
视图类
#视图类
class ZJGG_mvt_ViewSet(APIView):
def get(self,request,z, x, y):
tile=make_mvt(ZJGG,mvt_temp,x,y,z)
if tile:
return HttpResponse(tile, content_type="application/x-protobuf")
else:
return Response(status=status.HTTP_404_NOT_FOUND)
然后用postman调试一下接口,瓦片请求成功
4.瓦片局部更新接口实现
这一步的主要内容是为了前端需要对矢量数据增删改查的时候,同时删除缓存库的对应瓦片,用户在下一次访问的时候,通过上一步的mvt生成器完成新瓦片数据的更新。
首先造一个4326到3857坐标系的转换工具
import math
def lonlat2mercator(lon, lat):
semimajor_axis = 6378137.0
x = semimajor_axis * math.radians(lon)
y = semimajor_axis * math.log(math.tan((math.pi / 4) + (math.radians(lat) / 2)))
return x, y
def mercator2lonlat(x, y):
semimajor_axis = 6378137.0
lon = math.degrees(x / semimajor_axis)
lat = math.degrees(2 * math.atan(math.exp(y / semimajor_axis)) - math.pi / 2)
return lon, lat
def epsg4326_to_epsg3857(lon, lat):
x, y = lonlat2mercator(lon, lat)
r_major = 6378137.0
x = r_major * math.radians(lon)
scale = x / lon
y = 180.0 / math.pi * math.log(math.tan(math.pi / 4.0 + lat * (math.pi / 180.0) / 2.0)) * scale
return x, y
def epsg3857_to_epsg4326(x, y):
r_major = 6378137.0
lon = x / r_major * 180.0 / math.pi
lat = math.atan(math.exp(y / r_major)) * 360.0 / math.pi - 90.0
return lon, lat
造一个瓦片计算器,传入geojson的extend返回瓦片的x,y,z信息。
import math
HEMI_MAP_WIDTH = math.pi * float(6378137)
PRECISION = 6
def generate(zoomLevel, tileSize, rows, cbeg, cend):
# Calculate x-direction tile origins
cols = [(c, round(c * tileSize - HEMI_MAP_WIDTH, PRECISION)) for c in range(cbeg, cend + 1)]
cols = [(cols[i][0], cols[i][1], cols[i + 1][1]) for i in range(len(cols) - 1)]
tile_json=[]
# Generate and output tile features.
for row, ymin, ymax in rows:
for column, xmin, xmax in cols:
tile_json.append({
"Z": zoomLevel,
"X": column,
"Y": row,
})
return tile_json
def TileGenerate(xmin,ymin,xmax,ymax):
west = float(xmin)
east = float(xmax)
south = float(ymin)
north = float(ymax)
tile=[]
for i in range(6, 17):
zoomLevel = i
if zoomLevel < 0:
zoomLevel = 0
numColumns = int(math.pow(2, zoomLevel))
tileSize = 2.0 * HEMI_MAP_WIDTH / numColumns
rbeg = int(math.floor((HEMI_MAP_WIDTH - north) / tileSize))
rend = int(math.ceil((HEMI_MAP_WIDTH - south) / tileSize))
rows = [(r, round(HEMI_MAP_WIDTH - r * tileSize, PRECISION)) for r in range(rbeg, rend + 1)]
rows = [(rows[i][0], rows[i + 1][1], rows[i][1]) for i in range(len(rows) - 1)]
cbeg = int(math.floor((HEMI_MAP_WIDTH + west) / tileSize))
cend = int(math.ceil((HEMI_MAP_WIDTH + east) / tileSize))
if cbeg < cend:
tile_json=generate(zoomLevel, tileSize, rows, cbeg, cend)
else:
tile_json=generate(zoomLevel, tileSize, rows, cbeg, numColumns)
tile_json1=generate(zoomLevel, tileSize, rows, 0, cend)
tile_json.extend(tile_json1)
tile.extend(tile_json)
return tile
mvt局部更新函数
def del_mvt(model,temp_model,sm):
"""model为空间数据存储模型类
temp_model为缓存库模型类
sm为空间数据库查询结果
实现缓存库和空间库数据的局部删除
"""
data = make_geojson(model, sm)
data = json.loads(data)
xmin, ymin, xmax, ymax = bound(data)
xmin, ymin = epsg4326_to_epsg3857(xmin, ymin)
xmax, ymax = epsg4326_to_epsg3857(xmax, ymax)
tilelist = TileGenerate(xmin, ymin, xmax, ymax)
for i in tilelist:
DEL = temp_model.objects.filter(x=i["X"], y=i["Y"], z=i["Z"])
DEL.delete()
sm.delete()
视图函数
class ZJGG_mvt_del_ViewSet(APIView):
def post(self,request):
id = request.data.get('id')
sm=ZJGG.objects.filter(pk=id)
del_mvt(ZJGG,mvt_temp,sm)
return Response(status=status.HTTP_200_OK)
最后用postman测试接口,传入一个id,测试空间信息表和缓存表的对应内容是否都删除。
提交前
提交后
可以看到对应的空间库id为338的数据已经删除,同时缓存库中的对应的4条瓦片信息也被删。
总结
这项技术的前景是非常可观的,现阶段各类webgis平台对大批量地理空间数据的展现方式,几乎都为静态矢量瓦片和geojson配合的方式实现。但是这种方式在面对较大体量需要全局展示且需要动态更新的数据的时候,就显得捉襟见肘。