地图渲染
24 Oct 2016什么是地图?
通俗的说地图就是地球的画像。
什么是地图渲染?
地图渲染给地图的画像的过程,就是按照一定的比例关系、投影方法,使用一定的符号,把现地球上的山川、道路、建筑等信息在纸或屏幕上描绘出来。
互联网地图的表达方式
投影
球面到平面。
假设地图中心有一个灯,用一张纸沿着地球赤道包裹起地球,这样地球上每个点都会映射到纸上,这样就得到摩卡拖坐标系。
X轴:由于赤道半径为6378137米,则赤道周长为2*PI*r = 2*20037508.3427892
,因此X轴的取值范围:[-20037508.3427892,20037508.3427892]
。
Y轴:由墨卡托投影的公式可知,同时上图也有示意,当纬度接近两极,即90°时,y值趋向于无穷。为了便于计算,“懒惰的工程师”就把Y轴的取值范围也限定在[-20037508.3427892,20037508.3427892]
之间,搞个正方形。
切片与分层
为了避免一次加载全部地图数据,实现按需加载地图,这里需要对地图进行分层和切片。
地图渲染的过程
- 获取数据
- 数据预处理
- 配置样式
- 渲染
获取数据
数据包括什么
地图使用点、线、面描绘现实世界,目前地图数据主要包括
- 背景数据
- 水系、绿地、铁路、行政区划等
- 道路数据
- 路网、限行、红绿灯、监控摄像头等
- POI(兴趣点)数据
- 门址数据
- 建筑物数据
- 行人导航数据
- 公共交通数据
- 等
如何获取数据
- 购买商业数据
- 基础数据:高德,四维图新,TomTom
- 实时路况:世纪高通,高德,北大千方
- 卫星图:四维、DigitalGlobe
- 室内地图:图聚,高德,蜂鸟,indoorAtlas
- 自己采集数据
- 使用开源数据 OSM
- 抓取数据
- 数据挖掘
数据预处理
- 确定各种数据的在那些级别显示
- 合并由于跨图幅而被切割掉的几何对象
- 生成线和面的标注
- 数据抽稀,在不影响显示效果的前提下减少数据中点的个数
- POI Rank
- POI名称简称
- 标注数据的避让预处理,过滤掉大量无法显示的标注
- 处理立交效果
- 多来源数据的融合与去重
数据抽稀 道格拉斯-普克算法
- 在曲线首尾两点间虚连一条直线,求出其余各点到该直线的距离。
- 选其最大者与阈值相比较,若大于阈值,则离该直线距离最大的点保留,否则将直线两端点间各点全部舍去。
- 依据所保留的点,将已知曲线分成两部分处理,重复第1、2步操作,迭代操作,即仍选距离最大者与阈值比较,依次取舍,直到无点可舍去,最后得到满足给定精度限差的曲线点坐标。
POI Rank
- 根据类型配置,通常是一些经验值,比如火车站就比餐馆更重要。
- 根据父子关系计算。
- 根据用户的搜索量。
几何对象相识度
面相似度 = A交B面积 / A并B面积
点和线也可以通过生成缓冲区转化成面来计算
配置样式
样式是来定义地图包含那些图层,这些图层使用哪些数据,在哪些层显示,怎么显示的。一个简单的样式示例如下
<Map background-color="blue" srs="+init=epsg:4326">
<Style name="My Style">
<Rule>
<PolygonSymbolizer fill="#f2eff9" />
<LineSymbolizer stroke="rgb(50%,50%,50%)" stroke-width="0.1" />
</Rule>
</Style>
<Layer name="world" srs="+init=epsg:4326">
<StyleName>My Style</StyleName>
<Datasource>
<Parameter name="file">path/to/shapefile.shp</Parameter>
<Parameter name="type">shape</Parameter>
</Datasource>
</Layer>
</Map>
渲染
渲染栅格瓦片
渲染的过程
- 逐个层次,逐个瓦片进行渲染(1到5步)。
- 从样式中逐个获取图层。
- 读取图层在当前瓦片范围的数据,并进过平移缩放转化成瓦片上的坐标。
- 准备画布画笔画刷等,逐条绘制几何数据。
- 最终生成渲染好的栅格瓦片地图。
- 并发布成地图服务。
最终提供瓦片地图服务(Tile Map Service)。前端根据当前的位置,计算需要哪些瓦片的数据,下载这些瓦片进行拼接。
并行渲染
由于瓦片的数量非常的多,如果切18级一共包括14亿多个小瓦片。这是非常巨大的运算量。由于每个渲染瓦片都是相对独立的,因此可以并行计算。同时为了减少计算的次数,可以一次渲染多个(16*16)瓦片。
我们当时使用Hadoop来进行并行渲染,也可以使用RabbitMQ这样的消息队列实现。Map对原始数据进行切片,Reduce进行渲染栅格瓦片。使用2000个节点、全国切图用时在5个小时以内,生成的瓦片在180G样子。
遇到的问题
- 难以得到最优化标注
- 卡在切割一个特别大面的单节点上。
- 瓦片边缘出现半个字。
- 线的在瓦片边缘接头处显示不全。
解决办法
- 当前使用的是贪心算法,先标注最重要的,先占先得,优点是速度快。尝试使用效果更优的Three Rules Suffice for Good Label Placement方法。
- 采用多次MapReduce逐对原始数据进行切片。
- 只允许标注在瓦片的内部。
- 把实际渲染的范围扩大一圈。
演进到矢量瓦片
由于栅格瓦片使用的数据流量大,不支持地图的旋转、3维效果,难以支持多种地图样式,加上前端的渲染性能已逐步提高,目前绝大多数的App已经是前端渲染。
与后端渲染不同的是,前端渲染在切图的过程中并不会生成栅格瓦片,而是生成矢量瓦片。然后由前端根据样式文件通过OpenGL ES或WebGL完成渲染。
矢量瓦片格式
矢量瓦片文件采用Google Protocol Buffers进行编码。它包括若干图层,每个图层包含若干要素。要素是由几何图形和属性信息组成的。
几何图形编码
使用瓦片坐标系,把浮点型坐标转化成整形坐标。瓦片的左上角是坐标系的原点。X轴向右为正,Y轴向下为正。
一个简单的矢量瓦片示例
layers {
version: 2
name: "points"
features: {
id: 1
tags: 0
tags: 0
type: Point
geometry: 9
geometry: 2410
geometry: 3080
}
keys: "hello"
values: {
string_value: "world"
}
extent: 4096
}
OpenGL渲染
难点
- 只支持渲染三角形,多边形需要自己进行三角化。
- 而对与线需要自己实现抗锯齿。
- 同时显示对于文字需要自己使用纹理贴图实现。
数据更新
由于一次渲染耗时耗时还是相对较长的。在对于新高速通车、新地铁通车等实时性更新要求比较高的情况下,可以增量更新部分区域的瓦片。如果数据有定期的增量更新包(Diff数据),也可以实现实时更新的地图。